kimhyunwoo commited on
Commit
f5d0a5c
·
verified ·
1 Parent(s): 5c0aefa

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +110 -101
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
- <title>Refined Piano Visualizer</title>
7
  <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
8
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/MidiConvert.min.js"></script>
9
  <style>
@@ -12,16 +12,16 @@
12
  --piano-bg-start: #1c1c1c;
13
  --piano-bg-end: #0d0d0d;
14
  --piano-border: #2f2f2f;
15
- --white-key-bg-start: #fafafa;
16
- --white-key-bg-end: #dedede;
17
- --white-key-border: #999999;
18
- --black-key-bg-start: #333333;
19
- --black-key-bg-end: #1a1a1a;
20
  --black-key-border: #050505;
21
- --hit-line-color: rgba(255, 60, 180, 0.75);
22
- --key-active-glow: rgba(255, 60, 180, 0.85);
23
- --particle-base-color-h: 320; /* Magenta-Pink */
24
- --default-note-color-h: 180; /* Cyan */
25
  }
26
 
27
  body {
@@ -42,10 +42,10 @@
42
  left: 50%;
43
  transform: translateX(-50%);
44
  z-index: 100;
45
- background-color: rgba(28, 28, 28, 0.9);
46
  padding: 10px 15px;
47
- border-radius: 6px;
48
- box-shadow: 0 2px 6px rgba(0,0,0,0.35);
49
  display: flex;
50
  align-items: center;
51
  }
@@ -53,9 +53,9 @@
53
  #controls input[type="text"] {
54
  padding: 9px 12px;
55
  margin-right: 8px;
56
- border: 1px solid #4a4a4a;
57
- background-color: #222222;
58
- color: #e8e8e8;
59
  border-radius: 4px;
60
  width: 320px;
61
  font-size: 14px;
@@ -70,21 +70,21 @@
70
  border: none;
71
  border-radius: 4px;
72
  transition: background-color 0.2s, box-shadow 0.2s;
73
- box-shadow: 0 2px 3px rgba(0,0,0,0.2);
74
  }
75
  #controls button:hover {
76
  background-color: #4cae4c;
77
  box-shadow: 0 3px 5px rgba(0,0,0,0.3);
78
  }
79
  #controls button:disabled {
80
- background-color: #444;
81
  cursor: not-allowed;
82
  box-shadow: none;
83
  }
84
  #loading-indicator {
85
- color: #ccc;
86
  font-size: 13px;
87
- margin-left: 10px;
88
  display: none;
89
  }
90
 
@@ -92,7 +92,7 @@
92
  width: 98vw;
93
  max-width: 1500px;
94
  height: 28vh;
95
- min-height: 180px;
96
  display: flex;
97
  justify-content: center;
98
  align-items: flex-end;
@@ -106,8 +106,8 @@
106
  background: linear-gradient(to bottom, var(--piano-bg-start), var(--piano-bg-end));
107
  padding: 10px 10px 0 10px;
108
  border-radius: 10px 10px 0 0;
109
- box-shadow: 0 0 30px rgba(160, 160, 255, 0.2),
110
- 0 0 50px rgba(255, 160, 210, 0.1);
111
  border: 2px solid var(--piano-border);
112
  border-bottom: none;
113
  height: 100%;
@@ -128,51 +128,52 @@
128
  background: linear-gradient(to bottom, var(--white-key-bg-start), var(--white-key-bg-end));
129
  border-left: 1px solid var(--white-key-border);
130
  border-right: 1px solid var(--white-key-border);
131
- border-bottom: 5px solid #959595;
132
  border-radius: 0 0 4px 4px;
133
- box-shadow: 0 2px 3px rgba(0,0,0,0.2), inset 0 -2px 2px rgba(255,255,255,0.65);
134
  z-index: 1;
135
  margin-right: -1px;
136
  }
