lonestar108 commited on
Commit
1bbafd0
·
verified ·
1 Parent(s): d839c0b

enhance it, fix the keys offset rendering issue, improve everything

Browse files
Files changed (2) hide show
  1. about.html +139 -0
  2. index.html +326 -78
about.html ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>About - Sonic Canvas</title>
7
+ <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://unpkg.com/feather-icons"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.waves.min.js"></script>
11
+ </head>
12
+ <body class="bg-gray-900 text-white">
13
+ <!-- Header -->
14
+ <header class="absolute top-0 left-0 right-0 z-50 p-4 flex justify-between items-center bg-gray-900 bg-opacity-80 backdrop-blur-sm">
15
+ <div class="flex items-center space-x-3">
16
+ <div class="p-2 rounded-lg bg-gradient-to-r from-purple-500 to-indigo-600">
17
+ <i data-feather="music" class="text-white"></i>
18
+ </div>
19
+ <h1 class="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 via-pink-400 to-indigo-300">
20
+ Sonic Canvas
21
+ </h1>
22
+ </div>
23
+ <nav class="flex space-x-4">
24
+ <a href="index.html" class="px-4 py-2 rounded-lg hover:bg-gray-800 transition">Home</a>
25
+ <a href="about.html" class="px-4 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-indigo-600">About</a>
26
+ </nav>
27
+ </header>
28
+
29
+ <!-- Main Content -->
30
+ <div id="content" class="min-h-screen flex items-center justify-center p-8 pt-24">
31
+ <div class="max-w-4xl mx-auto">
32
+ <div class="text-center mb-12">
33
+ <h1 class="text-5xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-indigo-300">
34
+ About Sonic Canvas
35
+ </h1>
36
+ <p class="text-xl text-gray-300 max-w-2xl mx-auto">
37
+ An innovative step sequencer that transforms your browser into a musical playground
38
+ </p>
39
+ </div>
40
+
41
+ <div class="grid md:grid-cols-2 gap-8 mb-12">
42
+ <div class="bg-gray-800 bg-opacity-50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
43
+ <div class="flex items-center mb-4">
44
+ <div class="p-3 rounded-lg bg-gradient-to-r from-purple-500 to-indigo-600 mr-4">
45
+ <i data-feather="grid" class="w-6 h-6"></i>
46
+ </div>
47
+ <h2 class="text-2xl font-bold">Interactive Grid</h2>
48
+ </div>
49
+ <p class="text-gray-300">
50
+ Our 12x16 grid interface allows you to visually compose music by activating cells.
51
+ Each row represents a different note, and each column represents a step in time.
52
+ </p>
53
+ </div>
54
+
55
+ <div class="bg-gray-800 bg-opacity-50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
56
+ <div class="flex items-center mb-4">
57
+ <div class="p-3 rounded-lg bg-gradient-to-r from-pink-500 to-rose-600 mr-4">
58
+ <i data-feather="music" class="w-6 h-6"></i>
59
+ </div>
60
+ <h2 class="text-2xl font-bold">Musical Scales</h2>
61
+ </div>
62
+ <p class="text-gray-300">
63
+ Choose from various musical scales including Major, Minor, Pentatonic, Blues,
64
+ Dorian, and Mixolydian to create different moods and styles.
65
+ </p>
66
+ </div>
67
+
68
+ <div class="bg-gray-800 bg-opacity-50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
69
+ <div class="flex items-center mb-4">
70
+ <div class="p-3 rounded-lg bg-gradient-to-r from-blue-500 to-cyan-600 mr-4">
71
+ <i data-feather="sliders" class="w-6 h-6"></i>
72
+ </div>
73
+ <h2 class="text-2xl font-bold">Precise Control</h2>
74
+ </div>
75
+ <p class="text-gray-300">
76
+ Adjust tempo, volume, and octave range to fine-tune your composition.
77
+ The virtual keyboard lets you preview notes before adding them to your sequence.
78
+ </p>
79
+ </div>
80
+
81
+ <div class="bg-gray-800 bg-opacity-50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
82
+ <div class="flex items-center mb-4">
83
+ <div class="p-3 rounded-lg bg-gradient-to-r from-green-500 to-emerald-600 mr-4">
84
+ <i data-feather="code" class="w-6 h-6"></i>
85
+ </div>
86
+ <h2 class="text-2xl font-bold">Web Audio API</h2>
87
+ </div>
88
+ <p class="text-gray-300">
89
+ Built with modern web technologies including Web Audio API for high-quality
90
+ sound synthesis and smooth performance directly in your browser.
91
+ </p>
92
+ </div>
93
+ </div>
94
+
95
+ <div class="text-center">
96
+ <h2 class="text-3xl font-bold mb-6">How to Use</h2>
97
+ <div class="bg-gray-800 bg-opacity-50 backdrop-blur-sm rounded-xl p-8 border border-gray-700">
98
+ <ol class="list-decimal list-inside space-y-4 text-left max-w-2xl mx-auto text-gray-300">
99
+ <li>Click cells in the grid to activate notes</li>
100
+ <li>Adjust tempo and volume using the sliders</li>
101
+ <li>Change octaves and musical scales for different sounds</li>
102
+ <li>Use the virtual keyboard to preview notes</li>
103
+ <li>Press Play to hear your composition come to life</li>
104
+ <li>Press Clear to start a new sequence</li>
105
+ </ol>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Footer -->
112
+ <footer class="py-8 text-center text-gray-500 text-sm border-t border-gray-800 mt-12">
113
+ <div class="container mx-auto px-4">
114
+ <p>© 2023 Sonic Canvas - A modern step sequencer experience</p>
115
+ </div>
116
+ </footer>
117
+
118
+ <script>
119
+ // Initialize Feather Icons
120
+ feather.replace();
121
+
122
+ // Initialize Vanta.js background
123
+ VANTA.WAVES({
124
+ el: "#content",
125
+ mouseControls: true,
126
+ touchControls: true,
127
+ gyroControls: false,
128
+ minHeight: 200.00,
129
+ minWidth: 200.00,
130
+ scale: 1.00,
131
+ scaleMobile: 1.00,
132
+ color: 0x1a1a2e,
133
+ shininess: 15.00,
134
+ waveHeight: 10.00,
135
+ waveSpeed: 0.50
136
+ });
137
+ </script>
138
+ </body>
139
+ </html>
index.html CHANGED
@@ -12,91 +12,236 @@
12
  .note-cell {
13
  transition: all 0.2s ease;
14
  cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  }
