kimhyunwoo commited on
Commit
38df87f
ยท
verified ยท
1 Parent(s): 5959290

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +352 -248
index.html CHANGED
@@ -1,46 +1,92 @@
1
  <!DOCTYPE html>
2
- <html lang="ko">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
- <title>Interactive Piano with Falling Notes</title>
7
  <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
 
8
  <style>
9
  body {
10
  margin: 0;
11
  height: 100vh;
12
- background-color: #0a0a0a; /* ์–ด๋‘์šด ๋ฐฐ๊ฒฝ */
13
  display: flex;
14
  flex-direction: column;
15
  justify-content: flex-end;
16
  align-items: center;
17
  overflow: hidden;
18
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
 
21
  .piano-container {
22
- width: 95vw;
23
- max-width: 1400px; /* ๊ฑด๋ฐ˜์ด ๋„ˆ๋ฌด ์ž‘์•„์ง€์ง€ ์•Š๋„๋ก ์ตœ๋Œ€ ๋„ˆ๋น„ ์ œํ•œ */
24
- height: 30vh; /* ๊ฑด๋ฐ˜ ๋†’์ด ๋น„์œจ */
25
- min-height: 180px;
26
  display: flex;
27
  justify-content: center;
28
  align-items: flex-end;
29
- position: relative; /* ๋…ธํŠธ ๋–จ์–ด์ง€๋Š” ์˜์—ญ์˜ ๊ธฐ์ค€ */
30
- padding-bottom: 10px;
31
  }
32
 
33
  .piano {
34
  display: flex;
35
  position: relative;
36
- background: linear-gradient(to bottom, #282828, #181818);
37
- padding: 15px 15px 0 15px;
38
- border-radius: 12px 12px 0 0;
39
- box-shadow: 0 0 40px rgba(173, 216, 230, 0.3), /* ์—ฐํ•œ ํ•˜๋Š˜์ƒ‰ ๋น› */
40
- 0 0 60px rgba(255, 182, 193, 0.2); /* ์—ฐํ•œ ํ•‘ํฌ์ƒ‰ ๋น› */
41
- border: 3px solid #3a3a3a;
42
  border-bottom: none;
43
- height: 100%; /* piano-container ๋†’์ด์— ๋งž์ถค */
44
  }
45
 
46
  .key {
@@ -48,362 +94,420 @@
48
  cursor: pointer;
49
  user-select: none;
50
  -webkit-tap-highlight-color: transparent;
51
- transition: all 0.05s ease-out;
52
- position: relative; /* ๋ผ๋ฒจ ์œ„์น˜ ๊ธฐ์ค€ */
53
  }
54
 
55
  .white-key {
56
- width: 40px; /* ๋” ๋งŽ์€ ๊ฑด๋ฐ˜์„ ์œ„ํ•ด ๋„ˆ๋น„ ์•ฝ๊ฐ„ ์ค„์ž„ */
57
  height: 100%;
58
- background: linear-gradient(to bottom, #fdfdfd, #e8e8e8);
59
- border: 1px solid #999;
60
- border-bottom: 6px solid #b0b0b0;
61
- border-radius: 0 0 6px 6px;
62
- margin-right: 1px;
63
- box-shadow: 0 3px 5px rgba(0,0,0,0.25), inset 0 -3px 3px rgba(255,255,255,0.6);
64
  z-index: 1;
 
65
  }
66
- .white-key:last-child { margin-right: 0; }
 
 
67
 
68
  .black-key {
69
- width: 24px;
70
- height: 60%; /* ํฐ ๊ฑด๋ฐ˜ ๋†’์ด์˜ 60% */
71
- background: linear-gradient(to bottom, #444, #222);
72
- border: 1px solid #111;
73
- border-bottom: 5px solid #333;
74
- border-radius: 0 0 4px 4px;
75
  position: absolute;
76
  z-index: 2;
77
- box-shadow: 0 2px 4px rgba(0,0,0,0.5), inset 0 -2px 2px rgba(120,120,120,0.4);
78
  }
79
 
80
  .white-key.active {
81
- background: linear-gradient(to bottom, #e0e0e0, #cccccc);
82
- transform: translateY(3px);
83
  border-bottom-width: 3px;
84
- box-shadow: 0 0 25px 7px var(--key-glow-color, rgba(135, 206, 250, 0.9)), /* ํ•˜๋Š˜์ƒ‰ ๋น› */
85
- inset 0 -1px 1px rgba(255,255,255,0.4);
86
  }
87
 
88
  .black-key.active {
89
- background: linear-gradient(to bottom, #333, #111);
90
- transform: translateY(2px);
91
  border-bottom-width: 3px;
92
- box-shadow: 0 0 25px 7px var(--key-glow-color, rgba(135, 206, 250, 0.9)), /* ํ•˜๋Š˜์ƒ‰ ๋น› */
93
- inset 0 -1px 1px rgba(100,100,100,0.2);
94
  }
95
 
96
- .key-label {
97
  position: absolute;
98
- bottom: 8px;
99
  left: 50%;
100
  transform: translateX(-50%);
101
- font-size: 10px;
102
- color: #666;
103
- pointer-events: none; /* ๋ผ๋ฒจ์ด ํ„ฐ์น˜ ์ด๋ฒคํŠธ ๋ฐฉํ•ดํ•˜์ง€ ์•Š๋„๋ก */
104
  }
105
- .black-key .key-label { color: #bbb; font-size: 9px; }
106
 
107
  #note-fall-area {
108
  width: 100%;
109
- height: calc(100vh - 30vh - 30px); /* ํ”ผ์•„๋…ธ ๋†’์ด, ๋ฐ”๋‹ฅ ์—ฌ๋ฐฑ ์ œ์™ธ */
110
  position: absolute;
111
  top: 0;
112
  left: 50%;
113
- transform: translateX(-50%); /* ์ค‘์•™ ์ •๋ ฌ */
114
- /* background-color: rgba(20,20,20,0.5); */ /* ๊ฐœ๋ฐœ ์‹œ ์˜์—ญ ํ™•์ธ์šฉ */
115
- pointer-events: none; /* ๋…ธํŠธ๊ฐ€ ๋งˆ์šฐ์Šค/ํ„ฐ์น˜ ์ด๋ฒคํŠธ ๊ฐ€๋กœ์ฑ„์ง€ ์•Š๋„๋ก */
116
  }
117
 
118
  .note-bar {
119
  position: absolute;
120
  box-sizing: border-box;
121
- border-radius: 3px;
122
- opacity: 0.9;
123
- background: linear-gradient(to bottom, var(--note-color-start, #00ff00), var(--note-color-end, #008800));
124
- box-shadow: 0 0 10px var(--note-color-start, #00ff00);
125
- border: 1px solid rgba(255, 255, 255, 0.3);
126
  }
127
 
128
  #hit-line {
129
  position: absolute;
130
- bottom: calc(30vh + 10px); /* ํ”ผ์•„๋…ธ ์ปจํ…Œ์ด๋„ˆ ํ•˜๋‹จ์— ๋งž์ถค */
131
  left: 50%;
132
  transform: translateX(-50%);
133
- width: 95vw; /* ํ”ผ์•„๋…ธ ์ปจํ…Œ์ด๋„ˆ ๋„ˆ๋น„์™€ ์œ ์‚ฌํ•˜๊ฒŒ */
134
- max-width: 1400px;
135
- height: 4px;
136
- background: linear-gradient(to right, transparent, rgba(255,255,255,0.7), transparent);
137
- border-radius: 2px;
138
  z-index: 3;
139
- box-shadow: 0 0 15px rgba(200, 200, 255, 0.6);
140
  }
141
 
 
 
 
 
 
 
 
 
 
142
  .particle {
143
  position: absolute;
144
- width: 5px;
145
- height: 5px;
146
  background-color: var(--particle-color, white);
147
  border-radius: 50%;
148
  opacity: 1;
149
- pointer-events: none;
150
- animation: fadeOutAndRise 0.5s ease-out forwards;
151
- }
152
-
153
- @keyframes fadeOutAndRise {
154
- to {
155
- transform: translateY(-30px) scale(0.5);
156
- opacity: 0;
157
- }
158
- }
159
-
160
- #controls {
161
- position: absolute;
162
- top: 20px;
163
- left: 20px;
164
- z-index: 10;
165
- }
166
- #controls button {
167
- padding: 8px 12px;
168
- font-size: 14px;
169
- cursor: pointer;
170
- background-color: #333;
171
- color: white;
172
- border: 1px solid #555;
173
- border-radius: 4px;
174
  }
175
- #controls button:hover {
176
- background-color: #555;
 
177
  }
178
-
179
  </style>
180
  </head>
181
  <body>
182
- <div id="note-fall-area"></div>
 
 
 
 
 
 
 
 
183
  <div id="hit-line"></div>
184
 
185
  <div class="piano-container">
186
  <div class="piano" id="piano">
187
- <!-- ๊ฑด๋ฐ˜์€ JavaScript๋กœ ์ƒ์„ฑ -->
188
  </div>
189
  </div>
190
-
191
- <div id="controls">
192
- <button id="playButton">Play Sample</button>
193
- </div>
194
 
195
  <script>
196
  const pianoElement = document.getElementById('piano');
197
  const noteFallArea = document.getElementById('note-fall-area');
198
- const playButton = document.getElementById('playButton');
 
 
 
199
 
200
- // ํ”ผ์•„๋…ธ Synth ์ดˆ๊ธฐํ™” (Tone.js)
201
  const synth = new Tone.PolySynth(Tone.Synth, {
202
- oscillator: { type: "sine" }, // ๊ธฐ๋ณธ ์‚ฌ์ธํŒŒ, ์ข€ ๋” ํ”ผ์•„๋…ธ์ฒ˜๋Ÿผ ํ•˜๋ ค๋ฉด 'triangle8' ๋“ฑ๋„ ๊ณ ๋ ค
203
  envelope: {
204
  attack: 0.005,
205
- decay: 0.1,
206
- sustain: 0.3,
207
- release: 1
208
  },
209
- volume: -6 // ๋ณผ๋ฅจ ์กฐ์ ˆ
210
  }).toDestination();
211
 
212
- const whiteKeyNotes = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
213
- const blackKeyNotes = ['C#', 'D#', 'F#', 'G#', 'A#'];
214
- const notesWithSharps = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
215
-
216
- const baseOctave = 2; // ์‹œ์ž‘ ์˜ฅํƒ€๋ธŒ
217
- const numOctaves = 4; // ํ‘œ์‹œํ•  ์˜ฅํƒ€๋ธŒ ์ˆ˜
218
- let totalWhiteKeys = 0;
 
 
 
 
219
 
220
- // ๊ฑด๋ฐ˜ ์ƒ์„ฑ
221
- for (let octave = baseOctave; octave < baseOctave + numOctaves; octave++) {
222
- notesWithSharps.forEach((noteName, index) => {
223
  const keyElement = document.createElement('div');
224
  keyElement.classList.add('key');
225
- const fullNoteName = noteName + octave;
226
- keyElement.dataset.note = fullNoteName;
 
227
 
228
- // const labelElement = document.createElement('span');
229
- // labelElement.classList.add('key-label');
230
- // labelElement.textContent = noteName.length === 1 ? noteName : noteName.charAt(0); // C# -> C
231
- // keyElement.appendChild(labelElement);
 
232
 
233
- if (noteName.includes('#')) { // ๊ฒ€์€ ๊ฑด๋ฐ˜
234
  keyElement.classList.add('black-key');
235
- // ๊ฒ€์€ ๊ฑด๋ฐ˜ ์œ„์น˜ ๊ณ„์‚ฐ: (์ด์ „ ํฐ ๊ฑด๋ฐ˜ ๊ฐœ์ˆ˜ * ํฐ ๊ฑด๋ฐ˜ ๋„ˆ๋น„) + ์˜คํ”„์…‹
236
- // C#์€ C ๋’ค, D#์€ D ๋’ค...
237
- let offsetMultiplier = totalWhiteKeys - 0.35; // ๊ฒ€์€ ๊ฑด๋ฐ˜์„ ํฐ ๊ฑด๋ฐ˜ ์‚ฌ์ด์— ์œ„์น˜์‹œํ‚ค๊ธฐ ์œ„ํ•œ ์กฐ์ •๊ฐ’
238
- keyElement.style.left = (offsetMultiplier * 41) + 'px'; // 40(๋„ˆ๋น„) + 1(๋งˆ์ง„)
239
  keyElement.style.top = '0px';
240
- } else { // ํฐ ๊ฑด๋ฐ˜
241
  keyElement.classList.add('white-key');
242
- totalWhiteKeys++;
243
  }
244
  pianoElement.appendChild(keyElement);
245
 
246
- // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
247
- const playNoteHandler = (event) => {
248
- if (event.type === 'touchstart') event.preventDefault();
249
  keyElement.classList.add('active');
250
  const randomHue = Math.random() * 360;
251
- keyElement.style.setProperty('--key-glow-color', `hsla(${randomHue}, 100%, 70%, 0.9)`);
252
- synth.triggerAttackRelease(fullNoteName, "8n"); // "8n"์€ 8๋ถ„์Œํ‘œ ๊ธธ์ด
253
- createParticles(keyElement, `hsla(${randomHue}, 100%, 70%, 0.9)`);
254
  };
255
- const stopNoteHandler = (event) => {
256
- if (event.type === 'touchend') event.preventDefault();
257
  keyElement.classList.remove('active');
 
258
  };
259
 
260
- keyElement.addEventListener('mousedown', playNoteHandler);
261
- keyElement.addEventListener('mouseup', stopNoteHandler);
262
- keyElement.addEventListener('mouseleave', stopNoteHandler);
263
- keyElement.addEventListener('touchstart', playNoteHandler, { passive: false });
264
- keyElement.addEventListener('touchend', stopNoteHandler);
265
- keyElement.addEventListener('touchcancel', stopNoteHandler);
 
 
266
  });
267
  }
268
- // ๋งˆ์ง€๋ง‰ C ๊ฑด๋ฐ˜ ์ถ”๊ฐ€
269
- const lastCOctave = baseOctave + numOctaves;
270
- const lastCKey = document.createElement('div');
271
- lastCKey.classList.add('key', 'white-key');
272
- lastCKey.dataset.note = 'C' + lastCOctave;
273
- // const lastCLabel = document.createElement('span');
274
- // lastCLabel.classList.add('key-label');
275
- // lastCLabel.textContent = 'C';
276
- // lastCKey.appendChild(lastCLabel);
277
- pianoElement.appendChild(lastCKey);
278
- const lastCNoteName = 'C' + lastCOctave;
279
- lastCKey.addEventListener('mousedown', (e) => { e.preventDefault(); lastCKey.classList.add('active'); synth.triggerAttackRelease(lastCNoteName, "8n"); createParticles(lastCKey); });
280
- lastCKey.addEventListener('mouseup', (e) => { e.preventDefault(); lastCKey.classList.remove('active'); });
281
- lastCKey.addEventListener('mouseleave', () => lastCKey.classList.remove('active'));
282
- lastCKey.addEventListener('touchstart', (e) => { e.preventDefault(); lastCKey.classList.add('active'); synth.triggerAttackRelease(lastCNoteName, "8n"); createParticles(lastCKey);}, { passive: false });
283
- lastCKey.addEventListener('touchend', (e) => { e.preventDefault(); lastCKey.classList.remove('active'); });
284
- lastCKey.addEventListener('touchcancel', () => lastCKey.classList.remove('active'));
285
-
286
-
287
- // ํŒŒํ‹ฐํด ์ƒ์„ฑ ํ•จ์ˆ˜
288
- function createParticles(keyElement, color = 'white') {
289
- const rect = keyElement.getBoundingClientRect();
290
- const pianoRect = pianoElement.getBoundingClientRect();
291
- for (let i = 0; i < 10; i++) { // 10๊ฐœ์˜ ํŒŒํ‹ฐํด ์ƒ์„ฑ
292
  const particle = document.createElement('div');
293
  particle.classList.add('particle');
294
  particle.style.setProperty('--particle-color', color);
295
 
296
- // ํ”ผ์•„๋…ธ ์ปจํ…Œ์ด๋„ˆ ๋‚ด์˜ ์ƒ๋Œ€์  ์œ„์น˜๋กœ ์„ค์ •
297
- particle.style.left = (rect.left - pianoRect.left + rect.width / 2 + (Math.random() - 0.5) * 20) + 'px';
298
- particle.style.bottom = (pianoRect.height - (rect.top - pianoRect.top) - rect.height / 2) + 'px'; // Y์ถ• ๋ฐ˜์ „ํ•˜์—ฌ bottom ๊ธฐ์ค€์œผ๋กœ ์„ค์ •
299
-
300
- noteFallArea.appendChild(particle);
301
- setTimeout(() => {
302
- particle.remove();
303
- }, 500); // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ๊ฐ„๊ณผ ๋™์ผํ•˜๊ฒŒ ์„ค์ •
 
304
  }
305
  }
306
 
 
 
307
 
308
- // --- ๋…ธํŠธ ๋–จ์–ด์ง€๋Š” ํšจ๊ณผ ---
309
- const hitLineY = noteFallArea.offsetHeight; // ํŒ์ •์„  ์œ„์น˜ (๋…ธํŠธ ๋–จ์–ด์ง€๋Š” ์˜์—ญ์˜ ๋ฐ”๋‹ฅ)
310
- const fallDurationSeconds = 2; // ๋…ธํŠธ๊ฐ€ ๋–จ์–ด์ง€๋Š” ๋ฐ ๊ฑธ๋ฆฌ๋Š” ์‹œ๊ฐ„ (์ดˆ)
311
-
312
- // ์ƒ˜ํ”Œ ์•…๋ณด ๋ฐ์ดํ„ฐ: { time: ์‹œ์ž‘์‹œ๊ฐ„(์ดˆ), note: ์Œ์ด๋ฆ„, duration: ๊ธธ์ด(์ดˆ), color: [r,g,b] }
313
- const sampleSong = [
314
- { time: 0, note: 'C4', duration: 0.4, color: [0, 255, 0] },
315
- { time: 0.5, note: 'C4', duration: 0.4, color: [0, 255, 0] },
316
- { time: 1, note: 'G4', duration: 0.4, color: [255, 255, 0] },
317
- { time: 1.5, note: 'G4', duration: 0.4, color: [255, 255, 0] },
318
- { time: 2, note: 'A4', duration: 0.4, color: [255, 0, 255] },
319
- { time: 2.5, note: 'A4', duration: 0.4, color: [255, 0, 255] },
320
- { time: 3, note: 'G4', duration: 0.8, color: [255, 255, 0] },
321
- { time: 4, note: 'F4', duration: 0.4, color: [0, 255, 255] },
322
- { time: 4.5, note: 'F4', duration: 0.4, color: [0, 255, 255] },
323
- { time: 5, note: 'E4', duration: 0.4, color: [255, 165, 0] },
324
- { time: 5.5, note: 'E4', duration: 0.4, color: [255, 165, 0] },
325
- { time: 6, note: 'D4', duration: 0.4, color: [0, 0, 255] },
326
- { time: 6.5, note: 'D4', duration: 0.4, color: [0, 0, 255] },
327
- { time: 7, note: 'C4', duration: 0.8, color: [0, 255, 0] },
328
- ];
329
-
330
- function playSampleSong() {
331
- if (Tone.Transport.state === 'started') {
332
- Tone.Transport.stop();
333
- // ์ด๋ฏธ ์ƒ์„ฑ๋œ ๋…ธํŠธ๋“ค ์ œ๊ฑฐ
334
- document.querySelectorAll('.note-bar').forEach(n => n.remove());
335
- playButton.textContent = 'Play Sample';
336
  return;
337
  }
338
-
339
- playButton.textContent = 'Stop';
340
- Tone.Transport.bpm.value = 100; // ๊ณก์˜ BPM ์„ค์ •
341
 
342
- sampleSong.forEach(noteData => {
343
- Tone.Transport.scheduleOnce(time => {
344
- // ๋…ธํŠธ ๋ฐ” ์ƒ์„ฑ
345
- const noteElement = document.createElement('div');
346
- noteElement.classList.add('note-bar');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
- const targetKeyElement = pianoElement.querySelector(`.key[data-note="${noteData.note}"]`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  if (targetKeyElement) {
 
 
 
350
  const keyRect = targetKeyElement.getBoundingClientRect();
351
- const pianoRect = pianoElement.getBoundingClientRect();
352
-
353
- noteElement.style.width = keyRect.width - 2 + 'px'; // -2 for border
354
  noteElement.style.left = (keyRect.left - pianoRect.left + 1) + 'px';
355
- noteElement.style.height = (noteData.duration * 100) + 'px'; // ๊ธธ์ด์— ๋”ฐ๋ผ ๋†’์ด ์กฐ์ ˆ (์ž„์˜์˜ ๋น„์œจ)
356
 
357
- // ์ƒ‰์ƒ ์„ค์ •
358
- const [r, g, b] = noteData.color;
359
- noteElement.style.setProperty('--note-color-start', `rgba(${r}, ${g}, ${b}, 0.9)`);
360
- noteElement.style.setProperty('--note-color-end', `rgba(${r*0.7}, ${g*0.7}, ${b*0.7}, 0.9)`);
 
361
 
 
 
 
 
 
362
 
363
- noteElement.style.top = `-${noteElement.style.height}`; // ํ™”๋ฉด ์œ„์—์„œ ์‹œ์ž‘
364
- noteFallArea.appendChild(noteElement);
365
 
366
- // ์• ๋‹ˆ๋ฉ”์ด์…˜
 
 
 
367
  noteElement.animate([
368
- { transform: `translateY(0px)` }, // ์‹œ์ž‘ ์œ„์น˜ (CSS์—์„œ top: -height๋กœ ์„ค์ •ํ–ˆ์œผ๋ฏ€๋กœ Y๋Š” 0๋ถ€ํ„ฐ ์‹œ์ž‘)
369
- { transform: `translateY(${hitLineY + parseFloat(noteElement.style.height)}px)` } // ํŒ์ •์„  ์•„๋ž˜๋กœ ์™„์ „ํžˆ ์ง€๋‚˜๊ฐ€๋„๋ก
370
  ], {
371
- duration: fallDurationSeconds * 1000,
372
  easing: 'linear'
373
  });
374
-
375
- // ํŒ์ • ์‹œ์ ์— ์†Œ๋ฆฌ ์žฌ์ƒ ๋ฐ ํšจ๊ณผ
376
  Tone.Transport.scheduleOnce(hitTime => {
377
- synth.triggerAttackRelease(noteData.note, noteData.duration, hitTime);
378
  targetKeyElement.classList.add('active');
379
- targetKeyElement.style.setProperty('--key-glow-color', `rgba(${r}, ${g}, ${b}, 0.9)`);
380
- createParticles(targetKeyElement, `rgba(${r}, ${g}, ${b}, 0.9)`);
381
-
382
- // ๋…ธํŠธ ์ œ๊ฑฐ ๋ฐ ๊ฑด๋ฐ˜ ๋น„ํ™œ์„ฑํ™” ํƒ€์ด๋จธ
383
  setTimeout(() => {
384
  targetKeyElement.classList.remove('active');
385
- }, noteData.duration * 1000 * (60 / Tone.Transport.bpm.value)); // BPM ๊ณ ๋ ค
386
 
387
- // ๋…ธํŠธ ๋ฐ” ์ œ๊ฑฐ (์• ๋‹ˆ๋ฉ”์ด์…˜ ๋๋‚œ ํ›„)
388
  setTimeout(() => {
389
- if (noteElement.parentNode) {
390
- noteElement.remove();
391
- }
392
- }, fallDurationSeconds * 1000);
393
-
394
- }, time + fallDurationSeconds); // ๋…ธํŠธ๊ฐ€ ๋–จ์–ด์ง€๋Š” ์‹œ๊ฐ„๋งŒํผ ๋’ค์— ์†Œ๋ฆฌ ์žฌ์ƒ
395
  }
396
- }, noteData.time);
397
  });
398
-
399
- Tone.Transport.start();
400
  }
401
 
402
- playButton.addEventListener('click', () => {
403
- Tone.start(); // ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ํ›„ ์˜ค๋””์˜ค ์ปจํ…์ŠคํŠธ ์‹œ์ž‘
404
- playSampleSong();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  });
406
 
 
407
  </script>
408
  </body>
409
  </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
+ <title>Advanced 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>
10
  body {
11
  margin: 0;
12
  height: 100vh;
13
+ background-color: #080808;
14
  display: flex;
15
  flex-direction: column;
16
  justify-content: flex-end;
17
  align-items: center;
18
  overflow: hidden;
19
+ font-family: Arial, sans-serif;
20
+ }
21
+
22
+ #controls {
23
+ position: absolute;
24
+ top: 10px;
25
+ left: 10px;
26
+ z-index: 100;
27
+ background-color: rgba(30, 30, 30, 0.8);
28
+ padding: 10px;
29
+ border-radius: 5px;
30
+ box-shadow: 0 2px 5px rgba(0,0,0,0.3);
31
+ }
32
+
33
+ #controls input[type="text"] {
34
+ padding: 8px;
35
+ margin-right: 5px;
36
+ border: 1px solid #555;
37
+ background-color: #222;
38
+ color: #eee;
39
+ border-radius: 3px;
40
+ width: 250px;
41
+ }
42
+
43
+ #controls button {
44
+ padding: 8px 15px;
45
+ font-size: 14px;
46
+ cursor: pointer;
47
+ background-color: #4CAF50;
48
+ color: white;
49
+ border: none;
50
+ border-radius: 3px;
51
+ transition: background-color 0.2s;
52
+ }
53
+ #controls button:hover {
54
+ background-color: #45a049;
55
+ }
56
+ #controls button:disabled {
57
+ background-color: #555;
58
+ cursor: not-allowed;
59
+ }
60
+ #loading-indicator {
61
+ color: #aaa;
62
+ font-size: 12px;
63
+ margin-left: 10px;
64
+ display: none;
65
  }