137
- .white-key:first-child { border-left: 1px solid #707070; }
138
- .white-key:last-child { border-right: 1px solid #707070; margin-right: 0;}
139
 
140
  .black-key {
141
  width: 18px;
142
  height: 58%;
143
  background: linear-gradient(to bottom, var(--black-key-bg-start), var(--black-key-bg-end));
144
  border: 1px solid var(--black-key-border);
145
- border-bottom: 4px solid #1a1a1a;
146
  border-radius: 0 0 3px 3px;
147
  position: absolute;
148
  z-index: 2;
149
- box-shadow: -1px 0 2px rgba(0,0,0,0.4), 1px 0 2px rgba(0,0,0,0.4), 0 2px 3px rgba(0,0,0,0.5), inset 0 -1px 1px rgba(65,65,65,0.3);
150
  }
151
 
152
  .white-key.active {
153
- background: linear-gradient(to bottom, #d5d5d5, #c5c5c5);
154
- transform: perspective(500px) rotateX(1deg) translateY(1.5px);
155
- border-bottom-width: 3.5px;
156
- box-shadow: 0 0 20px 6px var(--key-active-glow),
157
- inset 0 -1px 1px rgba(255,255,255,0.35);
158
  }
159
 
160
  .black-key.active {
161
- background: linear-gradient(to bottom, #282828, #080808);
162
- transform: perspective(500px) rotateX(0.5deg) translateY(1px);
163
- border-bottom-width: 3px;
164
- box-shadow: 0 0 20px 6px var(--key-active-glow),
165
- inset 0 -1px 1px rgba(65,65,65,0.15);
166
  }
167
 
168
  #note-fall-area {
169
- width: 100%;
170
- height: calc(100vh - 28vh - 15px); /* Adjusted to align notes better with hit line */
171
  position: absolute;
172
  top: 0;
173
  left: 50%;
174
  transform: translateX(-50%);
175
  pointer-events: none;
 
176
  }
177
 
178
  .note-bar {
@@ -181,9 +182,9 @@
181
  border-radius: 3px;
182
  opacity: 0.9;
183
  background: var(--note-gradient);
184
- box-shadow: 0 0 12px var(--note-glow);
185
- border: 1px solid rgba(255, 255, 255, 0.2);
186
- will-change: transform; /* Performance hint */
187
  }
188
 
189
  #hit-line {
@@ -191,41 +192,42 @@
191
  bottom: calc(28vh + 5px);
192
  left: 50%;
193
  transform: translateX(-50%);
194
- width: var(--piano-actual-width); /* Dynamically set by JS */
195
- height: 4px;
 
196
  background: linear-gradient(to right, transparent, var(--hit-line-color), transparent);
197
- border-radius: 2px;
198
  z-index: 3;
199
- box-shadow: 0 0 18px var(--hit-line-color);
200
  }
201
 
202
  .particle-container {
203
  position: absolute;
204
- width: var(--piano-actual-width); /* Match piano width */
205
- height: 100%; /* Cover the piano area */
206
- top: calc(100% - 28vh - 5px); /* Align with top of piano keys */
207
  left: 50%;
208
  transform: translateX(-50%);
209
  pointer-events: none;
210
  z-index: 5;
211
- /* border: 1px dashed yellow; */ /* Debugging */
212
  }
213
  .particle {
214
  position: absolute;
215
- width: 7px; /* Increased size */
216
- height: 7px; /* Increased size */
217
  background-color: var(--particle-color);
218
  border-radius: 50%;
219
  opacity: 1;
220
- animation: particleAnim 0.8s cubic-bezier(0.1, 0.9, 0.6, 1) forwards;
221
- will-change: transform, opacity; /* Performance hint */
222
  }
223
  @keyframes particleAnim {
224
- 0% { transform: translateY(0) translateX(0) scale(1.3); opacity: 0.9; }
225
  100% {
226
- transform: translateY(calc( (var(--random-y) - 0.5) * -70px - 30px)) /* More upward and varied movement */
227
- translateX(calc( (var(--random-x) - 0.5) * 50px))
228
- scale(0.1);
229
  opacity: 0;
230
  }
231
  }
@@ -258,7 +260,7 @@
258
  const synth = new Tone.PolySynth(Tone.Synth, {
259
  oscillator: { type: "triangle8" },
260
  envelope: { attack: 0.015, decay: 0.35, sustain: 0.15, release: 0.9 },
261
- volume: -9
262
  }).toDestination();
263
 
264
  const NOTES_ORDER = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
@@ -272,6 +274,15 @@
272
  let whiteKeyCount = 0;
273
  let pianoActualWidth = 0;
274
 
 
 
 
 
 
 
 
 
 
275
  function createPianoKeys() {
276
  pianoElement.innerHTML = '';
277
  whiteKeyCount = 0;
@@ -290,26 +301,26 @@
290
 
291
  if (noteBase.includes('#')) {
292
  keyElement.classList.add('black-key');
293
- keyElement.style.left = (whiteKeyCount * WHITE_KEY_WIDTH_PX) - (BLACK_KEY_WIDTH_PX / 2) - (whiteKeyCount > 0 ? 0.5 : 0) + 'px';
294
  keyElement.style.top = '0px';
295
  } else {
296
  keyElement.classList.add('white-key');
 
297
  whiteKeyCount++;
298
  }
299
  pianoElement.appendChild(keyElement);
300
- pianoActualWidth += (noteBase.includes('#') ? 0 : WHITE_KEY_WIDTH_PX);
301
-
302
 
303
  const playNote = (e) => {
304
- if(e) e.preventDefault();
 
305
  keyElement.classList.add('active');
306
- const hue = parseFloat(keyElement.style.getPropertyValue('--key-active-glow-hue') || Math.random() * 360);
307
  keyElement.style.setProperty('--key-active-glow', `hsla(${hue}, 100%, 65%, 0.9)`);
308
  synth.triggerAttack(noteName, Tone.now());
309
  createKeyParticles(keyElement, `hsla(${hue}, 100%, 65%, 0.9)`);
310
  };
311
  const releaseNote = (e) => {
312
- if(e) e.preventDefault();
313
  keyElement.classList.remove('active');
314
  synth.triggerRelease(noteName, Tone.now() + 0.05);
315
  };
@@ -321,12 +332,9 @@
321
  keyElement.addEventListener('touchend', releaseNote);
322
  });
323
  }