16
  .note-cell:hover {
17
- transform: scale(1.1);
18
  z-index: 10;
19
  }
20
  .active-note {
21
  animation: pulse 0.5s ease-in-out;
 
22
  }
23
  @keyframes pulse {
24
  0% { transform: scale(1); }
25
- 50% { transform: scale(1.3); }
26
  100% { transform: scale(1); }
27
  }
28
- .keyboard-key {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  transition: all 0.1s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
- .keyboard-key.active {
32
- transform: translateY(5px);
33
- box-shadow: inset 0 4px 8px rgba(0,0,0,0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
  </style>
36
  </head>
37
  <body class="bg-gray-900 text-white overflow-hidden">
38
  <!-- Header -->
39
- <header class="absolute top-0 left-0 right-0 z-50 p-4 flex justify-between items-center">
40
- <div class="flex items-center space-x-2">
41
- <i data-feather="music" class="text-indigo-400"></i>
42
- <h1 class="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-indigo-300">Sonic Canvas</h1>
 
 
 
 
43
  </div>
44
- <div class="flex space-x-4">
45
- <button id="playBtn" class="px-4 py-2 bg-indigo-600 rounded-lg hover:bg-indigo-700 transition flex items-center">
46
  <i data-feather="play" class="mr-2"></i> Play
47
  </button>
48
- <button id="clearBtn" class="px-4 py-2 bg-purple-600 rounded-lg hover:bg-purple-700 transition flex items-center">
49
  <i data-feather="trash-2" class="mr-2"></i> Clear
50
  </button>
51
  </div>
52
  </header>
53
 
54
  <!-- Main Grid -->
55
- <div id="grid-container" class="grid grid-cols-16 grid-rows-12 h-screen w-screen p-8 pt-20">
56
  <!-- Grid will be generated by JS -->
57
  </div>
58
 
59
  <!-- Controls -->
60
- <div class="absolute bottom-0 left-0 right-0 bg-gray-800 bg-opacity-80 p-4 backdrop-blur-sm">
61
- <div class="max-w-6xl mx-auto">
62
- <div class="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
63
- <div class="flex items-center space-x-4">
64
- <label class="flex items-center">
65
- <span class="mr-2">Tempo:</span>
66
- <input type="range" id="tempo" min="60" max="240" value="120" class="w-32">
67
- <span id="tempo-value" class="ml-2">120 BPM</span>
68
- </label>
69
- </div>
70
-
71
- <div class="flex space-x-2">
72
- <button id="octave-down" class="p-2 bg-gray-700 rounded hover:bg-gray-600">
73
- <i data-feather="chevrons-down"></i>
74
- </button>
75
- <span id="octave-display" class="px-3 py-2 bg-gray-700 rounded">Octave 4</span>
76
- <button id="octave-up" class="p-2 bg-gray-700 rounded hover:bg-gray-600">
77
- <i data-feather="chevrons-up"></i>
78
- </button>
79
  </div>
80
 
81
- <div class="flex items-center space-x-2">
82
- <span>Scale:</span>
83
- <select id="scale-select" class="bg-gray-700 rounded px-3 py-2">
84
- <option value="major">Major</option>
85
- <option value="minor">Minor</option>
86
- <option value="pentatonic">Pentatonic</option>
87
- <option value="blues">Blues</option>
88
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </div>
90
  </div>
91
 
92
  <!-- Virtual Keyboard -->
93
- <div id="keyboard" class="mt-4 flex justify-center">
94
- <!-- Keyboard will be generated by JS -->
 
 
 
 
 
 
 
 
 
 
 
95
  </div>
96
  </div>
97
  </div>
98
-
99
- <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
100
  <script>
101
  // Initialize Feather Icons
102
  feather.replace();
@@ -116,11 +261,15 @@
116
  waveHeight: 10.00,
117
  waveSpeed: 0.50
118
  });
119
-
120
  // Audio context
121
  const AudioContext = window.AudioContext || window.webkitAudioContext;
122
  const audioCtx = new AudioContext();
123
 
 
 
 
 
 
124
  // Grid configuration
125
  const rows = 12;
126
  const cols = 16;
@@ -128,9 +277,11 @@
128
  let grid = Array(rows).fill().map(() => Array(cols).fill(0));
129
  let isPlaying = false;
130
  let tempo = 120;
 
131
  let currentStep = 0;
132
  let octave = 4;
133
  let scale = 'major';
 
134
 
135
  // Note frequencies (C4 = 261.63 Hz)
136
  const noteFrequencies = {
@@ -153,7 +304,27 @@
153
  major: [0, 2, 4, 5, 7, 9, 11],
154
  minor: [0, 2, 3, 5, 7, 8, 10],
155
  pentatonic: [0, 2, 4, 7, 9],
156
- blues: [0, 3, 5, 6, 7, 10]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  };
158
 
159
  // Create grid
@@ -162,24 +333,29 @@
162
  for (let row = 0; row < rows; row++) {
163
  for (let col = 0; col < cols; col++) {
164
  const cell = document.createElement('div');
165
- cell.className = `note-cell border border-gray-700 rounded relative`;
166
  cell.dataset.row = row;
167
  cell.dataset.col = col;
168
 
169
  // Add note label
170
  const noteLabel = document.createElement('span');
171
- noteLabel.className = 'absolute top-1 left-1 text-xs opacity-50';
172
  noteLabel.textContent = getNoteName(row);
173
  cell.appendChild(noteLabel);
174
 
175
  // Add step indicator
176
  if (row === 0) {
177
  const stepLabel = document.createElement('span');
178
- stepLabel.className = 'absolute bottom-1 right-1 text-xs opacity-50';
179
  stepLabel.textContent = col + 1;
180
  cell.appendChild(stepLabel);
181
  }
182
 
 
 
 
 
 
183
  cell.addEventListener('click', () => toggleNote(row, col));
184
  gridContainer.appendChild(cell);
185
  }
@@ -199,6 +375,11 @@
199
  function toggleNote(row, col) {
200
  grid[row][col] = grid[row][col] ? 0 : 1;
201
  updateGridDisplay();
 
 
 
 
 
202
  }
203
 
204
  // Update grid display
@@ -208,27 +389,30 @@
208
  const row = parseInt(cell.dataset.row);
209
  const col = parseInt(cell.dataset.col);
210
  if (grid[row][col]) {
211
- cell.classList.add('bg-indigo-500');
212
  cell.classList.remove('bg-gray-800');
213
  } else {
214
- cell.classList.remove('bg-indigo-500');
215
  cell.classList.add('bg-gray-800');
216
  }
217
  });