66
 
67
  .piano-container {
68
+ width: 98vw;
69
+ max-width: 1600px; /* Increased max-width for more keys */
70
+ height: 28vh;
71
+ min-height: 190px;
72
  display: flex;
73
  justify-content: center;
74
  align-items: flex-end;
75
+ position: relative;
76
+ padding-bottom: 5px; /* Reduced padding */
77
  }
78
 
79
  .piano {
80
  display: flex;
81
  position: relative;
82
+ background: linear-gradient(to bottom, #202020, #101010);
83
+ padding: 10px 10px 0 10px;
84
+ border-radius: 10px 10px 0 0;
85
+ box-shadow: 0 0 35px rgba(150, 150, 255, 0.25),
86
+ 0 0 55px rgba(255, 150, 200, 0.15);
87
+ border: 2px solid #303030;
88
  border-bottom: none;
89
+ height: 100%;
90
  }
91
 
92
  .key {
 
94
  cursor: pointer;
95
  user-select: none;
96
  -webkit-tap-highlight-color: transparent;
97
+ transition: all 0.03s ease-out; /* Faster transition */
98
+ position: relative;
99
  }
100
 
101
  .white-key {
102
+ width: 32px; /* Adjusted for more keys */
103
  height: 100%;
104
+ background: linear-gradient(to bottom, #ffffff, #e0e0e0);
105
+ border-left: 1px solid #b0b0b0;
106
+ border-right: 1px solid #b0b0b0;
107
+ border-bottom: 5px solid #a0a0a0;
108
+ border-radius: 0 0 5px 5px;
109
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2), inset 0 -2px 2px rgba(255,255,255,0.7);
110
  z-index: 1;
111
+ margin-right: -1px; /* Overlap borders */
112
  }
113
+ .white-key:first-child { border-left: 1px solid #888; }
114
+ .white-key:last-child { border-right: 1px solid #888; margin-right: 0;}
115
+
116
 
117
  .black-key {
118
+ width: 20px; /* Adjusted for more keys */
119
+ height: 60%;
120
+ background: linear-gradient(to bottom, #3a3a3a, #1a1a1a);
121
+ border: 1px solid #050505;
122
+ border-bottom: 4px solid #202020;
123
+ border-radius: 0 0 3px 3px;
124
  position: absolute;
125
  z-index: 2;
126
+ box-shadow: -1px 0 3px rgba(0,0,0,0.3), 1px 0 3px rgba(0,0,0,0.3), 0 2px 3px rgba(0,0,0,0.4), inset 0 -2px 2px rgba(80,80,80,0.3);
127
  }
128
 
129
  .white-key.active {
130
+ background: linear-gradient(to bottom, #d8d8d8, #c8c8c8);
131
+ transform: translateY(2px);
132
  border-bottom-width: 3px;
133
+ box-shadow: 0 0 15px 4px var(--key-glow-color, rgba(255, 0, 255, 0.7)), /* Magenta glow */
134
+ inset 0 -1px 1px rgba(255,255,255,0.3);
135
  }
136
 
137
  .black-key.active {
138
+ background: linear-gradient(to bottom, #2a2a2a, #0a0a0a);
139
+ transform: translateY(1px);
140
  border-bottom-width: 3px;
141
+ box-shadow: 0 0 15px 4px var(--key-glow-color, rgba(255, 0, 255, 0.7)), /* Magenta glow */
142
+ inset 0 -1px 1px rgba(80,80,80,0.1);
143
  }
144
 
145
+ .key-label { /* Optional: for displaying note names */
146
  position: absolute;
147
+ bottom: 5px;
148
  left: 50%;
149
  transform: translateX(-50%);
150
+ font-size: 9px;
151
+ color: #555;
152
+ pointer-events: none;
153
  }
154
+ .black-key .key-label { color: #ccc; font-size: 8px; }
155
 
156
  #note-fall-area {
157
  width: 100%;
158
+ height: calc(100vh - 28vh - 25px); /* Adjusted height */
159
  position: absolute;
160
  top: 0;
161
  left: 50%;
162
+ transform: translateX(-50%);
163
+ pointer-events: none;
 
164
  }
165
 
166
  .note-bar {
167
  position: absolute;
168
  box-sizing: border-box;
169
+ border-radius: 2px;
170
+ opacity: 0.85;
171
+ background: var(--note-gradient, linear-gradient(to bottom, #00ddff, #0088aa)); /* Default blueish */
172
+ box-shadow: 0 0 8px var(--note-glow, #00ddff);
173
+ border: 1px solid rgba(255, 255, 255, 0.2);
174
  }
175
 
176
  #hit-line {
177
  position: absolute;
178
+ bottom: calc(28vh + 5px); /* Align with piano container bottom */
179
  left: 50%;
180
  transform: translateX(-50%);
181
+ width: 98vw;
182
+ max-width: 1600px;
183
+ height: 3px;
184
+ background: linear-gradient(to right, transparent, rgba(255, 0, 255, 0.6), transparent); /* Magenta hit line */
185
+ border-radius: 1.5px;
186
  z-index: 3;
187
+ box-shadow: 0 0 12px rgba(255, 0, 255, 0.5);
188
  }
189
 
190
+ .particle-container {
191
+ position: absolute;
192
+ width: 100%;
193
+ height: 100%;
194
+ top: 0;
195
+ left: 0;
196
+ pointer-events: none;
197
+ z-index: 5; /* Ensure particles are above notes */
198
+ }
199
  .particle {
200
  position: absolute;
201
+ width: 4px;
202
+ height: 4px;
203
  background-color: var(--particle-color, white);
204
  border-radius: 50%;
205
  opacity: 1;
206
+ animation: particleAnim 0.6s ease-out forwards;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  }
208
+ @keyframes particleAnim {
209
+ 0% { transform: translateY(0) scale(1); opacity: 1; }
210
+ 100% { transform: translateY(-40px) scale(0.3); opacity: 0; }
211
  }
 
212
  </style>
213
  </head>
214
  <body>
215
+ <div id="controls">
216
+ <input type="text" id="midiUrlInput" placeholder="Enter MIDI URL (e.g., https://...)">
217
+ <button id="loadMidiButton">Load & Play MIDI</button>
218
+ <span id="loading-indicator">Loading...</span>
219
+ </div>
220
+
221
+ <div id="note-fall-area">
222
+ <div class="particle-container" id="particle-container"></div>
223
+ </div>
224
  <div id="hit-line"></div>
225
 
226
  <div class="piano-container">
227
  <div class="piano" id="piano">
228
+ <!-- Keys will be generated by JavaScript -->
229
  </div>
230
  </div>
 
 
 
 
231
 
232
  <script>
233
  const pianoElement = document.getElementById('piano');
234
  const noteFallArea = document.getElementById('note-fall-area');
235
+ const particleContainer = document.getElementById('particle-container');
236
+ const midiUrlInput = document.getElementById('midiUrlInput');
237
+ const loadMidiButton = document.getElementById('loadMidiButton');
238
+ const loadingIndicator = document.getElementById('loading-indicator');
239
 
 
240
  const synth = new Tone.PolySynth(Tone.Synth, {
241
+ oscillator: { type: "triangle8" }, // A bit softer than sine
242
  envelope: {
243
  attack: 0.005,
244
+ decay: 0.2,
245
+ sustain: 0.2,
246
+ release: 0.8
247
  },
248
+ volume: -8
249
  }).toDestination();
250
 
251
+ const notesOrder = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
252
+ const startOctave = 2;
253
+ const endOctave = 6; // Covers a good range
254
+ const whiteKeyWidth = 32; // px
255
+ const blackKeyWidth = 20; // px
256
+ const pianoKeys = {}; // To store key elements for easy access
257
+
258
+ let whiteKeyCounter = 0;
259
+ for (let octave = startOctave; octave <= endOctave; octave++) {
260
+ notesOrder.forEach((noteBase, i) => {
261
+ if (octave === endOctave && noteBase !== 'C') return; // End at C of the last octave
262
 
 
 
 
263
  const keyElement = document.createElement('div');
264
  keyElement.classList.add('key');
265
+ const noteName = noteBase + octave;
266
+ keyElement.dataset.note = noteName;
267
+ pianoKeys[noteName] = keyElement;
268
 
269
+ // Add labels if you want them
270
+ // const label = document.createElement('span');
271
+ // label.classList.add('key-label');
272
+ // label.textContent = noteBase.length === 1 ? noteBase : noteBase[0];
273
+ // keyElement.appendChild(label);
274
 
275
+ if (noteBase.includes('#')) {
276
  keyElement.classList.add('black-key');
277
+ // Position black keys relative to the preceding white key
278
+ keyElement.style.left = (whiteKeyCounter * whiteKeyWidth) - (blackKeyWidth / 2) - (whiteKeyCounter > 0 ? (whiteKeyCounter-1)*0 : 0) + 'px';
 
 
279
  keyElement.style.top = '0px';
280
+ } else {
281
  keyElement.classList.add('white-key');
282
+ whiteKeyCounter++;
283
  }
284
  pianoElement.appendChild(keyElement);
285
 
286
+ const playNote = (event) => {
287
+ if (event && event.preventDefault) event.preventDefault();
 
288
  keyElement.classList.add('active');
289
  const randomHue = Math.random() * 360;
290
+ keyElement.style.setProperty('--key-glow-color', `hsla(${randomHue}, 90%, 65%, 0.85)`);
291
+ synth.triggerAttack(noteName, Tone.now());
292
+ createKeyParticles(keyElement, `hsla(${randomHue}, 90%, 65%, 0.85)`);
293
  };
294
+ const releaseNote = (event) => {
295
+ if (event && event.preventDefault) event.preventDefault();
296
  keyElement.classList.remove('active');
297
+ synth.triggerRelease(noteName, Tone.now() + 0.1); // Slight release time
298
  };
299
 
300
+ keyElement.addEventListener('mousedown', playNote);
301
+ keyElement.addEventListener('mouseup', releaseNote);
302
+ keyElement.addEventListener('mouseleave', () => {
303
+ if (keyElement.classList.contains('active')) releaseNote();
304
+ });
305
+ keyElement.addEventListener('touchstart', playNote, { passive: false });
306
+ keyElement.addEventListener('touchend', releaseNote);
307
+ keyElement.addEventListener('touchcancel', releaseNote);
308
  });
309
  }
310
+ // Adjust piano width based on the number of white keys
311
+ pianoElement.style.width = (whiteKeyCounter * whiteKeyWidth + (whiteKeyCounter -1)) + 'px';
312
+
313
+
314
+ function createKeyParticles(keyElement, color) {
315
+ const keyRect = keyElement.getBoundingClientRect();
316
+ const noteFallRect = noteFallArea.getBoundingClientRect(); // Use noteFallArea for correct relative positioning
317
+
318
+ for (let i = 0; i < 8; i++) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  const particle = document.createElement('div');
320
  particle.classList.add('particle');
321
  particle.style.setProperty('--particle-color', color);
322
 
323
+ // Calculate position relative to noteFallArea
324
+ const xPos = keyRect.left - noteFallRect.left + keyRect.width / 2 + (Math.random() - 0.5) * 15;
325
+ const yPos = keyRect.top - noteFallRect.top + keyRect.height / 2 + (Math.random() - 0.5) * 10; // From middle of key
326
+
327
+ particle.style.left = `${xPos}px`;
328
+ particle.style.top = `${yPos}px`; // Particles originate from key press
329
+
330
+ particleContainer.appendChild(particle);
331
+ setTimeout(() => particle.remove(), 600);
332
  }
333
  }
334
 
335
+ const hitLinePositionVh = 100 - 28 - (5 / window.innerHeight * 100); // hit-line bottom in vh
336
+ const noteFallDurationMs = 3000; // Notes take 3 seconds to fall
337
 
338
+ let currentMidiEvents = [];
339
+ let currentMidiTempo = 120; // Default BPM
340
+
341
+ async function loadAndPlayMidi(url) {
342
+ if (!url) {
343
+ // Default sample if no URL is provided (optional)
344
+ // playSampleData(getTwinkleTwinkle()); // Or some other default
345
+ alert("Please enter a MIDI URL.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  return;
347
  }
 
 
 
348
 
349
+ Tone.Transport.stop();
350
+ Tone.Transport.cancel(); // Clear previous events
351
+ document.querySelectorAll('.note-bar').forEach(n => n.remove());
352
+ loadingIndicator.style.display = 'inline';
353
+ loadMidiButton.disabled = true;
354
+ loadMidiButton.textContent = 'Loading...';
355
+
356
+ try {
357
+ const response = await fetch(url);
358
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
359
+ const arrayBuffer = await response.arrayBuffer();
360
+ const midi = MidiConvert.parse(arrayBuffer);
361
+
362
+ if (!midi || !midi.tracks || midi.tracks.length === 0) {
363
+ throw new Error("Invalid MIDI data or no tracks found.");
364
+ }
365
+
366
+ // Try to find tempo from MIDI meta events
367
+ if (midi.header && midi.header.tempos && midi.header.tempos.length > 0) {
368
+ currentMidiTempo = midi.header.tempos[0].bpm;
369
+ } else {
370
+ currentMidiTempo = 120; // Fallback
371
+ }
372
+ Tone.Transport.bpm.value = currentMidiTempo;
373
+
374
+ console.log("MIDI Loaded:", midi);
375
+ console.log("Tempo:", currentMidiTempo, "BPM");
376
+
377
+ currentMidiEvents = [];
378
+ midi.tracks.forEach(track => {
379
+ if (track.notes) {
380
+ track.notes.forEach(note => {
381
+ currentMidiEvents.push({
382
+ time: note.time, // Time in seconds
383
+ note: Tone.Frequency(note.midi, "midi").toNote(), // Convert MIDI number to note name
384
+ duration: note.duration, // Duration in seconds
385
+ velocity: note.velocity // Velocity (0-1)
386
+ });
387
+ });
388
+ }
389
+ });
390
+
391
+ currentMidiEvents.sort((a, b) => a.time - b.time); // Ensure notes are sorted by time
392
 
393
+ scheduleMidiEvents();
394
+ Tone.Transport.start();
395
+ loadMidiButton.textContent = 'Stop MIDI';
396
+
397
+ } catch (error) {
398
+ console.error("Error loading or parsing MIDI:", error);
399
+ alert("Failed to load or parse MIDI file. Check console for details. Error: " + error.message);
400
+ loadMidiButton.textContent = 'Load & Play MIDI';
401
+ } finally {
402
+ loadingIndicator.style.display = 'none';
403
+ loadMidiButton.disabled = false;
404
+ }
405
+ }
406
+
407
+ function scheduleMidiEvents() {
408
+ const noteColors = [
409
+ [0, 255, 127], // Spring Green
410
+ [255, 105, 180], // Hot Pink
411
+ [138, 43, 226], // Blue Violet
412
+ [255, 215, 0], // Gold
413
+ [0, 191, 255], // Deep Sky Blue
414
+ [255, 69, 0] // Orange Red
415
+ ];
416
+ let colorIndex = 0;
417
+
418
+ currentMidiEvents.forEach(noteData => {
419
+ Tone.Transport.scheduleOnce(time => {
420
+ const targetKeyElement = pianoKeys[noteData.note];
421
  if (targetKeyElement) {
422
+ const noteElement = document.createElement('div');
423
+ noteElement.classList.add('note-bar');
424
+
425
  const keyRect = targetKeyElement.getBoundingClientRect();
426
+ const pianoRect = pianoElement.getBoundingClientRect(); // Reference for piano keys
427
+
428
+ noteElement.style.width = (noteData.note.includes('#') ? blackKeyWidth : whiteKeyWidth) - 2 + 'px';
429
  noteElement.style.left = (keyRect.left - pianoRect.left + 1) + 'px';
 
430
 
431
+ // Calculate height based on duration and BPM
432
+ // (note.duration / (60 / bpm)) gives number of beats
433
+ // Multiply by a factor for visual height
434
+ const noteHeight = (noteData.duration / (60 / currentMidiTempo)) * 50; // 50px per beat (adjust as needed)
435
+ noteElement.style.height = Math.max(10, noteHeight) + 'px'; // Minimum height
436
 
437
+ // Assign a color cyclically
438
+ const [r, g, b] = noteColors[colorIndex % noteColors.length];
439
+ colorIndex++;
440
+ noteElement.style.setProperty('--note-gradient', `linear-gradient(to bottom, rgba(${r}, ${g}, ${b}, 0.9), rgba(${r*0.6}, ${g*0.6}, ${b*0.6}, 0.9))`);
441
+ noteElement.style.setProperty('--note-glow', `rgba(${r}, ${g}, ${b}, 0.7)`);
442
 
 
 
443
 
444
+ noteElement.style.top = `-${noteElement.style.height}`;
445
+ noteFallArea.appendChild(noteElement);
446
+
447
+ const fallAreaHeight = noteFallArea.clientHeight;
448
  noteElement.animate([
449
+ { transform: `translateY(0px)` },
450
+ { transform: `translateY(${fallAreaHeight + parseFloat(noteElement.style.height)}px)` }
451
  ], {
452
+ duration: noteFallDurationMs,
453
  easing: 'linear'
454
  });
455
+
456
+ // Schedule sound and visual feedback
457
  Tone.Transport.scheduleOnce(hitTime => {
458
+ synth.triggerAttackRelease(noteData.note, noteData.duration, hitTime, noteData.velocity);
459
  targetKeyElement.classList.add('active');
460
+ targetKeyElement.style.setProperty('--key-glow-color', `rgba(${r}, ${g}, ${b}, 0.85)`);
461
+ createKeyParticles(targetKeyElement, `rgba(${r}, ${g}, ${b}, 0.85)`);
462
+
 
463
  setTimeout(() => {
464
  targetKeyElement.classList.remove('active');
465
+ }, noteData.duration * 1000);
466
 
 
467
  setTimeout(() => {
468
+ if (noteElement.parentNode) noteElement.remove();
469
+ }, noteFallDurationMs);
470
+ }, time + (noteFallDurationMs / 1000)); // Trigger sound when note reaches hit line
 
 
 
471
  }
472
+ }, noteData.time); // Schedule note bar creation at its MIDI time
473
  });
 
 
474
  }
475
 
476
+
477
+ loadMidiButton.addEventListener('click', async () => {
478
+ await Tone.start(); // Ensure AudioContext is started by user interaction
479
+ if (Tone.Transport.state === 'started') {
480
+ Tone.Transport.stop();
481
+ Tone.Transport.cancel();
482
+ document.querySelectorAll('.note-bar').forEach(n => n.remove());
483
+ loadMidiButton.textContent = 'Load & Play MIDI';
484
+ } else {
485
+ const url = midiUrlInput.value.trim();
486
+ if (url) {
487
+ loadAndPlayMidi(url);
488
+ } else {
489
+ // Play a default sample if no URL is provided
490
+ loadAndPlayMidi("https://cdn.jsdelivr.net/gh/Tonejs/MidiConvert/examples/midi/Faur_-_Sicilienne_Op_78.mid"); // Example MIDI
491
+ }
492
+ }
493
+ });
494
+
495
+ // Adjust piano width dynamically on resize for responsiveness
496
+ window.addEventListener('resize', () => {
497
+ let currentWhiteKeyCount = 0;
498
+ for (let octave = startOctave; octave <= endOctave; octave++) {
499
+ notesOrder.forEach(noteBase => {
500
+ if (octave === endOctave && noteBase !== 'C') return;
501
+ if (!noteBase.includes('#')) currentWhiteKeyCount++;
502
+ });
503
+ }
504
+ pianoElement.style.width = (currentWhiteKeyCount * whiteKeyWidth + (currentWhiteKeyCount-1)) + 'px';
505
+
506
+ // Re-calculate hitLineY if needed for falling notes logic
507
+ // hitLineY = noteFallArea.offsetHeight; (If you change how hitLineY is defined)
508
  });
509
 
510
+
511
  </script>
512
  </body>
513
  </html>