324
- pianoActualWidth += (whiteKeyCount -1) * (-1); // Adjust for negative margins
325
  pianoElement.style.width = pianoActualWidth + 'px';
326
  document.documentElement.style.setProperty('--piano-actual-width', pianoActualWidth + 'px');
327
- noteFallArea.style.width = pianoActualWidth + 'px';
328
- particleContainer.style.width = pianoActualWidth + 'px';
329
- hitLineElement.style.width = pianoActualWidth + 'px';
330
  }
331
  createPianoKeys();
332
 
@@ -334,17 +342,16 @@
334
  const keyRect = keyElement.getBoundingClientRect();
335
  const particleContRect = particleContainer.getBoundingClientRect();
336
 
337
- for (let i = 0; i < 15; i++) { // Increased particle count
338
  const particle = document.createElement('div');
339
  particle.classList.add('particle');
340
  particle.style.setProperty('--particle-color', color);
341
  particle.style.setProperty('--random-x', Math.random());
342
  particle.style.setProperty('--random-y', Math.random());
343
 
344
-
345
  const xPos = (keyRect.left - particleContRect.left) + (keyRect.width / 2);
346
- const yPos = (keyRect.top - particleContRect.top) + (keyRect.height / 2);
347
-
348
 
349
  particle.style.left = `${xPos}px`;
350
  particle.style.top = `${yPos}px`;
@@ -365,12 +372,10 @@
365
  loadMidiButton.textContent = 'Load & Play MIDI';
366
  return;
367
  }
