kimhyunwoo commited on
Commit
5959290
ยท
verified ยท
1 Parent(s): 62e0bdf

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +334 -136
index.html CHANGED
@@ -3,207 +3,405 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
- <title>CSS Piano Interface</title>
 
7
  <style>
8
  body {
9
  margin: 0;
10
  height: 100vh;
11
- background-color: #000;
12
  display: flex;
13
  flex-direction: column;
14
- justify-content: flex-end; /* ๊ฑด๋ฐ˜์„ ํ•˜๋‹จ์— ๋ฐฐ์น˜ */
15
  align-items: center;
16
  overflow: hidden;
17
- font-family: sans-serif;
18
  }
19
 
20
- .piano-wrapper {
21
- /* 16:9 ๋น„์œจ์„ ๋Œ€๋žต์ ์œผ๋กœ ๋งž์ถ”๊ธฐ ์œ„ํ•ด ๋„ˆ๋น„๋ฅผ ํ™”๋ฉด ๋„ˆ๋น„์˜ ์ผ์ • ๋น„์œจ๋กœ ์„ค์ •ํ•˜๊ณ , ๋†’์ด๋Š” ๊ฑด๋ฐ˜ ๋†’์ด์— ๋งž์ถค */
22
- width: 90vw; /* ํ™”๋ฉด ๋„ˆ๋น„์˜ 90% */
23
- max-width: 1200px; /* ์ตœ๋Œ€ ๋„ˆ๋น„ ์ œํ•œ */
24
- height: 25vh; /* ํ™”๋ฉด ๋†’์ด์˜ 25% ์ •๋„, ๊ฑด๋ฐ˜ ๋†’์ด์— ๋”ฐ๋ผ ์กฐ์ ˆ */
25
- min-height: 200px; /* ์ตœ์†Œ ๊ฑด๋ฐ˜ ๋†’์ด */
26
  display: flex;
27
  justify-content: center;
28
  align-items: flex-end;
29
- padding-bottom: 20px; /* ํ•˜๋‹จ ์—ฌ๋ฐฑ */
 
30
  }
31
 