218
  }
219
 
220
  // Play a note
221
- function playNote(frequency, duration = 0.5) {
222
  const oscillator = audioCtx.createOscillator();
223
  const gainNode = audioCtx.createGain();
224
 
225
  oscillator.connect(gainNode);
226
- gainNode.connect(audioCtx.destination);
227
 
228
  oscillator.type = 'sine';
229
  oscillator.frequency.value = frequency;
230
 
231
- gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
 
 
 
232
  gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
233
 
234
  oscillator.start();
@@ -239,15 +423,14 @@
239
  function playSequence() {
240
  if (!isPlaying) return;
241
 
 
 
 
 
 
242
  // Highlight current column
243
- const cells = document.querySelectorAll('.note-cell');
244
- cells.forEach(cell => {
245
- const col = parseInt(cell.dataset.col);
246
- if (col === currentStep) {
247
- cell.classList.add('bg-purple-500');
248
- } else {
249
- cell.classList.remove('bg-purple-500');
250
- }
251
  });
252
 
253
  // Play notes in current column
@@ -255,8 +438,10 @@
255
  if (grid[row][currentStep]) {
256
  const scaleNotes = scales[scale];
257
  const noteIndex = scaleNotes[11 - row] || 0;
258
- const frequency = noteFrequencies[getNoteName(row)] * Math.pow(2, octave - 4);
259
- playNote(frequency, 60 / tempo);
 
 
260
 
261
  // Add visual feedback
262
  const cell = document.querySelector(`.note-cell[data-row="${row}"][data-col="${currentStep}"]`);
@@ -266,7 +451,13 @@
266
  }
267
 
268
  currentStep = (currentStep + 1) % cols;
269
- setTimeout(playSequence, (60 / tempo) * 1000);
 
 
 
 
 
 
270
  }
271
 
272
  // Create virtual keyboard
@@ -275,25 +466,33 @@
275
  keyboard.innerHTML = '';
276
 
277
  const whiteKeys = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
278
- const blackKeys = ['C#', 'D#', null, 'F#', 'G#', 'A#', null];
 
 
 
 
 
 
279
 
280
  // Create white keys
281
  whiteKeys.forEach((note, i) => {
282
  const key = document.createElement('div');
283
- key.className = 'keyboard-key w-12 h-32 bg-white border border-gray-300 rounded-b-md flex items-end justify-center pb-2 relative';
284
  key.dataset.note = note;
285
- key.textContent = note;
 
286
  key.addEventListener('mousedown', () => playKeyNote(note));
287
  keyboard.appendChild(key);
288
 
289
  // Add black key if needed
290
- if (blackKeys[i]) {
291
  const blackKey = document.createElement('div');
292
- blackKey.className = 'keyboard-key absolute w-8 h-20 bg-gray-900 rounded-b-md flex items-end justify-center pb-2 text-white -ml-6';
293
- blackKey.dataset.note = blackKeys[i];
294
- blackKey.textContent = blackKeys[i];
295
- blackKey.style.left = `${i * 48 + 36}px`;
296
- blackKey.addEventListener('mousedown', () => playKeyNote(blackKeys[i]));
 
297
  keyboard.appendChild(blackKey);
298
  }
299
  });
@@ -302,18 +501,47 @@
302
  // Play note from keyboard
303
  function playKeyNote(note) {
304
  const frequency = noteFrequencies[note] * Math.pow(2, octave - 4);
305
- playNote(frequency, 0.5);
306
 
307
  // Visual feedback
308
- const key = document.querySelector(`.keyboard-key[data-note="${note}"]`);
309
  if (key) {
310
  key.classList.add('active');
311
  setTimeout(() => key.classList.remove('active'), 200);
312
  }
313
  }
314
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  // Event listeners
316
  document.getElementById('playBtn').addEventListener('click', () => {
 
 
 
 
 
317
  isPlaying = !isPlaying;
318
  document.getElementById('playBtn').innerHTML = isPlaying ?
319
  '<i data-feather="pause" class="mr-2"></i> Pause' :
@@ -322,7 +550,13 @@
322
 
323
  if (isPlaying) {
324
  currentStep = 0;
325
- playSequence();
 
 
 
 
 
 
326
  }
327
  });
328
 
@@ -334,6 +568,15 @@
334
  document.getElementById('tempo').addEventListener('input', (e) => {
335
  tempo = e.target.value;
336
  document.getElementById('tempo-value').textContent = `${tempo} BPM`;
 
 
 
 
 
 
 
 
 
337
  });
338
 
339
  document.getElementById('octave-down').addEventListener('click', () => {
@@ -358,9 +601,14 @@
358
  createKeyboard();
359
  });
360
 
 
 
 
 
361
  // Initialize
362
  createGrid();
363
  createKeyboard();
 
364
  </script>
365
  </body>
366
  </html>
 
12
  .note-cell {
13
  transition: all 0.2s ease;
14
  cursor: pointer;
15
+ position: relative;
16
+ overflow: hidden;
17
+ }
18
+ .note-cell::before {
19
+ content: '';
20
+ position: absolute;
21
+ top: 0;
22
+ left: 0;
23
+ width: 100%;
24
+ height: 100%;
25
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 80%);
26
+ opacity: 0;
27
+ transition: opacity 0.3s ease;
28
+ }
29
+ .note-cell:hover::before {
30
+ opacity: 1;
31
  }