368
-
369
  if (!url) {
370
  alert("Please enter a MIDI URL.");
371
  return;
372
  }
373
-
374
  loadingIndicator.style.display = 'inline';
375
  loadMidiButton.disabled = true;
376
  loadMidiButton.textContent = 'Loading...';
@@ -423,9 +428,9 @@
423
  [147, 112, 219], [255, 255, 0], [0, 255, 255], [255,0,0]
424
  ];
425
  let colorIdx = 0;
426
- const fallAreaHeight = noteFallArea.clientHeight;
427
- const hitLineOffset = parseFloat(getComputedStyle(hitLineElement).bottom); // Get actual bottom position of hit line
428
-
429
 
430
  currentMidiEvents.forEach(noteData => {
431
  Tone.Transport.scheduleOnce(time => {
@@ -436,14 +441,13 @@
436
  noteElement.classList.add('note-bar');
437
 
438
  const keyRect = targetKeyElement.getBoundingClientRect();
439
- const pianoRect = pianoElement.getBoundingClientRect();
440
- const noteFallAreaRect = noteFallArea.getBoundingClientRect();
441
 
442
  const keyIsBlack = noteData.note.includes('#');
443
- noteElement.style.width = ((keyIsBlack ? BLACK_KEY_WIDTH_PX : WHITE_KEY_WIDTH_PX) - (keyIsBlack ? 1 : 2) ) + 'px'; // Slightly narrower than key
444
- noteElement.style.left = (keyRect.left - noteFallAreaRect.left + 1) + 'px'; // Align with key, considering borders
445
 
446
- const noteVisualHeight = Math.max(15, (noteData.duration / (60 / currentMidiTempo)) * 70); // 70px per beat height
447
  noteElement.style.height = noteVisualHeight + 'px';
448
 
449
  const [r, g, b] = noteColors[colorIdx % noteColors.length];
@@ -456,12 +460,11 @@
456
  noteElement.style.top = `-${noteVisualHeight}px`;
457
  noteFallArea.appendChild(noteElement);
458
 
459
- // Animate to the top edge of the hit line
460
- const targetY = fallAreaHeight - (hitLineOffset - parseFloat(getComputedStyle(pianoContainer).bottom)) - noteVisualHeight;
461
 
462
  noteElement.animate([
463
  { transform: `translateY(0px)` },
464
- { transform: `translateY(${targetY + noteVisualHeight}px)` } // End at bottom of note hitting top of hitline
465
  ], {
466
  duration: NOTE_FALL_DURATION_MS,
467
  easing: 'linear'
@@ -472,7 +475,7 @@
472
  Tone.Transport.scheduleOnce(hitTime => {
473
  synth.triggerAttackRelease(noteData.note, noteData.duration, hitTime, noteData.velocity);
474
  if (targetKeyElement) {
475
- targetKeyElement.style.setProperty('--key-active-glow-hue', (colorIdx * 45) % 360); // Cycle through hues
476
  targetKeyElement.classList.add('active');
477
  createKeyParticles(targetKeyElement, noteMainColor);
478
  setTimeout(() => {
@@ -491,20 +494,26 @@
491
  loadAndPlayMidiFromUrl(url);
492
  });
493
 
494
- window.addEventListener('load', () => {
495
- // Auto-load after a brief delay to ensure Tone.js is ready and user interaction might have occurred
496
- setTimeout(async () => {
497
- try {
498
- await Tone.start(); // Attempt to start audio context
499
- console.log("AudioContext started on load or by prior interaction.");
500
- loadAndPlayMidiFromUrl(midiUrlInput.value.trim());
501
- } catch (err) {
502
- console.warn("AudioContext could not be started automatically. User interaction might be needed.");
503
- }
504
- }, 500);
505
  });
506
 
507
- window.addEventListener('resize', createPianoKeys);
 
 
 
 
 
 
 
 
 
 
508
 
509
  </script>
510
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
+ <title>Refined Piano Visualizer with Keyboard Input</title>
7
  <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
8
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/MidiConvert.min.js"></script>
9
  <style>
 
12
  --piano-bg-start: #1c1c1c;
13
  --piano-bg-end: #0d0d0d;
14
  --piano-border: #2f2f2f;
15
+ --white-key-bg-start: #fdfdfd; /* Slightly brighter white */
16
+ --white-key-bg-end: #e4e4e4;
17
+ --white-key-border: #9e9e9e;
18
+ --black-key-bg-start: #353535;
19
+ --black-key-bg-end: #181818;
20
  --black-key-border: #050505;
21
+ --hit-line-color: rgba(255, 80, 180, 0.8);
22
+ --key-active-glow: rgba(255, 80, 180, 0.9);
23
+ --particle-base-color-h: 320;
24
+ --default-note-color-h: 180;
25
  }
26
 
27
  body {
 
42
  left: 50%;
43
  transform: translateX(-50%);
44
  z-index: 100;
45
+ background-color: rgba(30, 30, 30, 0.88);
46
  padding: 10px 15px;
47
+ border-radius: 7px;
48
+ box-shadow: 0 2px 7px rgba(0,0,0,0.4);
49
  display: flex;
50
  align-items: center;
51
  }
 
53
  #controls input[type="text"] {
54
  padding: 9px 12px;
55
  margin-right: 8px;
56
+ border: 1px solid #484848;
57
+ background-color: #252525;
58
+ color: #f0f0f0;
59
  border-radius: 4px;
60
  width: 320px;
61
  font-size: 14px;
 
70
  border: none;
71
  border-radius: 4px;
72
  transition: background-color 0.2s, box-shadow 0.2s;
73
+ box-shadow: 0 2px 3px rgba(0,0,0,0.25);
74
  }
75
  #controls button:hover {
76
  background-color: #4cae4c;
77
  box-shadow: 0 3px 5px rgba(0,0,0,0.3);
78
  }
79
  #controls button:disabled {
80
+ background-color: #484848;
81
  cursor: not-allowed;
82
  box-shadow: none;
83
  }
84
  #loading-indicator {
85
+ color: #c8c8c8;
86
  font-size: 13px;
87
+ margin-left: 12px;
88
  display: none;
89
  }
90
 
 
92
  width: 98vw;
93
  max-width: 1500px;
94
  height: 28vh;
95
+ min-height: 190px;
96
  display: flex;
97
  justify-content: center;
98
  align-items: flex-end;
 
106
  background: linear-gradient(to bottom, var(--piano-bg-start), var(--piano-bg-end));
107
  padding: 10px 10px 0 10px;
108
  border-radius: 10px 10px 0 0;
109
+ box-shadow: 0 0 35px rgba(170, 170, 255, 0.22),
110
+ 0 0 55px rgba(255, 170, 220, 0.12);
111
  border: 2px solid var(--piano-border);
112
  border-bottom: none;
113
  height: 100%;
 
128
  background: linear-gradient(to bottom, var(--white-key-bg-start), var(--white-key-bg-end));
129
  border-left: 1px solid var(--white-key-border);
130
  border-right: 1px solid var(--white-key-border);
131
+ border-bottom: 5px solid #909090;
132
  border-radius: 0 0 4px 4px;
133
+ box-shadow: 0 2px 3px rgba(0,0,0,0.22), inset 0 -2px 2px rgba(255,255,255,0.68);
134
  z-index: 1;
135
  margin-right: -1px;
136
  }
137
+ .white-key:first-child { border-left: 1px solid #686868; }
138
+ .white-key:last-child { border-right: 1px solid #686868; margin-right: 0;}
139
 
140
  .black-key {
141
  width: 18px;
142
  height: 58%;
143
  background: linear-gradient(to bottom, var(--black-key-bg-start), var(--black-key-bg-end));
144
  border: 1px solid var(--black-key-border);
145
+ border-bottom: 4px solid #181818;
146
  border-radius: 0 0 3px 3px;
147
  position: absolute;
148
  z-index: 2;
149
+ box-shadow: -1px 0 2px rgba(0,0,0,0.4), 1px 0 2px rgba(0,0,0,0.4), 0 2px 3px rgba(0,0,0,0.55), inset 0 -1px 1px rgba(60,60,60,0.3);
150
  }
151
 
152
  .white-key.active {
153
+ background: linear-gradient(to bottom, #d2d2d2, #c2c2c2);
154
+ transform: perspective(500px) rotateX(1.2deg) translateY(1.8px);
155
+ border-bottom-width: 3.2px;
156
+ box-shadow: 0 0 22px 6px var(--key-active-glow),
157
+ inset 0 -1px 1px rgba(255,255,255,0.38);
158
  }
159
 
160
  .black-key.active {
161
+ background: linear-gradient(to bottom, #222222, #020202);
162
+ transform: perspective(500px) rotateX(0.6deg) translateY(1.2px);
163
+ border-bottom-width: 2.8px;
164
+ box-shadow: 0 0 22px 6px var(--key-active-glow),
165
+ inset 0 -1px 1px rgba(60,60,60,0.18);
166
  }
167
 
168
  #note-fall-area {
169
+ width: var(--piano-actual-width, 100%); /* Default to 100% if not set */
170
+ height: calc(100vh - 28vh - 15px);
171
  position: absolute;
172
  top: 0;
173
  left: 50%;
174
  transform: translateX(-50%);
175
  pointer-events: none;
176
+ /* border: 1px solid red; */ /* For debugging layout */
177
  }
178
 
179
  .note-bar {
 
182
  border-radius: 3px;
183
  opacity: 0.9;
184
  background: var(--note-gradient);
185
+ box-shadow: 0 0 10px var(--note-glow);
186
+ border: 1px solid rgba(255, 255, 255, 0.18);
187
+ will-change: transform;
188
  }
189
 
190
  #hit-line {
 
192
  bottom: calc(28vh + 5px);
193
  left: 50%;
194
  transform: translateX(-50%);
195
+ width: var(--piano-actual-width, 98vw);
196
+ max-width: 1500px;
197
+ height: 3.5px;
198
  background: linear-gradient(to right, transparent, var(--hit-line-color), transparent);
199
+ border-radius: 1.75px;
200
  z-index: 3;
201
+ box-shadow: 0 0 16px var(--hit-line-color);
202
  }
203
 
204
  .particle-container {
205
  position: absolute;
206
+ width: var(--piano-actual-width, 100%);
207
+ height: 28vh; /* Height of the piano container */
208
+ bottom: 5px; /* Align with bottom of piano container */
209
  left: 50%;
210
  transform: translateX(-50%);
211
  pointer-events: none;
212
  z-index: 5;
213
+ /* border: 1px dashed lime; */ /* Debugging */
214
  }
215
  .particle {
216
  position: absolute;
217
+ width: 8px; /* Increased size */
218
+ height: 8px; /* Increased size */
219
  background-color: var(--particle-color);
220
  border-radius: 50%;
221
  opacity: 1;
222
+ animation: particleAnim 0.75s cubic-bezier(0.1, 0.9, 0.6, 1) forwards;
223
+ will-change: transform, opacity;
224
  }
225
  @keyframes particleAnim {
226
+ 0% { transform: translateY(0) translateX(0) scale(1.3); opacity: 0.95; }
227
  100% {
228
+ transform: translateY(calc( (var(--random-y) - 0.7) * -90px - 40px)) /* Adjusted upward movement */
229
+ translateX(calc( (var(--random-x) - 0.5) * 60px))
230
+ scale(0.15);
231
  opacity: 0;
232
  }
233
  }
 
260
  const synth = new Tone.PolySynth(Tone.Synth, {
261
  oscillator: { type: "triangle8" },
262
  envelope: { attack: 0.015, decay: 0.35, sustain: 0.15, release: 0.9 },
263
+ volume: -10
264
  }).toDestination();
265
 
266
  const NOTES_ORDER = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
 
274
  let whiteKeyCount = 0;
275
  let pianoActualWidth = 0;
276
 
277
+ const keyboardMapping = {
278
+ 'a': 'C4', 'w': 'C#4', 's': 'D4', 'e': 'D#4', 'd': 'E4', 'f': 'F4',
279
+ 't': 'F#4', 'g': 'G4', 'y': 'G#4', 'h': 'A4', 'u': 'A#4', 'j': 'B4',
280
+ 'k': 'C5', 'o': 'C#5', 'l': 'D5', 'p': 'D#5', ';': 'E5', "'": 'F5'
281
+ // Add more mappings as needed
282
+ };
283
+ const activeKeyboardKeys = new Set();
284
+
285
+
286
  function createPianoKeys() {
287
  pianoElement.innerHTML = '';
288
  whiteKeyCount = 0;
 
301
 
302
  if (noteBase.includes('#')) {
303
  keyElement.classList.add('black-key');
304
+ keyElement.style.left = (whiteKeyCount * WHITE_KEY_WIDTH_PX) - (BLACK_KEY_WIDTH_PX / 2) -0.5 + 'px';
305
  keyElement.style.top = '0px';
306
  } else {
307
  keyElement.classList.add('white-key');
308
+ pianoActualWidth += WHITE_KEY_WIDTH_PX;
309
  whiteKeyCount++;
310
  }
311
  pianoElement.appendChild(keyElement);
 
 
312
 
313
  const playNote = (e) => {
314
+ if(e && e.preventDefault) e.preventDefault();
315
+ if(keyElement.classList.contains('active')) return;
316
  keyElement.classList.add('active');
317
+ const hue = Math.random() * 360;
318
  keyElement.style.setProperty('--key-active-glow', `hsla(${hue}, 100%, 65%, 0.9)`);
319
  synth.triggerAttack(noteName, Tone.now());
320
  createKeyParticles(keyElement, `hsla(${hue}, 100%, 65%, 0.9)`);
321
  };
322
  const releaseNote = (e) => {
323
+ if(e && e.preventDefault) e.preventDefault();
324
  keyElement.classList.remove('active');
325
  synth.triggerRelease(noteName, Tone.now() + 0.05);
326
  };
 
332
  keyElement.addEventListener('touchend', releaseNote);
333
  });
334
  }
335
+ pianoActualWidth += (whiteKeyCount -1) * (-1);
336
  pianoElement.style.width = pianoActualWidth + 'px';
337
  document.documentElement.style.setProperty('--piano-actual-width', pianoActualWidth + 'px');
 
 
 
338
  }
339
  createPianoKeys();
340
 
 
342
  const keyRect = keyElement.getBoundingClientRect();
343
  const particleContRect = particleContainer.getBoundingClientRect();
344
 
345
+ for (let i = 0; i < 15; i++) {
346
  const particle = document.createElement('div');
347
  particle.classList.add('particle');
348
  particle.style.setProperty('--particle-color', color);
349
  particle.style.setProperty('--random-x', Math.random());
350
  particle.style.setProperty('--random-y', Math.random());
351
 
 
352
  const xPos = (keyRect.left - particleContRect.left) + (keyRect.width / 2);
353
+ // Particle origin slightly above the key
354
+ const yPos = (keyRect.top - particleContRect.top) - 10; // 10px above the key's top
355
 
356
  particle.style.left = `${xPos}px`;
357
  particle.style.top = `${yPos}px`;
 
372
  loadMidiButton.textContent = 'Load & Play MIDI';
373
  return;
374
  }
 
375
  if (!url) {
376
  alert("Please enter a MIDI URL.");
377
  return;
378
  }
 
379
  loadingIndicator.style.display = 'inline';
380
  loadMidiButton.disabled = true;
381
  loadMidiButton.textContent = 'Loading...';
 
428
  [147, 112, 219], [255, 255, 0], [0, 255, 255], [255,0,0]
429
  ];
430
  let colorIdx = 0;
431
+ const fallAreaRect = noteFallArea.getBoundingClientRect();
432
+ const hitLineRect = hitLineElement.getBoundingClientRect();
433
+ const hitLineTopRelativeToFallArea = hitLineRect.top - fallAreaRect.top;
434
 
435
  currentMidiEvents.forEach(noteData => {
436
  Tone.Transport.scheduleOnce(time => {
 
441
  noteElement.classList.add('note-bar');
442
 
443
  const keyRect = targetKeyElement.getBoundingClientRect();
444
+ const pianoElementRect = pianoElement.getBoundingClientRect();
 
445
 
446
  const keyIsBlack = noteData.note.includes('#');
447
+ noteElement.style.width = ((keyIsBlack ? BLACK_KEY_WIDTH_PX : WHITE_KEY_WIDTH_PX) - (keyIsBlack ? 1 : 2)) + 'px';
448
+ noteElement.style.left = (keyRect.left - fallAreaRect.left + 1) + 'px';
449
 
450
+ const noteVisualHeight = Math.max(15, (noteData.duration / (60 / currentMidiTempo)) * 70);
451
  noteElement.style.height = noteVisualHeight + 'px';
452
 
453
  const [r, g, b] = noteColors[colorIdx % noteColors.length];
 
460
  noteElement.style.top = `-${noteVisualHeight}px`;
461
  noteFallArea.appendChild(noteElement);
462
 
463
+ const targetY = hitLineTopRelativeToFallArea - noteVisualHeight;
 
464
 
465
  noteElement.animate([
466
  { transform: `translateY(0px)` },
467
+ { transform: `translateY(${targetY}px)` }
468
  ], {
469
  duration: NOTE_FALL_DURATION_MS,
470
  easing: 'linear'
 
475
  Tone.Transport.scheduleOnce(hitTime => {
476
  synth.triggerAttackRelease(noteData.note, noteData.duration, hitTime, noteData.velocity);
477
  if (targetKeyElement) {
478
+ targetKeyElement.style.setProperty('--key-active-glow-hue', (colorIdx * 40) % 360);
479
  targetKeyElement.classList.add('active');
480
  createKeyParticles(targetKeyElement, noteMainColor);
481
  setTimeout(() => {
 
494
  loadAndPlayMidiFromUrl(url);
495
  });
496
 
497
+ window.addEventListener('keydown', (event) => {
498
+ if (event.repeat || midiUrlInput === document.activeElement) return;
499
+ const note = keyboardMapping[event.key.toLowerCase()];
500
+ if (note && pianoKeys[note] && !activeKeyboardKeys.has(note)) {
501
+ activeKeyboardKeys.add(note);
502
+ pianoKeys[note].dispatchEvent(new MouseEvent('mousedown'));
503
+ }
 
 
 
 
504
  });
505
 
506
+ window.addEventListener('keyup', (event) => {
507
+ const note = keyboardMapping[event.key.toLowerCase()];
508
+ if (note && pianoKeys[note]) {
509
+ activeKeyboardKeys.delete(note);
510
+ pianoKeys[note].dispatchEvent(new MouseEvent('mouseup'));
511
+ }
512
+ });
513
+
514
+ window.addEventListener('resize', () => {
515
+ createPianoKeys();
516
+ });
517
 
518
  </script>
519
  </body>