32
  .piano {
33
  display: flex;
34
  position: relative;
35
- background: linear-gradient(to bottom, #222, #111); /* ๊ฑด๋ฐ˜ ๋’ค์ชฝ ์–ด๋‘์šด ๋ฐฐ๊ฒฝ */
36
- padding: 10px 10px 0 10px; /* ๊ฑด๋ฐ˜ ์ƒ๋‹จ๊ณผ ์ขŒ์šฐ ํŒจ๋”ฉ */
37
- border-radius: 10px 10px 0 0;
38
- box-shadow: 0 0 30px rgba(128, 0, 128, 0.4), /* ๊ธฐ๋ณธ ๋ณด๋ผ์ƒ‰ ๋น› */
39
- 0 0 50px rgba(255, 105, 180, 0.3); /* ๊ธฐ๋ณธ ํ•‘ํฌ์ƒ‰ ๋น› */
40
- border: 2px solid #444;
41
  border-bottom: none;
 
42
  }
43
 
44
  .key {
45
  box-sizing: border-box;
46
  cursor: pointer;
47
- user-select: none; /* ํ…์ŠคํŠธ ์„ ํƒ ๋ฐฉ์ง€ */
48
- -webkit-tap-highlight-color: transparent; /* ๋ชจ๋ฐ”์ผ ํ„ฐ์น˜ ์‹œ ํ•˜์ด๋ผ์ดํŠธ ์ œ๊ฑฐ */
49
- transition: transform 0.05s ease-out, box-shadow 0.05s ease-out, background-color 0.05s ease-out;
 
50
  }
51
 
52
  .white-key {
53
- width: 50px; /* ํฐ ๊ฑด๋ฐ˜ ๋„ˆ๋น„ */
54
- height: 200px; /* ํฐ ๊ฑด๋ฐ˜ ๋†’์ด (piano-wrapper ๋†’์ด์™€ ์—ฐ๊ด€) */
55
- background: linear-gradient(to bottom, #f8f8f8, #e0e0e0);
56
- border: 1px solid #888;
57
- border-bottom: 5px solid #aaa;
58
- border-radius: 0 0 5px 5px;
59
  margin-right: 1px;
60
- box-shadow: 0 2px 3px rgba(0,0,0,0.2), inset 0 -2px 2px rgba(255,255,255,0.5);
61
- position: relative;
62
  z-index: 1;
63
  }
64
- .white-key:last-child {
65
- margin-right: 0;
66
- }
67
 
68
  .black-key {
69
- width: 30px; /* ๊ฒ€์€ ๊ฑด๋ฐ˜ ๋„ˆ๋น„ */
70
- height: 120px; /* ๊ฒ€์€ ๊ฑด๋ฐ˜ ๋†’์ด */
71
- background: linear-gradient(to bottom, #333, #111);
72
- border: 1px solid #000;
73
- border-bottom: 5px solid #222;
74
- border-radius: 0 0 3px 3px;
75
  position: absolute;
76
  z-index: 2;
77
- box-shadow: 0 2px 3px rgba(0,0,0,0.4), inset 0 -2px 2px rgba(100,100,100,0.3);
78
  }
79
 
80
- /* ํฐ ๊ฑด๋ฐ˜ ๋ˆŒ๋ ธ์„ ๋•Œ ํšจ๊ณผ */
81
  .white-key.active {
82
- background: linear-gradient(to bottom, #e0e0e0, #d0d0d0);
83
- transform: translateY(2px);
84
  border-bottom-width: 3px;
85
- box-shadow: 0 0 20px 5px var(--key-glow-color, rgba(255, 105, 180, 0.8)), /* ํ•‘ํฌ์ƒ‰ ๋น› */
86
- inset 0 -1px 1px rgba(255,255,255,0.3);
87
  }
88
 
89
- /* ๊ฒ€์€ ๊ฑด๋ฐ˜ ๋ˆŒ๋ ธ์„ ๋•Œ ํšจ๊ณผ */
90
  .black-key.active {
91
- background: linear-gradient(to bottom, #222, #000);
92
  transform: translateY(2px);
93
  border-bottom-width: 3px;
94
- box-shadow: 0 0 20px 5px var(--key-glow-color, rgba(255, 105, 180, 0.8)), /* ํ•‘ํฌ์ƒ‰ ๋น› */
95
- inset 0 -1px 1px rgba(100,100,100,0.1);
96
  }
97
-
98
- /* ๊ฑด๋ฐ˜ ๋ผ๋ฒจ (์„ ํƒ ์‚ฌํ•ญ) */
99
  .key-label {
100
  position: absolute;
101
- bottom: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  left: 50%;
103
  transform: translateX(-50%);
104
- font-size: 12px;
105
- color: #555;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  }
107
- .black-key .key-label {
108
- color: #ccc;
 
 
 
 
109
  }
110
 
111
- /* ๋™์  ์Šคํƒ€์ผ ์ฃผ์ž…์„ ์œ„ํ•œ ๊ณต๊ฐ„ */
112
- #dynamic-styles {
113
- display: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
 
116
  </style>
117
  </head>
118
  <body>
119
- <div class="piano-wrapper">
 
 
 
120
  <div class="piano" id="piano">
121
- <!-- JavaScript๋กœ ๊ฑด๋ฐ˜ ์ƒ์„ฑ -->
122
  </div>
123
  </div>
124
-
125
- <!-- ๋™์  ์Šคํƒ€์ผ ์ฃผ์ž…์šฉ (๋ˆŒ๋ ธ์„๋•Œ ๋‹ค๋ฅธ ์ƒ‰์ƒ ๋น›) -->
126
- <style id="dynamic-styles"></style>
 
127
 
128
  <script>
129
- const piano = document.getElementById('piano');
130
- const dynamicStyles = document.getElementById('dynamic-styles');
131
-
132
- const keysData = [
133
- // Octave 3
134
- { type: 'white', note: 'C3', label: 'C' },
135
- { type: 'black', note: 'C#3', label: 'C#', offset: 0.65 },
136
- { type: 'white', note: 'D3', label: 'D' },
137
- { type: 'black', note: 'D#3', label: 'D#', offset: 1.7 },
138
- { type: 'white', note: 'E3', label: 'E' },
139
- { type: 'white', note: 'F3', label: 'F' },
140
- { type: 'black', note: 'F#3', label: 'F#', offset: 3.65 },
141
- { type: 'white', note: 'G3', label: 'G' },
142
- { type: 'black', note: 'G#3', label: 'G#', offset: 4.7 },
143
- { type: 'white', note: 'A3', label: 'A' },
144
- { type: 'black', note: 'A#3', label: 'A#', offset: 5.75 },
145
- { type: 'white', note: 'B3', label: 'B' },
146
- // Octave 4
147
- { type: 'white', note: 'C4', label: 'C' },
148
- { type: 'black', note: 'C#4', label: 'C#', offset: 7.65 },
149
- { type: 'white', note: 'D4', label: 'D' },
150
- { type: 'black', note: 'D#4', label: 'D#', offset: 8.7 },
151
- { type: 'white', note: 'E4', label: 'E' },
152
- { type: 'white', note: 'F4', label: 'F' },
153
- { type: 'black', note: 'F#4', label: 'F#', offset: 10.65 },
154
- { type: 'white', note: 'G4', label: 'G' },
155
- { type: 'black', note: 'G#4', label: 'G#', offset: 11.7 },
156
- { type: 'white', note: 'A4', label: 'A' },
157
- { type: 'black', note: 'A#4', label: 'A#', offset: 12.75 },
158
- { type: 'white', note: 'B4', label: 'B' },
159
- // Octave 5
160
- { type: 'white', note: 'C5', label: 'C' },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  ];
162
 
163
- const whiteKeyWidth = 50; // CSS์™€ ๋™์ผํ•˜๊ฒŒ
164
- let whiteKeyIndex = 0;
165
-
166
- keysData.forEach(keyData => {
167
- const keyElement = document.createElement('div');
168
- keyElement.classList.add('key');
169
- keyElement.classList.add(keyData.type === 'white' ? 'white-key' : 'black-key');
170
- keyElement.dataset.note = keyData.note;
171
-
172
- // const labelElement = document.createElement('span');
173
- // labelElement.classList.add('key-label');
174
- // labelElement.textContent = keyData.label;
175
- // keyElement.appendChild(labelElement);
176
-
177
- if (keyData.type === 'black') {
178
- // ๊ฒ€์€ ๊ฑด๋ฐ˜์˜ offset์€ ์ด์ „ ํฐ ๊ฑด๋ฐ˜์˜ ์ˆ˜์— ๋”ฐ๋ผ ๊ณ„์‚ฐ
179
- keyElement.style.left = (keyData.offset * (whiteKeyWidth + 1)) + 'px'; // +1์€ margin-right ๊ณ ๋ ค
180
- } else {
181
- whiteKeyIndex++;
182
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
- piano.appendChild(keyElement);
185
-
186
- // ํ„ฐ์น˜ ๋ฐ ํด๋ฆญ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
187
- const activateKey = (event) => {
188
- event.preventDefault(); // ์Šคํฌ๋กค ๋“ฑ ๊ธฐ๋ณธ ๋™์ž‘ ๋ฐฉ์ง€
189
- keyElement.classList.add('active');
190
- // ๋ˆŒ๋ฆฐ ํ‚ค์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ƒ‰์ƒ์˜ ๋น› ํšจ๊ณผ (์„ ํƒ ์‚ฌํ•ญ)
191
- // const randomColor = `rgba(${Math.random()*255}, ${Math.random()*255}, ${Math.random()*255}, 0.8)`;
192
- // dynamicStyles.innerHTML = `.key.active { --key-glow-color: ${randomColor}; }`;
193
- };
194
-
195
- const deactivateKey = (event) => {
196
- event.preventDefault();
197
- keyElement.classList.remove('active');
198
- };
199
-
200
- keyElement.addEventListener('mousedown', activateKey);
201
- keyElement.addEventListener('mouseup', deactivateKey);
202
- keyElement.addEventListener('mouseleave', deactivateKey); // ๋งˆ์šฐ์Šค๊ฐ€ ๊ฑด๋ฐ˜ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๋ฉด ํšจ๊ณผ ์ œ๊ฑฐ
203
-
204
- keyElement.addEventListener('touchstart', activateKey, { passive: false });
205
- keyElement.addEventListener('touchend', deactivateKey);
206
- keyElement.addEventListener('touchcancel', deactivateKey);
207
  });
208
 
209
  </script>
 
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 {
47
  box-sizing: border-box;
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>