trysem commited on
Commit
d4118d0
·
verified ·
1 Parent(s): b322a42

Create OG-BEST-COPY

Browse files
Files changed (1) hide show
  1. OG-BEST-COPY +851 -0
OG-BEST-COPY ADDED
@@ -0,0 +1,851 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { Upload, Play, Pause, Image as ImageIcon, Video, Settings2, Loader2, StopCircle, Sparkles, Monitor, ImagePlus } from 'lucide-react';
3
+
4
+ export default function App() {
5
+ const canvasRef = useRef(null);
6
+ const audioRef = useRef(null);
7
+ const audioCtxRef = useRef(null);
8
+ const analyserRef = useRef(null);
9
+ const sourceRef = useRef(null);
10
+ const destRef = useRef(null);
11
+ const reqIdRef = useRef(null);
12
+ const mediaRecorderRef = useRef(null);
13
+ const chunksRef = useRef([]);
14
+ const bgImgRef = useRef(null);
15
+
16
+ // State
17
+ const [audioSrc, setAudioSrc] = useState(null);
18
+ const [fileName, setFileName] = useState('');
19
+ const [isPlaying, setIsPlaying] = useState(false);
20
+ const [isExportingVideo, setIsExportingVideo] = useState(false);
21
+ const [exportProgress, setExportProgress] = useState(0);
22
+
23
+ // Settings
24
+ const [vizType, setVizType] = useState('bars'); // 'bars', 'wave', 'circle'
25
+ const [color, setColor] = useState('#00ffcc');
26
+ const [thickness, setThickness] = useState(12);
27
+ const [spacing, setSpacing] = useState(8); // NEW
28
+ const [sensitivity, setSensitivity] = useState(1.5); // Changed to Amplitude Multiplier
29
+ const [smoothing, setSmoothing] = useState(0.85); // NEW
30
+
31
+ // Transform Settings (NEW)
32
+ const [offsetX, setOffsetX] = useState(0);
33
+ const [offsetY, setOffsetY] = useState(0);
34
+ const [scale, setScale] = useState(1.0);
35
+ const [rotation, setRotation] = useState(0);
36
+
37
+ // Advanced Settings
38
+ const [colorMode, setColorMode] = useState('solid');
39
+ const [color2, setColor2] = useState('#b829ff');
40
+ const [glow, setGlow] = useState(false);
41
+ const [resolution, setResolution] = useState('4k_16_9');
42
+ const [bgType, setBgType] = useState('transparent');
43
+ const [bgColor, setBgColor] = useState('#0f172a');
44
+ const [bgImageSrc, setBgImageSrc] = useState(null);
45
+
46
+ const RESOLUTIONS = {
47
+ '4k_16_9': { w: 3840, h: 2160, label: '4K (16:9)', isVertical: false },
48
+ '1080p_16_9': { w: 1920, h: 1080, label: '1080p (16:9)', isVertical: false },
49
+ '4k_9_16': { w: 2160, h: 3840, label: '4K Vertical (9:16)', isVertical: true },
50
+ '1080p_9_16': { w: 1080, h: 1920, label: '1080p Vertical (9:16)', isVertical: true }
51
+ };
52
+
53
+ // Load background image
54
+ useEffect(() => {
55
+ if (bgImageSrc) {
56
+ const img = new Image();
57
+ img.onload = () => { bgImgRef.current = img; };
58
+ img.src = bgImageSrc;
59
+ } else {
60
+ bgImgRef.current = null;
61
+ }
62
+ }, [bgImageSrc]);
63
+
64
+ const handleBgUpload = (e) => {
65
+ const file = e.target.files[0];
66
+ if (file) {
67
+ if (bgImageSrc) URL.revokeObjectURL(bgImageSrc);
68
+ setBgImageSrc(URL.createObjectURL(file));
69
+ setBgType('image');
70
+ }
71
+ };
72
+
73
+ // Initialize Web Audio API
74
+ const initAudio = useCallback(() => {
75
+ if (!audioCtxRef.current) {
76
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
77
+ audioCtxRef.current = new AudioContext();
78
+ analyserRef.current = audioCtxRef.current.createAnalyser();
79
+ destRef.current = audioCtxRef.current.createMediaStreamDestination();
80
+
81
+ // We only want to create the source once for the audio element
82
+ if (!sourceRef.current && audioRef.current) {
83
+ sourceRef.current = audioCtxRef.current.createMediaElementSource(audioRef.current);
84
+ sourceRef.current.connect(analyserRef.current);
85
+ analyserRef.current.connect(audioCtxRef.current.destination); // to speakers
86
+ analyserRef.current.connect(destRef.current); // to recording destination
87
+ }
88
+ }
89
+
90
+ // Resume context if suspended (browser autoplay policy)
91
+ if (audioCtxRef.current.state === 'suspended') {
92
+ audioCtxRef.current.resume();
93
+ }
94
+ }, []);
95
+
96
+ const handleFileUpload = (e) => {
97
+ const file = e.target.files[0];
98
+ if (file) {
99
+ if (audioSrc) URL.revokeObjectURL(audioSrc);
100
+ const url = URL.createObjectURL(file);
101
+ setAudioSrc(url);
102
+ setFileName(file.name);
103
+ setIsPlaying(false);
104
+ if (audioRef.current) {
105
+ audioRef.current.pause();
106
+ audioRef.current.currentTime = 0;
107
+ }
108
+ }
109
+ };
110
+
111
+ const togglePlay = () => {
112
+ if (!audioSrc) return;
113
+ initAudio();
114
+
115
+ if (isPlaying) {
116
+ audioRef.current.pause();
117
+ } else {
118
+ audioRef.current.play();
119
+ }
120
+ setIsPlaying(!isPlaying);
121
+ };
122
+
123
+ // The Animation Loop
124
+ const draw = useCallback(() => {
125
+ if (!canvasRef.current || !analyserRef.current) {
126
+ reqIdRef.current = requestAnimationFrame(draw);
127
+ return;
128
+ }
129
+
130
+ const canvas = canvasRef.current;
131
+ const ctx = canvas.getContext('2d');
132
+ const res = RESOLUTIONS[resolution] || RESOLUTIONS['4k_16_9'];
133
+
134
+ // Update canvas native resolution if changed
135
+ if (canvas.width !== res.w || canvas.height !== res.h) {
136
+ canvas.width = res.w;
137
+ canvas.height = res.h;
138
+ }
139
+
140
+ const width = canvas.width;
141
+ const height = canvas.height;
142
+
143
+ // Background handling
144
+ ctx.clearRect(0, 0, width, height);
145
+ if (bgType === 'color') {
146
+ ctx.fillStyle = bgColor;
147
+ ctx.fillRect(0, 0, width, height);
148
+ } else if (bgType === 'image' && bgImgRef.current) {
149
+ const img = bgImgRef.current;
150
+ const imgRatio = img.width / img.height;
151
+ const canvasRatio = width / height;
152
+ let drawW, drawH, drawX, drawY;
153
+ if (imgRatio > canvasRatio) {
154
+ drawH = height;
155
+ drawW = height * imgRatio;
156
+ drawX = (width - drawW) / 2;
157
+ drawY = 0;
158
+ } else {
159
+ drawW = width;
160
+ drawH = width / imgRatio;
161
+ drawX = 0;
162
+ drawY = (height - drawH) / 2;
163
+ }
164
+ ctx.drawImage(img, drawX, drawY, drawW, drawH);
165
+ }
166
+
167
+ // Dynamic smoothing and fixed FFT for better reactivity control
168
+ analyserRef.current.smoothingTimeConstant = smoothing;
169
+ analyserRef.current.fftSize = 2048;
170
+
171
+ const bufferLength = analyserRef.current.frequencyBinCount;
172
+ const dataArray = new Uint8Array(bufferLength);
173
+
174
+ ctx.save(); // Save context before applying transforms
175
+
176
+ // Apply Transformations (Position, Scale, Rotation)
177
+ const centerX = width / 2 + (width * (offsetX / 100));
178
+ const centerY = height / 2 + (height * (offsetY / 100));
179
+ ctx.translate(centerX, centerY);
180
+ ctx.scale(scale, scale);
181
+ ctx.rotate((rotation * Math.PI) / 180);
182
+
183
+ // Color and Glow logic (Coordinates adapted to centered origin)
184
+ let activeColor = color;
185
+ if (colorMode === 'gradient') {
186
+ const grad = ctx.createLinearGradient(-width/2, -height/2, width/2, height/2);
187
+ grad.addColorStop(0, color);
188
+ grad.addColorStop(1, color2);
189
+ activeColor = grad;
190
+ } else if (colorMode === 'rainbow') {
191
+ const grad = ctx.createLinearGradient(-width/2, 0, width/2, 0);
192
+ grad.addColorStop(0, '#ff0000');
193
+ grad.addColorStop(0.16, '#ffff00');
194
+ grad.addColorStop(0.33, '#00ff00');
195
+ grad.addColorStop(0.5, '#00ffff');
196
+ grad.addColorStop(0.66, '#0000ff');
197
+ grad.addColorStop(0.83, '#ff00ff');
198
+ grad.addColorStop(1, '#ff0000');
199
+ activeColor = grad;
200
+ }
201
+
202
+ ctx.lineWidth = thickness;
203
+ ctx.strokeStyle = activeColor;
204
+ ctx.fillStyle = activeColor;
205
+ ctx.lineCap = 'round';
206
+ ctx.lineJoin = 'round';
207
+
208
+ if (glow) {
209
+ ctx.shadowBlur = thickness * 2;
210
+ ctx.shadowColor = colorMode === 'solid' ? color : (colorMode === 'gradient' ? color2 : '#ffffff');
211
+ } else {
212
+ ctx.shadowBlur = 0;
213
+ }
214
+
215
+ if (vizType === 'bars') {
216
+ analyserRef.current.getByteFrequencyData(dataArray);
217
+
218
+ const step = thickness + spacing;
219
+ // Calculate exactly how many bars can fit in half the screen
220
+ const maxBars = Math.floor((width / 2) / step);
221
+ const usefulLength = Math.floor(bufferLength * 0.75); // Skip extreme silent highs
222
+ const numBars = Math.min(maxBars, usefulLength);
223
+
224
+ for (let i = 0; i < numBars; i++) {
225
+ const dataIndex = Math.floor((i / numBars) * usefulLength);
226
+
227
+ // Progressively boost higher frequencies
228
+ const boost = Math.pow(1 + (i / numBars), 1.5);
229
+
230
+ const value = dataArray[dataIndex] * boost * sensitivity;
231
+
232
+ // Minimum height ensures a nice dot/line is drawn even at complete silence
233
+ const barHeight = Math.max(thickness / 2, (value / 255) * height * 0.8);
234
+ const xOffset = i * step + (step / 2);
235
+
236
+ // Draw Right Side (Centered at 0,0 where bottom is height/2)
237
+ ctx.beginPath();
238
+ ctx.moveTo(xOffset, height / 2 - (thickness / 2));
239
+ ctx.lineTo(xOffset, height / 2 - barHeight);
240
+ ctx.stroke();
241
+
242
+ // Draw Left Side (Mirrored)
243
+ ctx.beginPath();
244
+ ctx.moveTo(-xOffset, height / 2 - (thickness / 2));
245
+ ctx.lineTo(-xOffset, height / 2 - barHeight);
246
+ ctx.stroke();
247
+ }
248
+ } else if (vizType === 'wave') {
249
+ analyserRef.current.getByteTimeDomainData(dataArray);
250
+
251
+ ctx.beginPath();
252
+ const sliceWidth = width / bufferLength;
253
+ let x = -width / 2; // Start from left edge relative to center
254
+
255
+ for (let i = 0; i < bufferLength; i++) {
256
+ // Apply sensitivity to the wave
257
+ const normalized = (dataArray[i] / 128.0) - 1;
258
+ const y = normalized * sensitivity * (height / 2); // Centered on Y axis
259
+
260
+ if (i === 0) {
261
+ ctx.moveTo(x, y);
262
+ } else {
263
+ ctx.lineTo(x, y);
264
+ }
265
+ x += sliceWidth;
266
+ }
267
+ ctx.stroke();
268
+ } else if (vizType === 'circle') {
269
+ analyserRef.current.getByteFrequencyData(dataArray);
270
+
271
+ const radius = height / 4;
272
+
273
+ ctx.beginPath();
274
+ // Calculate number of bars based on thickness and spacing
275
+ const circumference = 2 * Math.PI * radius;
276
+ const stepSize = thickness + spacing;
277
+ const bars = Math.min(180, Math.floor(circumference / stepSize));
278
+ const step = (Math.PI * 2) / bars;
279
+
280
+ for (let i = 0; i < bars; i++) {
281
+ const dataIndex = Math.floor((i / bars) * (bufferLength / 2));
282
+
283
+ const value = (dataArray[dataIndex] / 255) * sensitivity;
284
+
285
+ const barHeight = Math.max(thickness / 2, value * (height / 3));
286
+ const angle = i * step;
287
+
288
+ const x1 = Math.cos(angle) * radius;
289
+ const y1 = Math.sin(angle) * radius;
290
+ const x2 = Math.cos(angle) * (radius + barHeight);
291
+ const y2 = Math.sin(angle) * (radius + barHeight);
292
+
293
+ ctx.moveTo(x1, y1);
294
+ ctx.lineTo(x2, y2);
295
+ }
296
+ ctx.stroke();
297
+
298
+ // Inner solid circle
299
+ ctx.beginPath();
300
+ ctx.arc(0, 0, radius - thickness, 0, Math.PI * 2);
301
+ ctx.lineWidth = thickness / 2;
302
+ ctx.stroke();
303
+ }
304
+
305
+ ctx.restore(); // Restore context to original state for next frame
306
+ reqIdRef.current = requestAnimationFrame(draw);
307
+ }, [vizType, color, thickness, spacing, sensitivity, smoothing, colorMode, color2, glow, resolution, bgType, bgColor, offsetX, offsetY, scale, rotation]);
308
+
309
+ // Handle Play/Pause side effects and loop
310
+ useEffect(() => {
311
+ reqIdRef.current = requestAnimationFrame(draw);
312
+ return () => cancelAnimationFrame(reqIdRef.current);
313
+ }, [draw]);
314
+
315
+ const handleAudioEnded = () => {
316
+ setIsPlaying(false);
317
+ if (isExportingVideo) {
318
+ stopVideoExport();
319
+ }
320
+ };
321
+
322
+ // Export Image (PNG)
323
+ const exportImage = () => {
324
+ if (!canvasRef.current) return;
325
+ const link = document.createElement('a');
326
+ link.download = `visualizer_${Date.now()}.png`;
327
+ link.href = canvasRef.current.toDataURL('image/png');
328
+ link.click();
329
+ };
330
+
331
+ // Export 4K Transparent Video
332
+ const startVideoExport = async () => {
333
+ if (!audioSrc || !canvasRef.current || !audioCtxRef.current) {
334
+ alert("Please upload an audio file and press play at least once to initialize.");
335
+ return;
336
+ }
337
+
338
+ setIsExportingVideo(true);
339
+ setExportProgress(0);
340
+ chunksRef.current = [];
341
+
342
+ // Reset audio to start
343
+ audioRef.current.pause();
344
+ audioRef.current.currentTime = 0;
345
+
346
+ // Capture Canvas Stream at 60fps
347
+ const canvasStream = canvasRef.current.captureStream(60);
348
+
349
+ // Get Audio Stream from destination
350
+ const audioStream = destRef.current.stream;
351
+
352
+ // Combine Streams
353
+ const combinedTracks = [...canvasStream.getTracks(), ...audioStream.getAudioTracks()];
354
+ const combinedStream = new MediaStream(combinedTracks);
355
+
356
+ // Setup MediaRecorder for Transparent Video (WebM VP9/VP8)
357
+ let options = { mimeType: 'video/webm; codecs=vp9' };
358
+ if (!MediaRecorder.isTypeSupported(options.mimeType)) {
359
+ options = { mimeType: 'video/webm; codecs=vp8' }; // Fallback
360
+ }
361
+
362
+ if (!MediaRecorder.isTypeSupported(options.mimeType)) {
363
+ alert("Your browser does not support transparent video export (WebM with VP8/VP9 codecs). Export will proceed but may not be transparent.");
364
+ options = {}; // Use browser default
365
+ }
366
+
367
+ try {
368
+ mediaRecorderRef.current = new MediaRecorder(combinedStream, options);
369
+ } catch (e) {
370
+ console.error(e);
371
+ alert("Error starting video recorder. See console.");
372
+ setIsExportingVideo(false);
373
+ return;
374
+ }
375
+
376
+ mediaRecorderRef.current.ondataavailable = (e) => {
377
+ if (e.data && e.data.size > 0) {
378
+ chunksRef.current.push(e.data);
379
+ }
380
+ };
381
+
382
+ mediaRecorderRef.current.onstop = () => {
383
+ const blob = new Blob(chunksRef.current, { type: mediaRecorderRef.current.mimeType });
384
+ const url = URL.createObjectURL(blob);
385
+ const link = document.createElement('a');
386
+ link.download = `visualizer_4k_${Date.now()}.webm`;
387
+ link.href = url;
388
+ link.click();
389
+ URL.revokeObjectURL(url);
390
+ setIsExportingVideo(false);
391
+ setExportProgress(0);
392
+ };
393
+
394
+ // Progress timer
395
+ const duration = audioRef.current.duration;
396
+ const progressInterval = setInterval(() => {
397
+ if (audioRef.current && !audioRef.current.paused) {
398
+ setExportProgress((audioRef.current.currentTime / duration) * 100);
399
+ } else {
400
+ clearInterval(progressInterval);
401
+ }
402
+ }, 500);
403
+
404
+ // Start Recording & Playback
405
+ mediaRecorderRef.current.start(100); // collect data every 100ms
406
+ await audioRef.current.play();
407
+ setIsPlaying(true);
408
+ };
409
+
410
+ const stopVideoExport = () => {
411
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
412
+ mediaRecorderRef.current.stop();
413
+ }
414
+ audioRef.current.pause();
415
+ setIsPlaying(false);
416
+ };
417
+
418
+ return (
419
+ <div className="min-h-screen bg-slate-950 text-slate-200 font-sans selection:bg-cyan-500/30">
420
+ {/* Header */}
421
+ <header className="border-b border-slate-800 bg-slate-900/50 p-6 flex items-center justify-between">
422
+ <div className="flex items-center gap-3">
423
+ <div className="bg-cyan-500/20 p-2 rounded-lg">
424
+ <Video className="w-6 h-6 text-cyan-400" />
425
+ </div>
426
+ <h1 className="text-xl font-bold tracking-tight text-white">4K Transparent Visualizer</h1>
427
+ </div>
428
+ <div className="text-sm text-slate-400 hidden sm:block">
429
+ All processing is strictly local.
430
+ </div>
431
+ </header>
432
+
433
+ <main className="container mx-auto p-6 grid lg:grid-cols-12 gap-8">
434
+
435
+ {/* Left Column: Controls */}
436
+ <div className="lg:col-span-4 space-y-6">
437
+
438
+ {/* Upload Section */}
439
+ <section className="bg-slate-900 p-6 rounded-2xl border border-slate-800 shadow-xl">
440
+ <h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-4 flex items-center gap-2">
441
+ <Upload className="w-4 h-4" /> Audio Input
442
+ </h2>
443
+ <label className="block w-full cursor-pointer bg-slate-800 hover:bg-slate-700 transition-colors border-2 border-dashed border-slate-600 rounded-xl p-8 text-center group">
444
+ <input
445
+ type="file"
446
+ accept="audio/*"
447
+ onChange={handleFileUpload}
448
+ className="hidden"
449
+ disabled={isExportingVideo}
450
+ />
451
+ <div className="mx-auto w-12 h-12 bg-slate-900 rounded-full flex items-center justify-center mb-3 group-hover:scale-110 transition-transform">
452
+ <Upload className="w-6 h-6 text-cyan-400" />
453
+ </div>
454
+ <p className="font-medium text-slate-300">
455
+ {fileName ? fileName : 'Click to browse audio file'}
456
+ </p>
457
+ <p className="text-xs text-slate-500 mt-2">MP3, WAV, FLAC</p>
458
+ </label>
459
+
460
+ {/* Hidden Audio Element */}
461
+ <audio
462
+ ref={audioRef}
463
+ src={audioSrc}
464
+ onEnded={handleAudioEnded}
465
+ onPlay={() => setIsPlaying(true)}
466
+ onPause={() => setIsPlaying(false)}
467
+ />
468
+
469
+ {/* Playback Controls */}
470
+ {audioSrc && (
471
+ <div className="mt-4 flex gap-3">
472
+ <button
473
+ onClick={togglePlay}
474
+ disabled={isExportingVideo}
475
+ className="flex-1 bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-bold py-3 px-4 rounded-xl flex items-center justify-center gap-2 transition-colors disabled:opacity-50"
476
+ >
477
+ {isPlaying ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5" />}
478
+ {isPlaying ? 'Pause' : 'Play Audio'}
479
+ </button>
480
+ </div>
481
+ )}
482
+ </section>
483
+
484
+ {/* Settings Section */}
485
+ <section className="bg-slate-900 p-6 rounded-2xl border border-slate-800 shadow-xl">
486
+ <h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-4 flex items-center gap-2">
487
+ <Settings2 className="w-4 h-4" /> Visual Settings
488
+ </h2>
489
+
490
+ <div className="space-y-5">
491
+ {/* Type */}
492
+ <div>
493
+ <label className="block text-sm font-medium text-slate-400 mb-2">Style</label>
494
+ <div className="grid grid-cols-3 gap-2">
495
+ {['bars', 'wave', 'circle'].map(type => (
496
+ <button
497
+ key={type}
498
+ onClick={() => setVizType(type)}
499
+ className={`py-2 px-3 rounded-lg text-sm font-medium capitalize transition-all ${
500
+ vizType === type
501
+ ? 'bg-slate-700 text-white shadow-inner border border-slate-600'
502
+ : 'bg-slate-950 text-slate-400 border border-slate-800 hover:border-slate-600'
503
+ }`}
504
+ >
505
+ {type}
506
+ </button>
507
+ ))}
508
+ </div>
509
+ </div>
510
+
511
+ {/* Color & Style */}
512
+ <div>
513
+ <div className="flex justify-between items-center mb-2">
514
+ <label className="text-sm font-medium text-slate-400">Color Style</label>
515
+ <select
516
+ value={colorMode}
517
+ onChange={(e) => setColorMode(e.target.value)}
518
+ className="bg-slate-950 border border-slate-700 text-slate-300 text-xs rounded px-2 py-1 outline-none"
519
+ >
520
+ <option value="solid">Solid</option>
521
+ <option value="gradient">Gradient</option>
522
+ <option value="rainbow">Rainbow</option>
523
+ </select>
524
+ </div>
525
+
526
+ <div className="flex items-center gap-3">
527
+ {colorMode !== 'rainbow' && (
528
+ <input
529
+ type="color"
530
+ value={color}
531
+ onChange={(e) => setColor(e.target.value)}
532
+ className="h-10 w-14 rounded cursor-pointer bg-slate-950 border border-slate-700 shrink-0"
533
+ />
534
+ )}
535
+ {colorMode === 'gradient' && (
536
+ <>
537
+ <span className="text-slate-500 text-xs font-medium">to</span>
538
+ <input
539
+ type="color"
540
+ value={color2}
541
+ onChange={(e) => setColor2(e.target.value)}
542
+ className="h-10 w-14 rounded cursor-pointer bg-slate-950 border border-slate-700 shrink-0"
543
+ />
544
+ </>
545
+ )}
546
+ {colorMode === 'solid' && (
547
+ <input
548
+ type="text"
549
+ value={color}
550
+ onChange={(e) => setColor(e.target.value)}
551
+ className="flex-1 bg-slate-950 border border-slate-800 rounded-lg px-3 py-2 text-sm focus:ring-1 focus:ring-cyan-500 outline-none uppercase font-mono"
552
+ />
553
+ )}
554
+ </div>
555
+ </div>
556
+
557
+ {/* Glow Effect */}
558
+ <div className="flex items-center justify-between bg-slate-950 p-3 rounded-xl border border-slate-800">
559
+ <div className="flex items-center gap-2">
560
+ <Sparkles className="w-4 h-4 text-amber-400" />
561
+ <span className="text-sm font-medium text-slate-300">Neon Glow Effect</span>
562
+ </div>
563
+ <label className="relative inline-flex items-center cursor-pointer">
564
+ <input type="checkbox" checked={glow} onChange={(e) => setGlow(e.target.checked)} className="sr-only peer" />
565
+ <div className="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-cyan-500"></div>
566
+ </label>
567
+ </div>
568
+
569
+ {/* Thickness */}
570
+ <div>
571
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
572
+ <span>Line Thickness</span>
573
+ <span className="text-slate-500">{thickness}px</span>
574
+ </label>
575
+ <input
576
+ type="range"
577
+ min="2" max="64"
578
+ value={thickness}
579
+ onChange={(e) => setThickness(Number(e.target.value))}
580
+ className="w-full accent-cyan-500 cursor-pointer"
581
+ />
582
+ </div>
583
+
584
+ {/* Spacing */}
585
+ <div>
586
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
587
+ <span>Space Between Lines</span>
588
+ <span className="text-slate-500">{spacing}px</span>
589
+ </label>
590
+ <input
591
+ type="range"
592
+ min="0" max="64"
593
+ value={spacing}
594
+ onChange={(e) => setSpacing(Number(e.target.value))}
595
+ className="w-full accent-cyan-500 cursor-pointer"
596
+ />
597
+ </div>
598
+
599
+ {/* Amplitude (Sensitivity) */}
600
+ <div>
601
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
602
+ <span>Amplitude (Height)</span>
603
+ <span className="text-slate-500">{sensitivity.toFixed(1)}x</span>
604
+ </label>
605
+ <input
606
+ type="range"
607
+ min="0.5" max="3.0" step="0.1"
608
+ value={sensitivity}
609
+ onChange={(e) => setSensitivity(Number(e.target.value))}
610
+ className="w-full accent-cyan-500 cursor-pointer"
611
+ />
612
+ </div>
613
+
614
+ {/* Smoothing */}
615
+ <div>
616
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
617
+ <span>Motion Smoothing</span>
618
+ <span className="text-slate-500">{Math.round(smoothing * 100)}%</span>
619
+ </label>
620
+ <input
621
+ type="range"
622
+ min="0.1" max="0.99" step="0.01"
623
+ value={smoothing}
624
+ onChange={(e) => setSmoothing(Number(e.target.value))}
625
+ className="w-full accent-cyan-500 cursor-pointer"
626
+ />
627
+ </div>
628
+
629
+ {/* Transform Settings */}
630
+ <div className="pt-4 mt-4 border-t border-slate-800 space-y-5">
631
+ <h3 className="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">Transform & Position</h3>
632
+
633
+ {/* Scale */}
634
+ <div>
635
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
636
+ <span>Size (Scale)</span>
637
+ <span className="text-slate-500">{scale.toFixed(2)}x</span>
638
+ </label>
639
+ <input
640
+ type="range"
641
+ min="0.1" max="3.0" step="0.1"
642
+ value={scale}
643
+ onChange={(e) => setScale(Number(e.target.value))}
644
+ className="w-full accent-cyan-500 cursor-pointer"
645
+ />
646
+ </div>
647
+
648
+ {/* Rotation */}
649
+ <div>
650
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
651
+ <span>Rotation</span>
652
+ <span className="text-slate-500">{rotation}°</span>
653
+ </label>
654
+ <input
655
+ type="range"
656
+ min="0" max="360" step="1"
657
+ value={rotation}
658
+ onChange={(e) => setRotation(Number(e.target.value))}
659
+ className="w-full accent-cyan-500 cursor-pointer"
660
+ />
661
+ </div>
662
+
663
+ {/* Offset X */}
664
+ <div>
665
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
666
+ <span>Horizontal Position</span>
667
+ <span className="text-slate-500">{offsetX}%</span>
668
+ </label>
669
+ <input
670
+ type="range"
671
+ min="-50" max="50" step="1"
672
+ value={offsetX}
673
+ onChange={(e) => setOffsetX(Number(e.target.value))}
674
+ className="w-full accent-cyan-500 cursor-pointer"
675
+ />
676
+ </div>
677
+
678
+ {/* Offset Y */}
679
+ <div>
680
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
681
+ <span>Vertical Position</span>
682
+ <span className="text-slate-500">{offsetY}%</span>
683
+ </label>
684
+ <input
685
+ type="range"
686
+ min="-50" max="50" step="1"
687
+ value={offsetY}
688
+ onChange={(e) => setOffsetY(Number(e.target.value))}
689
+ className="w-full accent-cyan-500 cursor-pointer"
690
+ />
691
+ </div>
692
+ </div>
693
+
694
+ </div>
695
+ </section>
696
+
697
+ {/* Advanced Layout Section */}
698
+ <section className="bg-slate-900 p-6 rounded-2xl border border-slate-800 shadow-xl">
699
+ <h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-4 flex items-center gap-2">
700
+ <Monitor className="w-4 h-4" /> Output Setup
701
+ </h2>
702
+
703
+ <div className="space-y-5">
704
+ {/* Resolution */}
705
+ <div>
706
+ <label className="block text-sm font-medium text-slate-400 mb-2">Aspect Ratio & Resolution</label>
707
+ <select
708
+ value={resolution}
709
+ onChange={(e) => setResolution(e.target.value)}
710
+ className="w-full bg-slate-950 border border-slate-700 text-slate-300 text-sm rounded-lg px-3 py-2.5 outline-none focus:ring-1 focus:ring-cyan-500"
711
+ >
712
+ <option value="4k_16_9">4K Landscape (3840x2160)</option>
713
+ <option value="1080p_16_9">1080p Landscape (1920x1080)</option>
714
+ <option value="4k_9_16">4K Vertical / Reels (2160x3840)</option>
715
+ <option value="1080p_9_16">1080p Vertical / Reels (1080x1920)</option>
716
+ </select>
717
+ </div>
718
+
719
+ {/* Background */}
720
+ <div>
721
+ <label className="block text-sm font-medium text-slate-400 mb-2">Background Type</label>
722
+ <div className="flex gap-2 mb-3">
723
+ {['transparent', 'color', 'image'].map(type => (
724
+ <button
725
+ key={type}
726
+ onClick={() => setBgType(type)}
727
+ className={`flex-1 py-2 px-2 rounded-lg text-xs font-medium capitalize transition-all ${
728
+ bgType === type
729
+ ? 'bg-slate-700 text-white shadow-inner border border-slate-600'
730
+ : 'bg-slate-950 text-slate-400 border border-slate-800 hover:border-slate-600'
731
+ }`}
732
+ >
733
+ {type}
734
+ </button>
735
+ ))}
736
+ </div>
737
+
738
+ {bgType === 'color' && (
739
+ <div className="flex items-center gap-3 mt-2 bg-slate-950 p-2 rounded-lg border border-slate-800">
740
+ <input type="color" value={bgColor} onChange={(e) => setBgColor(e.target.value)} className="h-8 w-12 rounded cursor-pointer bg-slate-950 border border-slate-700" />
741
+ <span className="text-sm font-mono text-slate-400 uppercase">{bgColor}</span>
742
+ </div>
743
+ )}
744
+
745
+ {bgType === 'image' && (
746
+ <div className="mt-2">
747
+ <label className="flex items-center justify-center gap-2 w-full cursor-pointer bg-slate-950 hover:bg-slate-800 transition-colors border border-dashed border-slate-600 rounded-lg p-3 text-center text-sm text-slate-300">
748
+ <ImagePlus className="w-4 h-4" />
749
+ {bgImageSrc ? 'Change Image' : 'Upload Background Image'}
750
+ <input type="file" accept="image/*" onChange={handleBgUpload} className="hidden" />
751
+ </label>
752
+ </div>
753
+ )}
754
+ </div>
755
+ </div>
756
+ </section>
757
+
758
+ {/* Export Section */}
759
+ <section className="bg-slate-900 p-6 rounded-2xl border border-slate-800 shadow-xl">
760
+ <h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-4">Export Options</h2>
761
+
762
+ <div className="grid grid-cols-2 gap-3">
763
+ <button
764
+ onClick={exportImage}
765
+ disabled={isExportingVideo}
766
+ className="col-span-2 bg-slate-800 hover:bg-slate-700 text-white font-medium py-3 px-4 rounded-xl flex items-center justify-center gap-2 transition-colors disabled:opacity-50"
767
+ >
768
+ <ImageIcon className="w-4 h-4" />
769
+ Save Snapshot (PNG)
770
+ </button>
771
+
772
+ {isExportingVideo ? (
773
+ <div className="col-span-2 space-y-3">
774
+ <button
775
+ onClick={stopVideoExport}
776
+ className="w-full bg-red-500/10 hover:bg-red-500/20 text-red-500 border border-red-500/20 font-bold py-3 px-4 rounded-xl flex items-center justify-center gap-2 transition-colors"
777
+ >
778
+ <StopCircle className="w-5 h-5" />
779
+ Stop & Save
780
+ </button>
781
+ <div className="w-full bg-slate-950 rounded-full h-2.5 border border-slate-800 overflow-hidden">
782
+ <div className="bg-cyan-500 h-2.5 rounded-full transition-all duration-300" style={{ width: `${exportProgress}%` }}></div>
783
+ </div>
784
+ <p className="text-xs text-center text-slate-400">Recording transparent 4K video... {Math.round(exportProgress)}%</p>
785
+ </div>
786
+ ) : (
787
+ <button
788
+ onClick={startVideoExport}
789
+ disabled={!audioSrc}
790
+ className="col-span-2 bg-gradient-to-r from-indigo-500 to-cyan-500 hover:from-indigo-400 hover:to-cyan-400 text-white font-bold py-3 px-4 rounded-xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-cyan-500/20 disabled:opacity-50 disabled:shadow-none"
791
+ >
792
+ <Video className="w-5 h-5" />
793
+ Export 4K Video (WebM)
794
+ </button>
795
+ )}
796
+ </div>
797
+ <p className="text-xs text-slate-500 mt-4 leading-relaxed">
798
+ * Video export records in real-time. Background will be transparent. WebM VP9 format is used for alpha channel support.
799
+ </p>
800
+ </section>
801
+
802
+ </div>
803
+
804
+ {/* Right Column: Preview */}
805
+ <div className="lg:col-span-8 flex flex-col">
806
+ <div className="bg-slate-900 rounded-2xl border border-slate-800 shadow-xl overflow-hidden flex-1 relative flex flex-col">
807
+
808
+ <div className="p-4 border-b border-slate-800 bg-slate-900/80 flex justify-between items-center z-10">
809
+ <span className="text-sm font-semibold text-slate-300 flex items-center gap-2">
810
+ Live Preview
811
+ <span className="bg-slate-800 text-xs px-2 py-0.5 rounded text-slate-400 border border-slate-700">
812
+ {RESOLUTIONS[resolution]?.w}x{RESOLUTIONS[resolution]?.h}
813
+ </span>
814
+ </span>
815
+ <span className="text-xs text-slate-500">
816
+ {bgType === 'transparent' ? 'Checkerboard denotes transparency' : 'Background included in export'}
817
+ </span>
818
+ </div>
819
+
820
+ {/* Checkerboard Background for Transparency check */}
821
+ <div
822
+ className="flex-1 w-full relative flex items-center justify-center p-4 sm:p-8 overflow-hidden bg-black/50"
823
+ style={ bgType === 'transparent' ? {
824
+ backgroundImage: 'repeating-linear-gradient(45deg, #0f172a 25%, transparent 25%, transparent 75%, #0f172a 75%, #0f172a), repeating-linear-gradient(45deg, #0f172a 25%, #1e293b 25%, #1e293b 75%, #0f172a 75%, #0f172a)',
825
+ backgroundPosition: '0 0, 10px 10px',
826
+ backgroundSize: '20px 20px'
827
+ } : {}}
828
+ >
829
+ {/* Actual Canvas */}
830
+ <canvas
831
+ ref={canvasRef}
832
+ width={RESOLUTIONS[resolution]?.w || 3840}
833
+ height={RESOLUTIONS[resolution]?.h || 2160}
834
+ className={`max-w-full max-h-full object-contain drop-shadow-2xl rounded-lg ring-1 ring-white/10 bg-transparent ${RESOLUTIONS[resolution]?.isVertical ? 'aspect-[9/16]' : 'aspect-video'}`}
835
+ />
836
+
837
+ {!audioSrc && (
838
+ <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none bg-slate-900/60 backdrop-blur-sm z-20">
839
+ <Loader2 className="w-12 h-12 text-slate-500 animate-spin mb-4 opacity-50" />
840
+ <p className="text-slate-400 font-medium">Awaiting Audio Input</p>
841
+ </div>
842
+ )}
843
+ </div>
844
+
845
+ </div>
846
+ </div>
847
+
848
+ </main>
849
+ </div>
850
+ );
851
+ }