Csplk commited on
Commit
38db183
·
verified ·
1 Parent(s): f894b49

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +827 -19
index.html CHANGED
@@ -1,19 +1,827 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </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">
6
+ <title>ASCII Audio Visualizer</title>
7
+ <style>
8
+ /*
9
+ * MODERN CSS RESET & VARIABLES
10
+ */
11
+ :root {
12
+ --bg-color: #050505;
13
+ --text-color: #0f0; /* Default Matrix Green */
14
+ --secondary-color: #003300;
15
+ --ui-bg: rgba(0, 0, 0, 0.85);
16
+ --ui-border: 1px solid #333;
17
+ --font-stack: 'Courier New', Courier, monospace;
18
+ --scanline-color: rgba(0, 0, 0, 0.5);
19
+ --glass-blur: blur(10px);
20
+
21
+ /* Theme Colors */
22
+ --theme-green: #00ff41;
23
+ --theme-amber: #ffb000;
24
+ --theme-cyan: #00ffff;
25
+ --theme-pink: #ff00de;
26
+ --theme-white: #ffffff;
27
+ }
28
+
29
+ * {
30
+ box-sizing: border-box;
31
+ margin: 0;
32
+ padding: 0;
33
+ user-select: none;
34
+ -webkit-user-select: none;
35
+ }
36
+
37
+ body {
38
+ background-color: var(--bg-color);
39
+ color: var(--text-color);
40
+ font-family: var(--font-stack);
41
+ height: 100vh;
42
+ width: 100vw;
43
+ overflow: hidden;
44
+ display: flex;
45
+ flex-direction: column;
46
+ transition: color 0.3s ease;
47
+ }
48
+
49
+ /*
50
+ * HEADER & LINK
51
+ */
52
+ header {
53
+ position: absolute;
54
+ top: 0;
55
+ left: 0;
56
+ width: 100%;
57
+ padding: 1rem;
58
+ z-index: 10;
59
+ display: flex;
60
+ justify-content: space-between;
61
+ align-items: center;
62
+ pointer-events: none; /* Let clicks pass through to canvas where possible */
63
+ }
64
+
65
+ .brand {
66
+ font-size: 1.2rem;
67
+ font-weight: bold;
68
+ text-shadow: 0 0 5px currentColor;
69
+ pointer-events: auto;
70
+ }
71
+
72
+ .anycoder-link {
73
+ font-size: 0.8rem;
74
+ color: var(--text-color);
75
+ text-decoration: none;
76
+ opacity: 0.7;
77
+ pointer-events: auto;
78
+ border-bottom: 1px dashed currentColor;
79
+ transition: opacity 0.2s;
80
+ }
81
+ .anycoder-link:hover {
82
+ opacity: 1;
83
+ text-shadow: 0 0 8px currentColor;
84
+ }
85
+
86
+ /*
87
+ * MAIN VISUALIZER AREA
88
+ */
89
+ main {
90
+ flex: 1;
91
+ position: relative;
92
+ display: flex;
93
+ justify-content: center;
94
+ align-items: center;
95
+ width: 100%;
96
+ height: 100%;
97
+ }
98
+
99
+ /* The Canvas where ASCII is drawn */
100
+ canvas {
101
+ display: block;
102
+ width: 100%;
103
+ height: 100%;
104
+ }
105
+
106
+ /*
107
+ * CRT EFFECT OVERLAY
108
+ */
109
+ .crt-overlay {
110
+ position: absolute;
111
+ top: 0;
112
+ left: 0;
113
+ width: 100%;
114
+ height: 100%;
115
+ pointer-events: none;
116
+ background: linear-gradient(
117
+ rgba(18, 16, 16, 0) 50%,
118
+ rgba(0, 0, 0, 0.25) 50%
119
+ ), linear-gradient(
120
+ 90deg,
121
+ rgba(255, 0, 0, 0.06),
122
+ rgba(0, 255, 0, 0.02),
123
+ rgba(0, 0, 255, 0.06)
124
+ );
125
+ background-size: 100% 2px, 3px 100%;
126
+ z-index: 5;
127
+ animation: flicker 0.15s infinite;
128
+ }
129
+
130
+ @keyframes flicker {
131
+ 0% { opacity: 0.97; }
132
+ 50% { opacity: 1; }
133
+ 100% { opacity: 0.98; }
134
+ }
135
+
136
+ /*
137
+ * UI CONTROLS
138
+ */
139
+ .controls-container {
140
+ position: absolute;
141
+ bottom: 2rem;
142
+ left: 50%;
143
+ transform: translateX(-50%);
144
+ width: 90%;
145
+ max-width: 800px;
146
+ background: var(--ui-bg);
147
+ border: var(--ui-border);
148
+ border-radius: 12px;
149
+ padding: 1rem;
150
+ display: flex;
151
+ flex-wrap: wrap;
152
+ gap: 1rem;
153
+ justify-content: center;
154
+ align-items: center;
155
+ backdrop-filter: var(--glass-blur);
156
+ z-index: 20;
157
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
158
+ transition: opacity 0.3s, transform 0.3s;
159
+ }
160
+
161
+ /* Hide controls when idle (optional interaction) */
162
+ .controls-container.idle {
163
+ opacity: 0.3;
164
+ }
165
+ .controls-container:hover {
166
+ opacity: 1;
167
+ }
168
+
169
+ .btn {
170
+ background: transparent;
171
+ border: 1px solid var(--text-color);
172
+ color: var(--text-color);
173
+ padding: 0.5rem 1rem;
174
+ font-family: inherit;
175
+ cursor: pointer;
176
+ border-radius: 4px;
177
+ text-transform: uppercase;
178
+ font-size: 0.8rem;
179
+ font-weight: bold;
180
+ transition: all 0.2s;
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 0.5rem;
184
+ }
185
+
186
+ .btn:hover {
187
+ background: var(--text-color);
188
+ color: var(--bg-color);
189
+ box-shadow: 0 0 10px currentColor;
190
+ }
191
+
192
+ .btn.active {
193
+ background: var(--text-color);
194
+ color: var(--bg-color);
195
+ }
196
+
197
+ .file-input-wrapper {
198
+ position: relative;
199
+ overflow: hidden;
200
+ display: inline-block;
201
+ }
202
+
203
+ .file-input-wrapper input[type=file] {
204
+ position: absolute;
205
+ left: 0;
206
+ top: 0;
207
+ opacity: 0;
208
+ width: 100%;
209
+ height: 100%;
210
+ cursor: pointer;
211
+ }
212
+
213
+ select {
214
+ background: var(--bg-color);
215
+ color: var(--text-color);
216
+ border: 1px solid var(--text-color);
217
+ padding: 0.5rem;
218
+ font-family: inherit;
219
+ border-radius: 4px;
220
+ outline: none;
221
+ }
222
+
223
+ /* Color Pickers */
224
+ .theme-dots {
225
+ display: flex;
226
+ gap: 0.5rem;
227
+ }
228
+ .theme-dot {
229
+ width: 20px;
230
+ height: 20px;
231
+ border-radius: 50%;
232
+ cursor: pointer;
233
+ border: 2px solid transparent;
234
+ transition: transform 0.2s;
235
+ }
236
+ .theme-dot:hover { transform: scale(1.2); }
237
+ .theme-dot.selected { border-color: #fff; }
238
+
239
+ /* Drag Overlay */
240
+ .drag-overlay {
241
+ position: absolute;
242
+ top: 0;
243
+ left: 0;
244
+ width: 100%;
245
+ height: 100%;
246
+ background: rgba(0,0,0,0.8);
247
+ display: flex;
248
+ justify-content: center;
249
+ align-items: center;
250
+ z-index: 50;
251
+ opacity: 0;
252
+ pointer-events: none;
253
+ transition: opacity 0.3s;
254
+ }
255
+ .drag-overlay.active {
256
+ opacity: 1;
257
+ pointer-events: all;
258
+ }
259
+ .drag-message {
260
+ border: 2px dashed var(--text-color);
261
+ padding: 2rem;
262
+ font-size: 1.5rem;
263
+ color: var(--text-color);
264
+ }
265
+
266
+ /* Toast Notification */
267
+ .toast {
268
+ position: absolute;
269
+ top: 5rem;
270
+ left: 50%;
271
+ transform: translateX(-50%);
272
+ background: var(--text-color);
273
+ color: var(--bg-color);
274
+ padding: 0.5rem 1rem;
275
+ border-radius: 4px;
276
+ font-weight: bold;
277
+ opacity: 0;
278
+ transition: opacity 0.5s;
279
+ z-index: 30;
280
+ pointer-events: none;
281
+ }
282
+ .toast.show { opacity: 1; }
283
+
284
+ /* Responsive */
285
+ @media (max-width: 600px) {
286
+ .controls-container {
287
+ bottom: 0;
288
+ width: 100%;
289
+ border-radius: 12px 12px 0 0;
290
+ padding-bottom: 1.5rem;
291
+ }
292
+ .btn span { display: none; } /* Hide text on small screens, show icons only */
293
+ .btn span.visible { display: inline; }
294
+ }
295
+ </style>
296
+ </head>
297
+ <body>
298
+
299
+ <!-- Header -->
300
+ <header>
301
+ <div class="brand">ASCII_VISUALIZER_V1</div>
302
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
303
+ Built with anycoder
304
+ </a>
305
+ </header>
306
+
307
+ <!-- Notification Area -->
308
+ <div id="toast" class="toast">Notification</div>
309
+
310
+ <!-- Drag & Drop Zone -->
311
+ <div id="dragOverlay" class="drag-overlay">
312
+ <div class="drag-message">DROP AUDIO FILE HERE</div>
313
+ </div>
314
+
315
+ <!-- Main Canvas -->
316
+ <main>
317
+ <canvas id="asciiCanvas"></canvas>
318
+ <div class="crt-overlay"></div>
319
+ </main>
320
+
321
+ <!-- Controls -->
322
+ <div class="controls-container" id="controls">
323
+ <!-- Playback -->
324
+ <button id="btnPlayPause" class="btn">
325
+ <span id="iconPlay">▶</span> <span class="visible">Play</span>
326
+ </button>
327
+
328
+ <!-- File Input -->
329
+ <div class="file-input-wrapper btn">
330
+ <span>📂</span> <span class="visible">Load File</span>
331
+ <input type="file" id="audioInput" accept="audio/*">
332
+ </div>
333
+
334
+ <!-- Mic Input -->
335
+ <button id="btnMic" class="btn">
336
+ <span>🎤</span> <span class="visible">Mic</span>
337
+ </button>
338
+
339
+ <!-- Visualization Mode -->
340
+ <select id="modeSelect" class="btn">
341
+ <option value="spectrum">Spectrum</option>
342
+ <option value="wave">Waveform</option>
343
+ <option value="circle">Radial</option>
344
+ <option value="matrix">Matrix Rain</option>
345
+ </select>
346
+
347
+ <!-- Theme Colors -->
348
+ <div class="theme-dots">
349
+ <div class="theme-dot selected" style="background: var(--theme-green);" data-color="var(--theme-green)"></div>
350
+ <div class="theme-dot" style="background: var(--theme-amber);" data-color="var(--theme-amber)"></div>
351
+ <div class="theme-dot" style="background: var(--theme-cyan);" data-color="var(--theme-cyan)"></div>
352
+ <div class="theme-dot" style="background: var(--theme-pink);" data-color="var(--theme-pink)"></div>
353
+ <div class="theme-dot" style="background: var(--theme-white);" data-color="var(--theme-white)"></div>
354
+ </div>
355
+ </div>
356
+
357
+ <!-- Hidden Audio Element -->
358
+ <audio id="audioElement" crossorigin="anonymous"></audio>
359
+
360
+ <script>
361
+ /**
362
+ * ASCII MUSIC VISUALIZER
363
+ * Core Logic: Web Audio API + Canvas API
364
+ */
365
+
366
+ // --- Configuration & State ---
367
+ const config = {
368
+ fftSize: 2048, // Resolution of audio analysis
369
+ smoothing: 0.8,
370
+ fontSize: 14,
371
+ fontFamily: '"Courier New", monospace',
372
+ chars: " .:-=+*#%@".split(""), // Density map from low to high energy
373
+ matrixChars: "ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ".split("")
374
+ };
375
+
376
+ const state = {
377
+ isPlaying: false,
378
+ sourceType: null, // 'file' or 'mic'
379
+ mode: 'spectrum',
380
+ audioCtx: null,
381
+ analyser: null,
382
+ source: null,
383
+ dataArray: null,
384
+ animationId: null,
385
+ width: 0,
386
+ height: 0,
387
+ cols: 0,
388
+ rows: 0
389
+ };
390
+
391
+ // --- DOM Elements ---
392
+ const canvas = document.getElementById('asciiCanvas');
393
+ const ctx = canvas.getContext('2d', { alpha: false }); // Optimize for no transparency
394
+ const audioElement = document.getElementById('audioElement');
395
+ const btnPlayPause = document.getElementById('btnPlayPause');
396
+ const iconPlay = document.getElementById('iconPlay');
397
+ const audioInput = document.getElementById('audioInput');
398
+ const btnMic = document.getElementById('btnMic');
399
+ const modeSelect = document.getElementById('modeSelect');
400
+ const dragOverlay = document.getElementById('dragOverlay');
401
+ const toastEl = document.getElementById('toast');
402
+ const themeDots = document.querySelectorAll('.theme-dot');
403
+
404
+ // --- Audio System Initialization ---
405
+
406
+ function initAudioContext() {
407
+ if (!state.audioCtx) {
408
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
409
+ state.audioCtx = new AudioContext();
410
+ }
411
+ if (state.audioCtx.state === 'suspended') {
412
+ state.audioCtx.resume();
413
+ }
414
+ }
415
+
416
+ function setupAnalyser() {
417
+ state.analyser = state.audioCtx.createAnalyser();
418
+ state.analyser.fftSize = config.fftSize;
419
+ state.analyser.smoothingTimeConstant = config.smoothing;
420
+ const bufferLength = state.analyser.frequencyBinCount;
421
+ state.dataArray = new Uint8Array(bufferLength);
422
+ }
423
+
424
+ function loadAudioFile(file) {
425
+ initAudioContext();
426
+
427
+ // Cleanup previous source
428
+ if (state.source) {
429
+ state.source.disconnect();
430
+ }
431
+ if (state.sourceType === 'mic') {
432
+ // If switching from Mic, we handle logic differently
433
+ stopMic();
434
+ }
435
+
436
+ const objectUrl = URL.createObjectURL(file);
437
+ audioElement.src = objectUrl;
438
+
439
+ setupAnalyser();
440
+ state.source = state.audioCtx.createMediaElementSource(audioElement);
441
+ state.source.connect(state.analyser);
442
+ state.analyser.connect(state.audioCtx.destination);
443
+
444
+ state.sourceType = 'file';
445
+
446
+ audioElement.play()
447
+ .then(() => {
448
+ state.isPlaying = true;
449
+ updatePlayButton();
450
+ startVisualization();
451
+ showToast(`Playing: ${file.name}`);
452
+ })
453
+ .catch(err => {
454
+ console.error("Playback error:", err);
455
+ showToast("Error playing file");
456
+ });
457
+ }
458
+
459
+ async function enableMicrophone() {
460
+ initAudioContext();
461
+
462
+ try {
463
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
464
+
465
+ // Stop file playback if active
466
+ audioElement.pause();
467
+ if (state.source) state.source.disconnect();
468
+
469
+ setupAnalyser();
470
+ state.source = state.audioCtx.createMediaStreamSource(stream);
471
+ state.source.connect(state.analyser);
472
+ // Do NOT connect mic to destination (speakers) to avoid feedback loop
473
+
474
+ state.sourceType = 'mic';
475
+ state.isPlaying = true;
476
+ updatePlayButton();
477
+ startVisualization();
478
+ showToast("Microphone Active");
479
+ } catch (err) {
480
+ console.error("Mic access denied:", err);
481
+ showToast("Microphone access denied");
482
+ }
483
+ }
484
+
485
+ function stopMic() {
486
+ if (state.sourceType === 'mic' && state.source) {
487
+ state.source.mediaStream.getTracks().forEach(track => track.stop());
488
+ state.source.disconnect();
489
+ state.source = null;
490
+ }
491
+ }
492
+
493
+ // --- Visualization Engine ---
494
+
495
+ function resize() {
496
+ state.width = window.innerWidth;
497
+ state.height = window.innerHeight;
498
+ canvas.width = state.width;
499
+ canvas.height = state.height;
500
+
501
+ // Calculate grid dimensions
502
+ ctx.font = `${config.fontSize}px ${config.fontFamily}`;
503
+ const charWidth = ctx.measureText("M").width;
504
+ state.cols = Math.floor(state.width / charWidth);
505
+ state.rows = Math.floor(state.height / config.fontSize);
506
+ }
507
+
508
+ function getChar(value, max) {
509
+ // Map value (0-255) to character index
510
+ const index = Math.floor((value / 255) * (config.chars.length - 1));
511
+ return config.chars[index];
512
+ }
513
+
514
+ function drawSpectrum() {
515
+ state.analyser.getByteFrequencyData(state.dataArray);
516
+
517
+ // Clear screen with trail effect
518
+ ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--bg-color');
519
+ ctx.fillRect(0, 0, state.width, state.height);
520
+
521
+ ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--text-color');
522
+ ctx.textBaseline = 'middle';
523
+
524
+ const barWidth = state.cols / (state.dataArray.length / 4); // Use lower freqs mostly
525
+ const centerY = state.height / 2;
526
+
527
+ for (let i = 0; i < state.cols; i++) {
528
+ // Map column to data index
529
+ const dataIndex = Math.floor(i * (state.dataArray.length / 4) / state.cols);
530
+ const value = state.dataArray[dataIndex];
531
+
532
+ // Calculate height of bar
533
+ const barHeight = (value / 255) * (state.rows / 2);
534
+
535
+ // Draw mirrored bars
536
+ const char = getChar(value, 255);
537
+ const charWidth = ctx.measureText(char).width;
538
+ const x = i * (state.width / state.cols);
539
+
540
+ // Draw Top Half
541
+ for (let j = 0; j < barHeight; j++) {
542
+ const y = centerY - (j * config.fontSize);
543
+ ctx.fillText(char, x, y);
544
+ }
545
+
546
+ // Draw Bottom Half
547
+ for (let j = 0; j < barHeight; j++) {
548
+ const y = centerY + (j * config.fontSize);
549
+ ctx.fillText(char, x, y);
550
+ }
551
+ }
552
+ }
553
+
554
+ function drawWaveform() {
555
+ state.analyser.getByteTimeDomainData(state.dataArray);
556
+
557
+ ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--bg-color');
558
+ ctx.fillRect(0, 0, state.width, state.height);
559
+
560
+ ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--text-color');
561
+ ctx.textBaseline = 'middle';
562
+
563
+ const sliceWidth = state.width / state.dataArray.length;
564
+ let x = 0;
565
+
566
+ for(let i = 0; i < state.dataArray.length; i++) {
567
+ const v = state.dataArray[i] / 128.0; // 0..2
568
+ const y = (v * state.height) / 2;
569
+
570
+ const char = getChar(Math.abs(state.dataArray[i] - 128) * 2, 255);
571
+
572
+ // Optimization: only draw if there is significant change or every few pixels
573
+ // To make it look like a line, we draw vertical strips of characters
574
+ if (i % 2 === 0) { // Downsample slightly
575
+ ctx.fillText(char, x, y);
576
+ }
577
+
578
+ x += sliceWidth;
579
+ }
580
+ }
581
+
582
+ function drawRadial() {
583
+ state.analyser.getByteFrequencyData(state.dataArray);
584
+
585
+ // Fade out effect
586
+ ctx.fillStyle = 'rgba(5, 5, 5, 0.2)';
587
+ ctx.fillRect(0, 0, state.width, state.height);
588
+
589
+ ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--text-color');
590
+ ctx.textAlign = 'center';
591
+ ctx.textBaseline = 'middle';
592
+
593
+ const cx = state.width / 2;
594
+ const cy = state.height / 2;
595
+ const radius = Math.min(state.width, state.height) / 4;
596
+ const maxBars = 180; // Limit number of bars for clarity
597
+
598
+ const step = Math.floor(state.dataArray.length / maxBars);
599
+
600
+ for (let i = 0; i < maxBars; i++) {
601
+ const value = state.dataArray[i * step];
602
+ const barHeight = (value / 255) * radius;
603
+ const angle = (i / maxBars) * Math.PI * 2;
604
+
605
+ const char = getChar(value, 255);
606
+
607
+ // Calculate position
608
+ const x1 = cx + Math.cos(angle) * radius;
609
+ const y1 = cy + Math.sin(angle) * radius;
610
+ const x2 = cx + Math.cos(angle) * (radius + barHeight);
611
+ const y2 = cy + Math.sin(angle) * (radius + barHeight);
612
+
613
+ // Draw character at the tip
614
+ ctx.fillText(char, x2, y2);
615
+
616
+ // Optional: Draw connecting line or base
617
+ if (value > 100) {
618
+ ctx.fillRect(x1, y1, 2, 2);
619
+ }
620
+ }
621
+ }
622
+
623
+ // Matrix Rain Effect (Custom Logic)
624
+ // We maintain a drops array
625
+ const matrixDrops = [];
626
+
627
+ function initMatrix() {
628
+ matrixDrops.length = 0;
629
+ for (let i = 0; i < state.cols; i++) {
630
+ matrixDrops[i] = Math.random() * state.rows; // Random start Y
631
+ }
632
+ }
633
+
634
+ function drawMatrix() {
635
+ // Get audio data to influence color/speed or character density
636
+ state.analyser.getByteFrequencyData(state.dataArray);
637
+ const bass = state.dataArray[10]; // Low freq average proxy
638
+
639
+ ctx.fillStyle = 'rgba(5, 5, 5, 0.1)'; // Very slow fade
640
+ ctx.fillRect(0, 0, state.width, state.height);
641
+
642
+ ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--text-color');
643
+
644
+ const charWidth = state.width / state.cols;
645
+
646
+ for (let i = 0; i < matrixDrops.length; i++) {
647
+ // Pick random char
648
+ const text = config.matrixChars[Math.floor(Math.random() * config.matrixChars.length)];
649
+
650
+ const x = i * charWidth;
651
+ const y = matrixDrops[i] * config.fontSize;
652
+
653
+ // Draw
654
+ ctx.fillText(text, x, y);
655
+
656
+ // Reset or move drop
657
+ if (y > state.height && Math.random() > 0.975) {
658
+ matrixDrops[i] = 0;
659
+ }
660
+
661
+ // Speed up drops based on bass
662
+ const speed = 0.5 + (bass / 255);
663
+ matrixDrops[i] += speed;
664
+ }
665
+ }
666
+
667
+ function animate() {
668
+ if (!state.isPlaying) return;
669
+
670
+ switch (state.mode) {
671
+ case 'spectrum':
672
+ drawSpectrum();
673
+ break;
674
+ case 'wave':
675
+ drawWaveform();
676
+ break;
677
+ case 'circle':
678
+ drawRadial();
679
+ break;
680
+ case 'matrix':
681
+ drawMatrix();
682
+ break;
683
+ }
684
+
685
+ state.animationId = requestAnimationFrame(animate);
686
+ }
687
+
688
+ function startVisualization() {
689
+ if (state.animationId) cancelAnimationFrame(state.animationId);
690
+
691
+ if (state.mode === 'matrix' && matrixDrops.length === 0) {
692
+ initMatrix();
693
+ }
694
+
695
+ animate();
696
+ }
697
+
698
+ function stopVisualization() {
699
+ if (state.animationId) cancelAnimationFrame(state.animationId);
700
+ // Clear canvas
701
+ ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--bg-color');
702
+ ctx.fillRect(0, 0, state.width, state.height);
703
+ }
704
+
705
+ // --- Event Listeners ---
706
+
707
+ window.addEventListener('resize', () => {
708
+ resize();
709
+ if (state.mode === 'matrix') initMatrix();
710
+ });
711
+
712
+ // File Upload
713
+ audioInput.addEventListener('change', (e) => {
714
+ if (e.target.files.length > 0) {
715
+ loadAudioFile(e.target.files[0]);
716
+ }
717
+ });
718
+
719
+ // Play/Pause Button
720
+ btnPlayPause.addEventListener('click', () => {
721
+ if (!state.sourceType) return; // Nothing loaded
722
+
723
+ if (state.sourceType === 'file') {
724
+ if (state.isPlaying) {
725
+ audioElement.pause();
726
+ state.isPlaying = false;
727
+ } else {
728
+ initAudioContext();
729
+ audioElement.play();
730
+ state.isPlaying = true;
731
+ startVisualization();
732
+ }
733
+ } else if (state.sourceType === 'mic') {
734
+ // Mic is always "playing" when active, this button could mute/unmute logic
735
+ // For now, let's just stop the mic
736
+ stopMic();
737
+ state.isPlaying = false;
738
+ showToast("Microphone Stopped");
739
+ }
740
+ updatePlayButton();
741
+ });
742
+
743
+ // Mic Button
744
+ btnMic.addEventListener('click', enableMicrophone);
745
+
746
+ // Mode Select
747
+ modeSelect.addEventListener('change', (e) => {
748
+ state.mode = e.target.value;
749
+ if (state.mode === 'matrix') initMatrix();
750
+ if (state.isPlaying) startVisualization();
751
+ });
752
+
753
+ // Theme Switcher
754
+ themeDots.forEach(dot => {
755
+ dot.addEventListener('click', () => {
756
+ // Update UI selection
757
+ themeDots.forEach(d => d.classList.remove('selected'));
758
+ dot.classList.add('selected');
759
+
760
+ // Update CSS Variables
761
+ const color = dot.getAttribute('data-color');
762
+ document.documentElement.style.setProperty('--text-color', color);
763
+
764
+ // Update CRT overlay border to match slightly (optional aesthetic)
765
+ // document.documentElement.style.setProperty('--ui-border', `1px solid ${color}`);
766
+ });
767
+ });
768
+
769
+ // Drag and Drop
770
+ window.addEventListener('dragover', (e) => {
771
+ e.preventDefault();
772
+ dragOverlay.classList.add('active');
773
+ });
774
+
775
+ dragOverlay.addEventListener('dragleave', (e) => {
776
+ e.preventDefault();
777
+ dragOverlay.classList.remove('active');
778
+ });
779
+
780
+ dragOverlay.addEventListener('drop', (e) => {
781
+ e.preventDefault();
782
+ dragOverlay.classList.remove('active');
783
+
784
+ if (e.dataTransfer.files.length > 0) {
785
+ const file = e.dataTransfer.files[0];
786
+ if (file.type.startsWith('audio/')) {
787
+ loadAudioFile(file);
788
+ } else {
789
+ showToast("Please drop an audio file");
790
+ }
791
+ }
792
+ });
793
+
794
+ // --- Helpers ---
795
+
796
+ function updatePlayButton() {
797
+ if (state.isPlaying) {
798
+ iconPlay.textContent = '⏸';
799
+ btnPlayPause.querySelector('.visible').textContent = 'Pause';
800
+ } else {
801
+ iconPlay.textContent = '▶';
802
+ btnPlayPause.querySelector('.visible').textContent = 'Play';
803
+ }
804
+ }
805
+
806
+ function showToast(msg) {
807
+ toastEl.textContent = msg;
808
+ toastEl.classList.add('show');
809
+ setTimeout(() => {
810
+ toastEl.classList.remove('show');
811
+ }, 3000);
812
+ }
813
+
814
+ // --- Init ---
815
+ resize();
816
+ ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--bg-color');
817
+ ctx.fillRect(0, 0, state.width, state.height);
818
+
819
+ // Initial welcome text
820
+ ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--text-color');
821
+ ctx.textAlign = 'center';
822
+ ctx.font = "20px monospace";
823
+ ctx.fillText("LOAD AUDIO FILE OR ENABLE MIC TO START", state.width/2, state.height/2);
824
+
825
+ </script>
826
+ </body>
827
+ </html>