32
  .note-cell:hover {
33
+ transform: scale(1.05);
34
  z-index: 10;
35
  }
36
  .active-note {
37
  animation: pulse 0.5s ease-in-out;
38
+ box-shadow: 0 0 15px rgba(139, 92, 246, 0.7);
39
  }
40
  @keyframes pulse {
41
  0% { transform: scale(1); }
42
+ 50% { transform: scale(1.15); }
43
  100% { transform: scale(1); }
44
  }
45
+ .playing-column {
46
+ background-color: rgba(139, 92, 246, 0.3) !important;
47
+ box-shadow: inset 0 0 10px rgba(139, 92, 246, 0.5);
48
+ }
49
+ .keyboard-container {
50
+ position: relative;
51
+ height: 150px;
52
+ display: flex;
53
+ padding: 0 10px;
54
+ }
55
+ .white-key {
56
+ position: relative;
57
+ width: 50px;
58
+ height: 100%;
59
+ background: linear-gradient(to bottom, #fff 0%, #f5f5f5 100%);
60
+ border: 1px solid #ccc;
61
+ border-radius: 0 0 5px 5px;
62
+ margin-right: -1px;
63
+ z-index: 1;
64
+ display: flex;
65
+ align-items: flex-end;
66
+ justify-content: center;
67
+ padding-bottom: 10px;
68
+ font-size: 12px;
69
+ color: #333;
70
+ cursor: pointer;
71
+ transition: all 0.1s ease;
72
+ box-shadow: 0 5px 5px rgba(0,0,0,0.2);
73
+ }
74
+ .white-key.active {
75
+ background: linear-gradient(to bottom, #e0e0e0 0%, #d0d0d0 100%);
76
+ transform: translateY(3px);
77
+ box-shadow: 0 2px 2px rgba(0,0,0,0.2);
78
+ }
79
+ .black-key {
80
+ position: absolute;
81
+ width: 30px;
82
+ height: 60%;
83
+ background: linear-gradient(to bottom, #000 0%, #333 100%);
84
+ border: 1px solid #000;
85
+ border-radius: 0 0 3px 3px;
86
+ z-index: 2;
87
+ display: flex;
88
+ align-items: flex-end;
89
+ justify-content: center;
90
+ padding-bottom: 10px;
91
+ font-size: 10px;
92
+ color: #fff;
93
+ cursor: pointer;
94
  transition: all 0.1s ease;
95
+ box-shadow: 0 3px 3px rgba(0,0,0,0.3);
96
+ }
97
+ .black-key.active {
98
+ background: linear-gradient(to bottom, #333 0%, #555 100%);
99
+ transform: translateY(2px);
100
+ box-shadow: 0 1px 1px rgba(0,0,0,0.3);
101
+ }
102
+ .control-panel {
103
+ background: rgba(31, 41, 55, 0.85);
104
+ backdrop-filter: blur(10px);
105
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
106
+ }
107
+ .btn-primary {
108
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
109
+ border: none;
110
+ transition: all 0.3s ease;
111
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
112
  }
113
+ .btn-primary:hover {
114
+ transform: translateY(-2px);
115
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
116
+ }
117
+ .btn-secondary {
118
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
119
+ border: none;
120
+ transition: all 0.3s ease;
121
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
122
+ }
123
+ .btn-secondary:hover {
124
+ transform: translateY(-2px);
125
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
126
+ }
127
+ .slider {
128
+ -webkit-appearance: none;
129
+ width: 100%;
130
+ height: 6px;
131
+ border-radius: 3px;
132
+ background: linear-gradient(90deg, #667eea, #764ba2);
133
+ outline: none;
134
+ }
135
+ .slider::-webkit-slider-thumb {
136
+ -webkit-appearance: none;
137
+ width: 18px;
138
+ height: 18px;
139
+ border-radius: 50%;
140
+ background: #fff;
141
+ cursor: pointer;
142
+ box-shadow: 0 2px 4px rgba(0,0,0,0.3);
143
+ }
144
+ .pulse-animation {
145
+ animation: pulse-glow 2s infinite;
146
+ }
147
+ @keyframes pulse-glow {
148
+ 0% { box-shadow: 0 0 5px rgba(139, 92, 246, 0.5); }
149
+ 50% { box-shadow: 0 0 20px rgba(139, 92, 246, 0.8); }
150
+ 100% { box-shadow: 0 0 5px rgba(139, 92, 246, 0.5); }
151
  }
152
  </style>
153
  </head>
154
  <body class="bg-gray-900 text-white overflow-hidden">
155
  <!-- Header -->
156
+ <header class="absolute top-0 left-0 right-0 z-50 p-4 flex justify-between items-center bg-gray-900 bg-opacity-80 backdrop-blur-sm">
157
+ <div class="flex items-center space-x-3">
158
+ <div class="p-2 rounded-lg bg-gradient-to-r from-purple-500 to-indigo-600">
159
+ <i data-feather="music" class="text-white"></i>
160
+ </div>
161
+ <h1 class="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 via-pink-400 to-indigo-300">
162
+ Sonic Canvas
163
+ </h1>
164
  </div>
165
+ <div class="flex space-x-3">
166
+ <button id="playBtn" class="px-5 py-2 btn-primary rounded-lg flex items-center font-medium">
167
  <i data-feather="play" class="mr-2"></i> Play
168
  </button>
169
+ <button id="clearBtn" class="px-5 py-2 btn-secondary rounded-lg flex items-center font-medium">
170
  <i data-feather="trash-2" class="mr-2"></i> Clear
171
  </button>
172
  </div>
173
  </header>
174
 
175
  <!-- Main Grid -->
176
+ <div id="grid-container" class="grid grid-cols-16 grid-rows-12 h-screen w-screen p-6 pt-24 pb-40">
177
  <!-- Grid will be generated by JS -->
178
  </div>
179
 
180
  <!-- Controls -->
181
+ <div class="absolute bottom-0 left-0 right-0 control-panel p-5">
182
+ <div class="max-w-7xl mx-auto">
183
+ <div class="flex flex-col lg:flex-row justify-between items-center space-y-4 lg:space-y-0">
184
+ <div class="flex items-center space-x-6">
185
+ <div class="flex items-center space-x-3">
186
+ <i data-feather="clock" class="text-indigo-400"></i>
187
+ <label class="flex items-center">
188
+ <span class="mr-3 text-sm">Tempo:</span>
189
+ <input type="range" id="tempo" min="40" max="240" value="120" class="slider w-32">
190
+ <span id="tempo-value" class="ml-3 text-sm font-mono bg-gray-700 px-2 py-1 rounded">120 BPM</span>
191
+ </label>
192
+ </div>
193
+
194
+ <div class="flex items-center space-x-3">
195
+ <i data-feather="sliders" class="text-indigo-400"></i>
196
+ <span class="text-sm">Volume:</span>
197
+ <input type="range" id="volume" min="0" max="100" value="70" class="slider w-24">
198
+ <span id="volume-value" class="text-sm font-mono bg-gray-700 px-2 py-1 rounded">70%</span>
199
+ </div>
200
  </div>
201
 
202
+ <div class="flex space-x-4">
203
+ <div class="flex items-center space-x-2 bg-gray-700 rounded-lg px-3 py-2">
204
+ <button id="octave-down" class="p-1 hover:bg-gray-600 rounded">
205
+ <i data-feather="minus" class="w-4 h-4"></i>
206
+ </button>
207
+ <span id="octave-display" class="text-sm font-medium px-2">Octave 4</span>
208
+ <button id="octave-up" class="p-1 hover:bg-gray-600 rounded">
209
+ <i data-feather="plus" class="w-4 h-4"></i>
210
+ </button>
211
+ </div>
212
+
213
+ <div class="flex items-center space-x-2 bg-gray-700 rounded-lg px-3 py-2">
214
+ <span class="text-sm">Scale:</span>
215
+ <select id="scale-select" class="bg-gray-600 rounded px-2 py-1 text-sm">
216
+ <option value="major">Major</option>
217
+ <option value="minor">Minor</option>
218
+ <option value="pentatonic">Pentatonic</option>
219
+ <option value="blues">Blues</option>
220
+ <option value="dorian">Dorian</option>
221
+ <option value="mixolydian">Mixolydian</option>
222
+ </select>
223
+ </div>
224
  </div>
225
  </div>
226
 
227
  <!-- Virtual Keyboard -->
228
+ <div id="keyboard-container" class="mt-6">
229
+ <div class="flex justify-between items-center mb-3">
230
+ <h3 class="text-lg font-semibold flex items-center">
231
+ <i data-feather="keyboard" class="mr-2 text-indigo-400"></i>
232
+ Virtual Keyboard
233
+ </h3>
234
+ <div class="text-sm text-gray-400">
235
+ Click or press keys A-S-D-F-G-H-J-K-L-; (white keys) and W-E-T-Y-U-O-P (black keys)
236
+ </div>
237
+ </div>
238
+ <div id="keyboard" class="keyboard-container">
239
+ <!-- Keyboard will be generated by JS -->
240
+ </div>
241
  </div>
242
  </div>
243
  </div>
244
+ <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
 
245
  <script>
246
  // Initialize Feather Icons
247
  feather.replace();
 
261
  waveHeight: 10.00,
262
  waveSpeed: 0.50
263
  });
 
264
  // Audio context
265
  const AudioContext = window.AudioContext || window.webkitAudioContext;
266
  const audioCtx = new AudioContext();
267
 
268
+ // Global variables
269
+ let masterGain = audioCtx.createGain();
270
+ masterGain.connect(audioCtx.destination);
271
+ masterGain.gain.value = 0.7;
272
+
273
  // Grid configuration
274
  const rows = 12;
275
  const cols = 16;
 
277
  let grid = Array(rows).fill().map(() => Array(cols).fill(0));
278
  let isPlaying = false;
279
  let tempo = 120;
280
+ let volume = 70;
281
  let currentStep = 0;
282
  let octave = 4;
283
  let scale = 'major';
284
+ let schedulerInterval;
285
 
286
  // Note frequencies (C4 = 261.63 Hz)
287
  const noteFrequencies = {
 
304
  major: [0, 2, 4, 5, 7, 9, 11],
305
  minor: [0, 2, 3, 5, 7, 8, 10],
306
  pentatonic: [0, 2, 4, 7, 9],
307
+ blues: [0, 3, 5, 6, 7, 10],
308
+ dorian: [0, 2, 3, 5, 7, 9, 10],
309
+ mixolydian: [0, 2, 4, 5, 7, 9, 10]
310
+ };
311
+
312
+ // Keyboard mapping
313
+ const keyMap = {
314
+ // White keys
315
+ 'a': 'C',
316
+ 's': 'D',
317
+ 'd': 'E',
318
+ 'f': 'F',
319
+ 'g': 'G',
320
+ 'h': 'A',
321
+ 'j': 'B',
322
+ // Black keys
323
+ 'w': 'C#',
324
+ 'e': 'D#',
325
+ 't': 'F#',
326
+ 'y': 'G#',
327
+ 'u': 'A#'
328
  };
329
 
330
  // Create grid
 
333
  for (let row = 0; row < rows; row++) {
334
  for (let col = 0; col < cols; col++) {
335
  const cell = document.createElement('div');
336
+ cell.className = `note-cell border border-gray-700 rounded-lg relative flex items-center justify-center`;
337
  cell.dataset.row = row;
338
  cell.dataset.col = col;
339
 
340
  // Add note label
341
  const noteLabel = document.createElement('span');
342
+ noteLabel.className = 'absolute top-1 left-1 text-xs opacity-70 font-medium';
343
  noteLabel.textContent = getNoteName(row);
344
  cell.appendChild(noteLabel);
345
 
346
  // Add step indicator
347
  if (row === 0) {
348
  const stepLabel = document.createElement('span');
349
+ stepLabel.className = 'absolute bottom-1 right-1 text-xs opacity-70 font-medium';
350
  stepLabel.textContent = col + 1;
351
  cell.appendChild(stepLabel);
352
  }
353
 
354
+ // Add glow effect element
355
+ const glow = document.createElement('div');
356
+ glow.className = 'absolute inset-0 rounded-lg opacity-0 bg-gradient-to-br from-purple-500 to-indigo-600';
357
+ cell.appendChild(glow);
358
+
359
  cell.addEventListener('click', () => toggleNote(row, col));
360
  gridContainer.appendChild(cell);
361
  }
 
375
  function toggleNote(row, col) {
376
  grid[row][col] = grid[row][col] ? 0 : 1;
377
  updateGridDisplay();
378
+
379
+ // Visual feedback
380
+ const cell = document.querySelector(`.note-cell[data-row="${row}"][data-col="${col}"]`);
381
+ cell.classList.add('pulse-animation');
382
+ setTimeout(() => cell.classList.remove('pulse-animation'), 200);
383
  }
384
 
385
  // Update grid display
 
389
  const row = parseInt(cell.dataset.row);
390
  const col = parseInt(cell.dataset.col);
391
  if (grid[row][col]) {
392
+ cell.classList.add('bg-gradient-to-br', 'from-purple-600', 'to-indigo-700');
393
  cell.classList.remove('bg-gray-800');
394
  } else {
395
+ cell.classList.remove('bg-gradient-to-br', 'from-purple-600', 'to-indigo-700');
396
  cell.classList.add('bg-gray-800');
397
  }
398
  });
399
  }
400
 
401
  // Play a note
402
+ function playNote(frequency, duration = 0.5, volumeLevel = 1) {
403
  const oscillator = audioCtx.createOscillator();
404
  const gainNode = audioCtx.createGain();
405
 
406
  oscillator.connect(gainNode);
407
+ gainNode.connect(masterGain);
408
 
409
  oscillator.type = 'sine';
410
  oscillator.frequency.value = frequency;
411
 
412
+ // Apply volume setting
413
+ const adjustedVolume = (volume / 100) * volumeLevel;
414
+
415
+ gainNode.gain.setValueAtTime(adjustedVolume, audioCtx.currentTime);
416
  gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
417
 
418
  oscillator.start();
 
423
  function playSequence() {
424
  if (!isPlaying) return;
425
 
426
+ // Remove previous column highlight
427
+ document.querySelectorAll('.note-cell').forEach(cell => {
428
+ cell.classList.remove('playing-column');
429
+ });
430
+
431
  // Highlight current column
432
+ document.querySelectorAll(`.note-cell[data-col="${currentStep}"]`).forEach(cell => {
433
+ cell.classList.add('playing-column');
 
 
 
 
 
 
434
  });
435
 
436
  // Play notes in current column
 
438
  if (grid[row][currentStep]) {
439
  const scaleNotes = scales[scale];
440
  const noteIndex = scaleNotes[11 - row] || 0;
441
+ const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
442
+ const noteName = noteNames[noteIndex];
443
+ const frequency = noteFrequencies[noteName] * Math.pow(2, octave - 4);
444
+ playNote(frequency, (60 / tempo) * 0.8);
445
 
446
  // Add visual feedback
447
  const cell = document.querySelector(`.note-cell[data-row="${row}"][data-col="${currentStep}"]`);
 
451
  }
452
 
453
  currentStep = (currentStep + 1) % cols;
454
+ }
455
+
456
+ // Scheduler for precise timing
457
+ function schedulePlayback() {
458
+ clearInterval(schedulerInterval);
459
+ const interval = (60 / tempo) * 1000;
460
+ schedulerInterval = setInterval(playSequence, interval);
461
  }
462
 
463
  // Create virtual keyboard
 
466
  keyboard.innerHTML = '';
467
 
468
  const whiteKeys = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
469
+ const blackKeys = {
470
+ 1: 'C#',
471
+ 2: 'D#',
472
+ 4: 'F#',
473
+ 5: 'G#',
474
+ 6: 'A#'
475
+ };
476
 
477
  // Create white keys
478
  whiteKeys.forEach((note, i) => {
479
  const key = document.createElement('div');
480
+ key.className = 'white-key';
481
  key.dataset.note = note;
482
+ key.dataset.key = Object.keys(keyMap).find(key => keyMap[key] === note);
483
+ key.innerHTML = `<span>${note}<br><small>${key.dataset.key || ''}</small></span>`;
484
  key.addEventListener('mousedown', () => playKeyNote(note));
485
  keyboard.appendChild(key);
486
 
487
  // Add black key if needed
488
+ if (blackKeys[i+1]) {
489
  const blackKey = document.createElement('div');
490
+ blackKey.className = 'black-key';
491
+ blackKey.dataset.note = blackKeys[i+1];
492
+ blackKey.dataset.key = Object.keys(keyMap).find(key => keyMap[key] === blackKeys[i+1]);
493
+ blackKey.innerHTML = `<span>${blackKeys[i+1]}<br><small>${blackKey.dataset.key || ''}</small></span>`;
494
+ blackKey.style.left = `${(i * 50) + 35}px`;
495
+ blackKey.addEventListener('mousedown', () => playKeyNote(blackKeys[i+1]));
496
  keyboard.appendChild(blackKey);
497
  }
498
  });
 
501
  // Play note from keyboard
502
  function playKeyNote(note) {
503
  const frequency = noteFrequencies[note] * Math.pow(2, octave - 4);
504
+ playNote(frequency, 0.5, 0.8);
505
 
506
  // Visual feedback
507
+ const key = document.querySelector(`.white-key[data-note="${note}"], .black-key[data-note="${note}"]`);
508
  if (key) {
509
  key.classList.add('active');
510
  setTimeout(() => key.classList.remove('active'), 200);
511
  }
512
  }
513
 
514
+ // Handle keyboard events
515
+ function handleKeyDown(e) {
516
+ const key = e.key.toLowerCase();
517
+ if (keyMap[key]) {
518
+ playKeyNote(keyMap[key]);
519
+
520
+ // Visual feedback
521
+ const keyElement = document.querySelector(`.white-key[data-key="${key}"], .black-key[data-key="${key}"]`);
522
+ if (keyElement) {
523
+ keyElement.classList.add('active');
524
+ }
525
+ }
526
+ }
527
+
528
+ function handleKeyUp(e) {
529
+ const key = e.key.toLowerCase();
530
+ if (keyMap[key]) {
531
+ const keyElement = document.querySelector(`.white-key[data-key="${key}"], .black-key[data-key="${key}"]`);
532
+ if (keyElement) {
533
+ keyElement.classList.remove('active');
534
+ }
535
+ }
536
+ }
537
+
538
  // Event listeners
539
  document.getElementById('playBtn').addEventListener('click', () => {
540
+ // Resume audio context on first interaction
541
+ if (audioCtx.state === 'suspended') {
542
+ audioCtx.resume();
543
+ }
544
+
545
  isPlaying = !isPlaying;
546
  document.getElementById('playBtn').innerHTML = isPlaying ?
547
  '<i data-feather="pause" class="mr-2"></i> Pause' :
 
550
 
551
  if (isPlaying) {
552
  currentStep = 0;
553
+ schedulePlayback();
554
+ } else {
555
+ clearInterval(schedulerInterval);
556
+ // Remove column highlights
557
+ document.querySelectorAll('.note-cell').forEach(cell => {
558
+ cell.classList.remove('playing-column');
559
+ });
560
  }
561
  });
562
 
 
568
  document.getElementById('tempo').addEventListener('input', (e) => {
569
  tempo = e.target.value;
570
  document.getElementById('tempo-value').textContent = `${tempo} BPM`;
571
+ if (isPlaying) {
572
+ schedulePlayback();
573
+ }
574
+ });
575
+
576
+ document.getElementById('volume').addEventListener('input', (e) => {
577
+ volume = e.target.value;
578
+ document.getElementById('volume-value').textContent = `${volume}%`;
579
+ masterGain.gain.value = volume / 100;
580
  });
581
 
582
  document.getElementById('octave-down').addEventListener('click', () => {
 
601
  createKeyboard();
602
  });
603
 
604
+ // Keyboard event listeners
605
+ document.addEventListener('keydown', handleKeyDown);
606
+ document.addEventListener('keyup', handleKeyUp);
607
+
608
  // Initialize
609
  createGrid();
610
  createKeyboard();
611
+ feather.replace();
612
  </script>
613
  </body>
614
  </html>