kimhyunwoo commited on
Commit
714bb7a
·
verified ·
1 Parent(s): 33ef2c6

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +76 -29
index.html CHANGED
@@ -115,7 +115,7 @@
115
  position: relative;
116
  }
117
  .white-key {
118
- width: 30px;
119
  height: 100%;
120
  background: linear-gradient(to bottom, var(--white-key-bg-start), var(--white-key-bg-end));
121
  border-left: 1px solid var(--white-key-border);
@@ -124,23 +124,23 @@
124
  border-radius: 0 0 4px 4px;
125
  box-shadow: 0 2px 3px rgba(0,0,0,0.22), inset 0 -2px 2px rgba(255,255,255,0.68);
126
  z-index: 1;
127
- margin-right: -1px;
128
  }
129
  .white-key:first-child { border-left: 1px solid #686868; }
130
  .white-key:last-child { border-right: 1px solid #686868; margin-right: 0;}
131
  .black-key {
132
- width: 18px;
133
  height: 58%;
134
  background: linear-gradient(to bottom, var(--black-key-bg-start), var(--black-key-bg-end));
135
  border: 1px solid var(--black-key-border);
136
  border-bottom: 4px solid #181818;
137
  border-radius: 0 0 3px 3px;
138
  position: absolute;
139
- z-index: 5; /* Black keys normally above white keys */
140
  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);
141
  }
142
  .key.active { /* Applied when key is pressed by user or MIDI */
143
- z-index: 4 !important; /* Active keys always on top */
144
  }
145
  .white-key.active {
146
  background: linear-gradient(to bottom, #d5d5d5, #c2c2c2);
@@ -164,7 +164,6 @@
164
  left: 50%;
165
  transform: translateX(-50%);
166
  pointer-events: none;
167
- /* border: 1px solid red; */
168
  }
169
  .note-bar {
170
  position: absolute;
@@ -186,19 +185,18 @@
186
  height: 3.5px;
187
  background: linear-gradient(to right, transparent, var(--hit-line-color), transparent);
188
  border-radius: 1.75px;
189
- z-index: 3;
190
  box-shadow: 0 0 16px var(--hit-line-color);
191
  }
192
  .particle-container {
193
  position: absolute;
194
  width: var(--piano-actual-width, 100%);
195
- height: calc(28vh + 5px); /* Cover piano keys area and slightly above */
196
- bottom: 0; /* Align with bottom of the page (below piano-container) */
197
  left: 50%;
198
  transform: translateX(-50%);
199
  pointer-events: none;
200
- z-index: 5;
201
- /* border: 1px dashed lime; */
202
  }
203
  .particle {
204
  position: absolute;
@@ -244,57 +242,75 @@
244
  const loadMidiButton = document.getElementById('loadMidiButton');
245
  const loadingIndicator = document.getElementById('loading-indicator');
246
  const hitLineElement = document.getElementById('hit-line');
 
247
  const synth = new Tone.PolySynth(Tone.Synth, {
248
  oscillator: { type: "triangle8" },
249
  envelope: { attack: 0.015, decay: 0.35, sustain: 0.15, release: 0.9 },
250
  volume: -10
251
  }).toDestination();
 
252
  const NOTES_ORDER = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
253
  const START_OCTAVE = 1;
254
  const END_OCTAVE = 7;
255
  const WHITE_KEY_WIDTH_PX = 30;
256
  const BLACK_KEY_WIDTH_PX = 18;
 
 
257
  const NOTE_FALL_DURATION_MS = 3800;
258
  const pianoKeys = {};
259
  let whiteKeyCount = 0;
260
- let pianoActualWidth = 0;
 
261
  const keyboardMapping = {
262
  'a': 'C4', 'w': 'C#4', 's': 'D4', 'e': 'D#4', 'd': 'E4', 'f': 'F4',
263
  't': 'F#4', 'g': 'G4', 'y': 'G#4', 'h': 'A4', 'u': 'A#4', 'j': 'B4',
264
  'k': 'C5', 'o': 'C#5', 'l': 'D5', 'p': 'D#5', ';': 'E5', "'": 'F5'
265
  };
266
  const activeKeyboardKeys = new Set();
 
267
  function createPianoKeys() {
268
  pianoElement.innerHTML = '';
269
  whiteKeyCount = 0;
270
  pianoActualWidth = 0;
 
 
271
  for (let octave = START_OCTAVE; octave <= END_OCTAVE; octave++) {
272
  NOTES_ORDER.forEach((noteBase) => {
273
  if (octave === END_OCTAVE && noteBase !== 'C') return;
274
  if (octave === START_OCTAVE && !['A','A#','B'].includes(noteBase)) return;
 
275
  const keyElement = document.createElement('div');
276
  keyElement.classList.add('key');
277
  const noteName = noteBase + octave;
278
  keyElement.dataset.note = noteName;
279
  pianoKeys[noteName] = keyElement;
280
- if (noteBase.includes('#')) {
 
281
  keyElement.classList.add('black-key');
282
- keyElement.style.left = (whiteKeyCount * WHITE_KEY_WIDTH_PX) - (BLACK_KEY_WIDTH_PX / 2) -0.5 + 'px';
 
 
283
  keyElement.style.top = '0px';
284
- } else {
285
  keyElement.classList.add('white-key');
286
- pianoActualWidth += WHITE_KEY_WIDTH_PX;
 
 
 
 
287
  whiteKeyCount++;
288
  }
289
  pianoElement.appendChild(keyElement);
 
290
  const playNote = (e) => {
291
  if(e && e.preventDefault) e.preventDefault();
292
  if(keyElement.classList.contains('active')) return;
293
  keyElement.classList.add('active');
294
  const hue = Math.random() * 360;
295
- keyElement.style.setProperty('--key-active-glow', `hsla(${hue}, 100%, 65%, 0.9)`);
 
296
  synth.triggerAttack(noteName, Tone.now());
297
- createKeyParticles(keyElement, `hsla(${hue}, 100%, 65%, 0.9)`);
298
  };
299
  const releaseNote = (e) => {
300
  if(e && e.preventDefault) e.preventDefault();
@@ -308,11 +324,19 @@
308
  keyElement.addEventListener('touchend', releaseNote);
309
  });
310
  }
311
- pianoActualWidth += (whiteKeyCount -1) * (-1);
 
 
 
 
 
 
 
312
  pianoElement.style.width = pianoActualWidth + 'px';
313
  document.documentElement.style.setProperty('--piano-actual-width', pianoActualWidth + 'px');
314
  }
315
  createPianoKeys();
 
316
  function createKeyParticles(keyElement, color) {
317
  const keyRect = keyElement.getBoundingClientRect();
318
  const particleContRect = particleContainer.getBoundingClientRect();
@@ -322,7 +346,7 @@
322
  particle.style.setProperty('--particle-color', color);
323
  particle.style.setProperty('--random-x', Math.random());
324
  particle.style.setProperty('--random-y', Math.random());
325
- const xPos = (keyRect.left - particleContRect.left) + (keyRect.width / 2);
326
  const yPos = (keyRect.top - particleContRect.top) - 15; // Particles start slightly above the key
327
  particle.style.left = `${xPos}px`;
328
  particle.style.top = `${yPos}px`;
@@ -330,13 +354,16 @@
330
  setTimeout(() => particle.remove(), 800);
331
  }
332
  }
 
333
  let currentMidiEvents = [];
334
  let currentMidiTempo = 120;
 
335
  async function loadAndPlayMidiFromUrl(url) {
336
  if (Tone.Transport.state === 'started') {
337
  Tone.Transport.stop();
338
  Tone.Transport.cancel();
339
  document.querySelectorAll('.note-bar').forEach(n => n.remove());
 
340
  loadMidiButton.textContent = 'Load & Play MIDI';
341
  return;
342
  }
@@ -393,6 +420,7 @@
393
  const fallAreaRect = noteFallArea.getBoundingClientRect();
394
  const hitLineRect = hitLineElement.getBoundingClientRect();
395
  const hitLineTopRelativeToFallArea = hitLineRect.top - fallAreaRect.top;
 
396
  currentMidiEvents.forEach(noteData => {
397
  Tone.Transport.scheduleOnce(time => {
398
  const targetKeyElement = pianoKeys[noteData.note];
@@ -401,10 +429,12 @@
401
  noteElement.classList.add('note-bar');
402
 
403
  const keyRect = targetKeyElement.getBoundingClientRect();
404
-
405
  const keyIsBlack = noteData.note.includes('#');
406
- noteElement.style.width = ((keyIsBlack ? BLACK_KEY_WIDTH_PX : WHITE_KEY_WIDTH_PX) - (keyIsBlack ? 1 : 2)) + 'px';
407
- noteElement.style.left = (keyRect.left - fallAreaRect.left + 1) + 'px';
 
 
 
408
 
409
  const noteVisualHeight = Math.max(15, (noteData.duration / (60 / currentMidiTempo)) * 70);
410
  noteElement.style.height = noteVisualHeight + 'px';
@@ -418,36 +448,42 @@
418
  noteElement.style.top = `-${noteVisualHeight}px`;
419
  noteFallArea.appendChild(noteElement);
420
 
421
- const targetYForNoteTopToHitLine = hitLineTopRelativeToFallArea; // Note's top aligns with hit line's top
422
- const targetYForNoteBottomToPassHitLine = hitLineTopRelativeToFallArea + noteVisualHeight; // Note's bottom passes hit line
423
 
424
  const timeToHitLine = NOTE_FALL_DURATION_MS;
425
  const timeAfterHitLine = noteData.duration * 1000;
426
  const totalAnimationDuration = timeToHitLine + timeAfterHitLine;
 
427
  noteElement.animate([
428
- { transform: `translateY(0px)` }, // Start from above
429
- { transform: `translateY(${targetYForNoteTopToHitLine}px)`, offset: timeToHitLine / totalAnimationDuration }, // Note's top reaches hit line
430
- { transform: `translateY(${targetYForNoteBottomToPassHitLine}px)` } // Note's bottom passes hit line
431
  ], {
432
  duration: totalAnimationDuration,
433
  easing: 'linear'
434
  }).onfinish = () => {
435
  if (noteElement.parentNode) noteElement.remove();
436
  };
 
437
  Tone.Transport.scheduleOnce(hitTime => {
438
  synth.triggerAttackRelease(noteData.note, noteData.duration, hitTime, noteData.velocity);
439
  if (targetKeyElement) {
440
- targetKeyElement.style.setProperty('--key-active-glow-hue', (colorIdx * 40) % 360);
 
441
  targetKeyElement.classList.add('active');
442
  createKeyParticles(targetKeyElement, noteMainColor);
443
  setTimeout(() => {
444
  targetKeyElement.classList.remove('active');
 
 
445
  }, noteData.duration * 1000);
446
  }
447
  }, time + (NOTE_FALL_DURATION_MS / 1000));
448
  }, noteData.time);
449
  });
450
  }
 
451
  loadMidiButton.addEventListener('click', async () => {
452
  await Tone.start();
453
  const url = midiUrlInput.value.trim();
@@ -471,6 +507,17 @@
471
  });
472
  window.addEventListener('resize', () => {
473
  createPianoKeys();
 
 
 
 
 
 
 
 
 
 
 
474
  });
475
  </script>
476
  </body>
 
115
  position: relative;
116
  }
117
  .white-key {
118
+ width: 30px; /* WHITE_KEY_WIDTH_PX */
119
  height: 100%;
120
  background: linear-gradient(to bottom, var(--white-key-bg-start), var(--white-key-bg-end));
121
  border-left: 1px solid var(--white-key-border);
 
124
  border-radius: 0 0 4px 4px;
125
  box-shadow: 0 2px 3px rgba(0,0,0,0.22), inset 0 -2px 2px rgba(255,255,255,0.68);
126
  z-index: 1;
127
+ margin-right: -1px; /* WHITE_KEY_OVERLAP_PX (negated) */
128
  }
129
  .white-key:first-child { border-left: 1px solid #686868; }
130
  .white-key:last-child { border-right: 1px solid #686868; margin-right: 0;}
131
  .black-key {
132
+ width: 18px; /* BLACK_KEY_WIDTH_PX */
133
  height: 58%;
134
  background: linear-gradient(to bottom, var(--black-key-bg-start), var(--black-key-bg-end));
135
  border: 1px solid var(--black-key-border);
136
  border-bottom: 4px solid #181818;
137
  border-radius: 0 0 3px 3px;
138
  position: absolute;
139
+ z-index: 2; /* White keys: 1, Active keys: 3, Particles: 4 */
140
  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);
141
  }
142
  .key.active { /* Applied when key is pressed by user or MIDI */
143
+ z-index: 3 !important; /* Active keys always on top of other keys */
144
  }
145
  .white-key.active {
146
  background: linear-gradient(to bottom, #d5d5d5, #c2c2c2);
 
164
  left: 50%;
165
  transform: translateX(-50%);
166
  pointer-events: none;
 
167
  }
168
  .note-bar {
169
  position: absolute;
 
185
  height: 3.5px;
186
  background: linear-gradient(to right, transparent, var(--hit-line-color), transparent);
187
  border-radius: 1.75px;
188
+ z-index: 3; /* Can be same as active keys, or below particles if particles are z-index 4 */
189
  box-shadow: 0 0 16px var(--hit-line-color);
190
  }
191
  .particle-container {
192
  position: absolute;
193
  width: var(--piano-actual-width, 100%);
194
+ height: calc(28vh + 5px + 10px); /* Cover piano base and slightly above */
195
+ bottom: 0;
196
  left: 50%;
197
  transform: translateX(-50%);
198
  pointer-events: none;
199
+ z-index: 4; /* Particles above keys and hit-line */
 
200
  }
201
  .particle {
202
  position: absolute;
 
242
  const loadMidiButton = document.getElementById('loadMidiButton');
243
  const loadingIndicator = document.getElementById('loading-indicator');
244
  const hitLineElement = document.getElementById('hit-line');
245
+
246
  const synth = new Tone.PolySynth(Tone.Synth, {
247
  oscillator: { type: "triangle8" },
248
  envelope: { attack: 0.015, decay: 0.35, sustain: 0.15, release: 0.9 },
249
  volume: -10
250
  }).toDestination();
251
+
252
  const NOTES_ORDER = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
253
  const START_OCTAVE = 1;
254
  const END_OCTAVE = 7;
255
  const WHITE_KEY_WIDTH_PX = 30;
256
  const BLACK_KEY_WIDTH_PX = 18;
257
+ const WHITE_KEY_OVERLAP_PX = 1; // Corresponds to white-key margin-right: -1px
258
+
259
  const NOTE_FALL_DURATION_MS = 3800;
260
  const pianoKeys = {};
261
  let whiteKeyCount = 0;
262
+ let pianoActualWidth = 0; // Will be calculated based on white keys and their overlap
263
+
264
  const keyboardMapping = {
265
  'a': 'C4', 'w': 'C#4', 's': 'D4', 'e': 'D#4', 'd': 'E4', 'f': 'F4',
266
  't': 'F#4', 'g': 'G4', 'y': 'G#4', 'h': 'A4', 'u': 'A#4', 'j': 'B4',
267
  'k': 'C5', 'o': 'C#5', 'l': 'D5', 'p': 'D#5', ';': 'E5', "'": 'F5'
268
  };
269
  const activeKeyboardKeys = new Set();
270
+
271
  function createPianoKeys() {
272
  pianoElement.innerHTML = '';
273
  whiteKeyCount = 0;
274
  pianoActualWidth = 0;
275
+ let currentWhiteKeyVisualOffset = 0; // Tracks the visual offset for black key positioning
276
+
277
  for (let octave = START_OCTAVE; octave <= END_OCTAVE; octave++) {
278
  NOTES_ORDER.forEach((noteBase) => {
279
  if (octave === END_OCTAVE && noteBase !== 'C') return;
280
  if (octave === START_OCTAVE && !['A','A#','B'].includes(noteBase)) return;
281
+
282
  const keyElement = document.createElement('div');
283
  keyElement.classList.add('key');
284
  const noteName = noteBase + octave;
285
  keyElement.dataset.note = noteName;
286
  pianoKeys[noteName] = keyElement;
287
+
288
+ if (noteBase.includes('#')) { // Black key
289
  keyElement.classList.add('black-key');
290
+ // Position black key centered over the "crack" of the preceding white key
291
+ // currentWhiteKeyVisualOffset is the right edge of the *previous* white key
292
+ keyElement.style.left = (currentWhiteKeyVisualOffset - (BLACK_KEY_WIDTH_PX / 2) - (WHITE_KEY_OVERLAP_PX / 2)) + 'px';
293
  keyElement.style.top = '0px';
294
+ } else { // White key
295
  keyElement.classList.add('white-key');
296
+ // If not the first white key, account for overlap with previous key
297
+ if (whiteKeyCount > 0) {
298
+ currentWhiteKeyVisualOffset -= WHITE_KEY_OVERLAP_PX;
299
+ }
300
+ currentWhiteKeyVisualOffset += WHITE_KEY_WIDTH_PX;
301
  whiteKeyCount++;
302
  }
303
  pianoElement.appendChild(keyElement);
304
+
305
  const playNote = (e) => {
306
  if(e && e.preventDefault) e.preventDefault();
307
  if(keyElement.classList.contains('active')) return;
308
  keyElement.classList.add('active');
309
  const hue = Math.random() * 360;
310
+ const activeColor = `hsla(${hue}, 100%, 65%, 0.9)`;
311
+ keyElement.style.setProperty('--key-active-glow', activeColor);
312
  synth.triggerAttack(noteName, Tone.now());
313
+ createKeyParticles(keyElement, activeColor);
314
  };
315
  const releaseNote = (e) => {
316
  if(e && e.preventDefault) e.preventDefault();
 
324
  keyElement.addEventListener('touchend', releaseNote);
325
  });
326
  }
327
+
328
+ // Calculate the actual width of the piano considering overlaps
329
+ if (whiteKeyCount > 0) {
330
+ pianoActualWidth = (whiteKeyCount * WHITE_KEY_WIDTH_PX) - ( (whiteKeyCount - 1) * WHITE_KEY_OVERLAP_PX );
331
+ } else {
332
+ pianoActualWidth = 0;
333
+ }
334
+
335
  pianoElement.style.width = pianoActualWidth + 'px';
336
  document.documentElement.style.setProperty('--piano-actual-width', pianoActualWidth + 'px');
337
  }
338
  createPianoKeys();
339
+
340
  function createKeyParticles(keyElement, color) {
341
  const keyRect = keyElement.getBoundingClientRect();
342
  const particleContRect = particleContainer.getBoundingClientRect();
 
346
  particle.style.setProperty('--particle-color', color);
347
  particle.style.setProperty('--random-x', Math.random());
348
  particle.style.setProperty('--random-y', Math.random());
349
+ const xPos = (keyRect.left - particleContRect.left) + (keyRect.width / 2) - (8/2); // Center particle
350
  const yPos = (keyRect.top - particleContRect.top) - 15; // Particles start slightly above the key
351
  particle.style.left = `${xPos}px`;
352
  particle.style.top = `${yPos}px`;
 
354
  setTimeout(() => particle.remove(), 800);
355
  }
356
  }
357
+
358
  let currentMidiEvents = [];
359
  let currentMidiTempo = 120;
360
+
361
  async function loadAndPlayMidiFromUrl(url) {
362
  if (Tone.Transport.state === 'started') {
363
  Tone.Transport.stop();
364
  Tone.Transport.cancel();
365
  document.querySelectorAll('.note-bar').forEach(n => n.remove());
366
+ Object.values(pianoKeys).forEach(key => key.classList.remove('active')); // Reset active keys
367
  loadMidiButton.textContent = 'Load & Play MIDI';
368
  return;
369
  }
 
420
  const fallAreaRect = noteFallArea.getBoundingClientRect();
421
  const hitLineRect = hitLineElement.getBoundingClientRect();
422
  const hitLineTopRelativeToFallArea = hitLineRect.top - fallAreaRect.top;
423
+
424
  currentMidiEvents.forEach(noteData => {
425
  Tone.Transport.scheduleOnce(time => {
426
  const targetKeyElement = pianoKeys[noteData.note];
 
429
  noteElement.classList.add('note-bar');
430
 
431
  const keyRect = targetKeyElement.getBoundingClientRect();
 
432
  const keyIsBlack = noteData.note.includes('#');
433
+ // Adjust width for visual fit
434
+ const noteBarWidth = (keyIsBlack ? BLACK_KEY_WIDTH_PX : WHITE_KEY_WIDTH_PX) - (keyIsBlack ? 2 : 2);
435
+ noteElement.style.width = noteBarWidth + 'px';
436
+ // Center the note bar on the key
437
+ noteElement.style.left = (keyRect.left - fallAreaRect.left + (keyRect.width - noteBarWidth) / 2) + 'px';
438
 
439
  const noteVisualHeight = Math.max(15, (noteData.duration / (60 / currentMidiTempo)) * 70);
440
  noteElement.style.height = noteVisualHeight + 'px';
 
448
  noteElement.style.top = `-${noteVisualHeight}px`;
449
  noteFallArea.appendChild(noteElement);
450
 
451
+ const targetYForNoteTopToHitLine = hitLineTopRelativeToFallArea;
452
+ const targetYForNoteBottomToPassHitLine = hitLineTopRelativeToFallArea + noteVisualHeight;
453
 
454
  const timeToHitLine = NOTE_FALL_DURATION_MS;
455
  const timeAfterHitLine = noteData.duration * 1000;
456
  const totalAnimationDuration = timeToHitLine + timeAfterHitLine;
457
+
458
  noteElement.animate([
459
+ { transform: `translateY(0px)` },
460
+ { transform: `translateY(${targetYForNoteTopToHitLine}px)`, offset: timeToHitLine / totalAnimationDuration },
461
+ { transform: `translateY(${targetYForNoteBottomToPassHitLine + 20}px)` } // +20 to ensure it goes off screen
462
  ], {
463
  duration: totalAnimationDuration,
464
  easing: 'linear'
465
  }).onfinish = () => {
466
  if (noteElement.parentNode) noteElement.remove();
467
  };
468
+
469
  Tone.Transport.scheduleOnce(hitTime => {
470
  synth.triggerAttackRelease(noteData.note, noteData.duration, hitTime, noteData.velocity);
471
  if (targetKeyElement) {
472
+ // Use noteMainColor for the glow of MIDI-played keys
473
+ targetKeyElement.style.setProperty('--key-active-glow', noteMainColor);
474
  targetKeyElement.classList.add('active');
475
  createKeyParticles(targetKeyElement, noteMainColor);
476
  setTimeout(() => {
477
  targetKeyElement.classList.remove('active');
478
+ // Optionally reset glow to default if needed
479
+ // targetKeyElement.style.removeProperty('--key-active-glow');
480
  }, noteData.duration * 1000);
481
  }
482
  }, time + (NOTE_FALL_DURATION_MS / 1000));
483
  }, noteData.time);
484
  });
485
  }
486
+
487
  loadMidiButton.addEventListener('click', async () => {
488
  await Tone.start();
489
  const url = midiUrlInput.value.trim();
 
507
  });
508
  window.addEventListener('resize', () => {
509
  createPianoKeys();
510
+ // If MIDI is playing, you might want to reschedule visuals or handle this more gracefully.
511
+ // For now, stopping and clearing visuals might be simplest if a resize happens during playback.
512
+ if (Tone.Transport.state === 'started') {
513
+ Tone.Transport.stop();
514
+ Tone.Transport.cancel();
515
+ document.querySelectorAll('.note-bar').forEach(n => n.remove());
516
+ Object.values(pianoKeys).forEach(key => key.classList.remove('active'));
517
+ loadMidiButton.textContent = 'Load & Play MIDI';
518
+ // Optionally, you could try to re-calculate and re-schedule notes here,
519
+ // but it's complex to get right without visual jumps.
520
+ }
521
  });
522
  </script>
523
  </body>