Spaces:
Running
Running
Update index.html
Browse files- 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:
|
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:
|
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
|
196 |
-
bottom: 0;
|
197 |
left: 50%;
|
198 |
transform: translateX(-50%);
|
199 |
pointer-events: none;
|
200 |
-
z-index:
|
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 |
-
|
|
|
281 |
keyElement.classList.add('black-key');
|
282 |
-
|
|
|
|
|
283 |
keyElement.style.top = '0px';
|
284 |
-
} else {
|
285 |
keyElement.classList.add('white-key');
|
286 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
296 |
synth.triggerAttack(noteName, Tone.now());
|
297 |
-
createKeyParticles(keyElement,
|
298 |
};
|
299 |
const releaseNote = (e) => {
|
300 |
if(e && e.preventDefault) e.preventDefault();
|
@@ -308,11 +324,19 @@
|
|
308 |
keyElement.addEventListener('touchend', releaseNote);
|
309 |
});
|
310 |
}
|
311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
407 |
-
|
|
|
|
|
|
|
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;
|
422 |
-
const targetYForNoteBottomToPassHitLine = hitLineTopRelativeToFallArea + noteVisualHeight;
|
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)` },
|
429 |
-
{ transform: `translateY(${targetYForNoteTopToHitLine}px)`, offset: timeToHitLine / totalAnimationDuration },
|
430 |
-
{ transform: `translateY(${targetYForNoteBottomToPassHitLine}px)` } //
|
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 |
-
|
|
|
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>
|