trysem commited on
Commit
7493ded
·
verified ·
1 Parent(s): 9467a83

Create 4ku-with-davinci-mp4-video

Browse files
Files changed (1) hide show
  1. 4ku-with-davinci-mp4-video +964 -0
4ku-with-davinci-mp4-video ADDED
@@ -0,0 +1,964 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>4K Transparent Audio Visualizer</title>
7
+
8
+ <!-- Tailwind CSS for Styling -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+
11
+ <!-- React & ReactDOM (Production Minified) -->
12
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
13
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
14
+
15
+ <!-- Babel standalone for in-browser JSX compilation -->
16
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
17
+
18
+ <style>
19
+ body { margin: 0; padding: 0; background-color: #020617; color: #e2e8f0; }
20
+ /* Custom scrollbar for webkit */
21
+ ::-webkit-scrollbar { width: 8px; }
22
+ ::-webkit-scrollbar-track { background: #0f172a; }
23
+ ::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
24
+ ::-webkit-scrollbar-thumb:hover { background: #475569; }
25
+ </style>
26
+ </head>
27
+ <body>
28
+ <div id="root"></div>
29
+
30
+ <script type="text/babel">
31
+ const { useState, useRef, useEffect, useCallback } = React;
32
+
33
+ // --- Native SVG Icons ---
34
+ const Upload = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>;
35
+ const Play = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>;
36
+ const Pause = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>;
37
+ const ImageIcon = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>;
38
+ const Video = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>;
39
+ const Settings2 = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>;
40
+ const Loader2 = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>;
41
+ const StopCircle = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><rect x="9" y="9" width="6" height="6"/></svg>;
42
+ const Sparkles = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg>;
43
+ const Monitor = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>;
44
+ const ImagePlus = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/><circle cx="9" cy="9" r="2"/><path d="M19 3v6"/><path d="M16 6h6"/></svg>;
45
+
46
+ function App() {
47
+ const canvasRef = useRef(null);
48
+ const audioRef = useRef(null);
49
+ const audioCtxRef = useRef(null);
50
+ const analyserRef = useRef(null);
51
+ const sourceRef = useRef(null);
52
+ const destRef = useRef(null);
53
+ const reqIdRef = useRef(null);
54
+ const mediaRecorderRef = useRef(null);
55
+ const chunksRef = useRef([]);
56
+ const bgImgRef = useRef(null);
57
+
58
+ // State
59
+ const [audioSrc, setAudioSrc] = useState(null);
60
+ const [fileName, setFileName] = useState('');
61
+ const [isPlaying, setIsPlaying] = useState(false);
62
+ const [isExportingVideo, setIsExportingVideo] = useState(false);
63
+ const [exportProgress, setExportProgress] = useState(0);
64
+
65
+ // Audio Player State
66
+ const [audioTime, setAudioTime] = useState(0);
67
+ const [audioDuration, setAudioDuration] = useState(0);
68
+
69
+ // Settings
70
+ const [vizType, setVizType] = useState('bars'); // 'bars', 'wave', 'circle'
71
+ const [color, setColor] = useState('#00ffcc');
72
+ const [thickness, setThickness] = useState(12);
73
+ const [spacing, setSpacing] = useState(8);
74
+ const [sensitivity, setSensitivity] = useState(1.5);
75
+ const [smoothing, setSmoothing] = useState(0.85);
76
+
77
+ // Transform Settings
78
+ const [offsetX, setOffsetX] = useState(0);
79
+ const [offsetY, setOffsetY] = useState(0);
80
+ const [scale, setScale] = useState(1.0);
81
+ const [rotation, setRotation] = useState(0);
82
+
83
+ // Drag State
84
+ const [isDragging, setIsDragging] = useState(false);
85
+ const dragRef = useRef({ startX: 0, startY: 0, initX: 0, initY: 0 });
86
+
87
+ // Advanced Settings
88
+ const [colorMode, setColorMode] = useState('solid');
89
+ const [color2, setColor2] = useState('#b829ff');
90
+ const [glow, setGlow] = useState(false);
91
+ const [resolution, setResolution] = useState('4k_16_9');
92
+ const [bgType, setBgType] = useState('transparent');
93
+ const [bgColor, setBgColor] = useState('#000000');
94
+ const [bgImageSrc, setBgImageSrc] = useState(null);
95
+ const [exportFormat, setExportFormat] = useState('webm'); // NEW: MP4 vs WebM
96
+
97
+ const RESOLUTIONS = {
98
+ '4k_16_9': { w: 3840, h: 2160, label: '4K (16:9)', isVertical: false },
99
+ '1080p_16_9': { w: 1920, h: 1080, label: '1080p (16:9)', isVertical: false },
100
+ '4k_9_16': { w: 2160, h: 3840, label: '4K Vertical (9:16)', isVertical: true },
101
+ '1080p_9_16': { w: 1080, h: 1920, label: '1080p Vertical (9:16)', isVertical: true }
102
+ };
103
+
104
+ useEffect(() => {
105
+ if (bgImageSrc) {
106
+ const img = new Image();
107
+ img.onload = () => { bgImgRef.current = img; };
108
+ img.src = bgImageSrc;
109
+ } else {
110
+ bgImgRef.current = null;
111
+ }
112
+ }, [bgImageSrc]);
113
+
114
+ const handleBgUpload = (e) => {
115
+ const file = e.target.files[0];
116
+ if (file) {
117
+ if (bgImageSrc) URL.revokeObjectURL(bgImageSrc);
118
+ setBgImageSrc(URL.createObjectURL(file));
119
+ setBgType('image');
120
+ }
121
+ };
122
+
123
+ const initAudio = useCallback(() => {
124
+ if (!audioCtxRef.current) {
125
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
126
+ audioCtxRef.current = new AudioContext();
127
+ analyserRef.current = audioCtxRef.current.createAnalyser();
128
+ destRef.current = audioCtxRef.current.createMediaStreamDestination();
129
+
130
+ if (!sourceRef.current && audioRef.current) {
131
+ sourceRef.current = audioCtxRef.current.createMediaElementSource(audioRef.current);
132
+ sourceRef.current.connect(analyserRef.current);
133
+ analyserRef.current.connect(audioCtxRef.current.destination);
134
+ analyserRef.current.connect(destRef.current);
135
+ }
136
+ }
137
+
138
+ if (audioCtxRef.current.state === 'suspended') {
139
+ audioCtxRef.current.resume();
140
+ }
141
+ }, []);
142
+
143
+ const handleFileUpload = (e) => {
144
+ const file = e.target.files[0];
145
+ if (file) {
146
+ if (audioSrc) URL.revokeObjectURL(audioSrc);
147
+ const url = URL.createObjectURL(file);
148
+ setAudioSrc(url);
149
+ setFileName(file.name);
150
+ setIsPlaying(false);
151
+ setAudioTime(0);
152
+ if (audioRef.current) {
153
+ audioRef.current.pause();
154
+ audioRef.current.currentTime = 0;
155
+ }
156
+ }
157
+ };
158
+
159
+ const togglePlay = () => {
160
+ if (!audioSrc) return;
161
+ initAudio();
162
+
163
+ if (isPlaying) {
164
+ audioRef.current.pause();
165
+ } else {
166
+ audioRef.current.play();
167
+ }
168
+ setIsPlaying(!isPlaying);
169
+ };
170
+
171
+ const handleTimeUpdate = () => {
172
+ if (audioRef.current) setAudioTime(audioRef.current.currentTime);
173
+ };
174
+
175
+ const handleLoadedMetadata = () => {
176
+ if (audioRef.current) setAudioDuration(audioRef.current.duration);
177
+ };
178
+
179
+ const handleSeek = (e) => {
180
+ const time = Number(e.target.value);
181
+ if (audioRef.current) audioRef.current.currentTime = time;
182
+ setAudioTime(time);
183
+ };
184
+
185
+ const formatTime = (time) => {
186
+ if (isNaN(time)) return "0:00";
187
+ const m = Math.floor(time / 60);
188
+ const s = Math.floor(time % 60).toString().padStart(2, '0');
189
+ return `${m}:${s}`;
190
+ };
191
+
192
+ const handleMouseDown = (e) => {
193
+ setIsDragging(true);
194
+ dragRef.current = {
195
+ startX: e.clientX,
196
+ startY: e.clientY,
197
+ initX: offsetX,
198
+ initY: offsetY
199
+ };
200
+ };
201
+
202
+ const handleMouseMove = (e) => {
203
+ if (!isDragging || !canvasRef.current) return;
204
+ const rect = canvasRef.current.getBoundingClientRect();
205
+
206
+ const deltaX = e.clientX - dragRef.current.startX;
207
+ const deltaY = e.clientY - dragRef.current.startY;
208
+
209
+ const percentX = (deltaX / rect.width) * 100;
210
+ const percentY = (deltaY / rect.height) * 100;
211
+
212
+ setOffsetX(Math.max(-50, Math.min(50, dragRef.current.initX + percentX)));
213
+ setOffsetY(Math.max(-50, Math.min(50, dragRef.current.initY + percentY)));
214
+ };
215
+
216
+ const handleMouseUpOrLeave = () => {
217
+ setIsDragging(false);
218
+ };
219
+
220
+ const draw = useCallback(() => {
221
+ if (!canvasRef.current || !analyserRef.current) {
222
+ reqIdRef.current = requestAnimationFrame(draw);
223
+ return;
224
+ }
225
+
226
+ const canvas = canvasRef.current;
227
+ const ctx = canvas.getContext('2d');
228
+ const res = RESOLUTIONS[resolution] || RESOLUTIONS['4k_16_9'];
229
+
230
+ if (canvas.width !== res.w || canvas.height !== res.h) {
231
+ canvas.width = res.w;
232
+ canvas.height = res.h;
233
+ }
234
+
235
+ const width = canvas.width;
236
+ const height = canvas.height;
237
+
238
+ ctx.clearRect(0, 0, width, height);
239
+ if (bgType === 'color') {
240
+ ctx.fillStyle = bgColor;
241
+ ctx.fillRect(0, 0, width, height);
242
+ } else if (bgType === 'image' && bgImgRef.current) {
243
+ const img = bgImgRef.current;
244
+ const imgRatio = img.width / img.height;
245
+ const canvasRatio = width / height;
246
+ let drawW, drawH, drawX, drawY;
247
+ if (imgRatio > canvasRatio) {
248
+ drawH = height;
249
+ drawW = height * imgRatio;
250
+ drawX = (width - drawW) / 2;
251
+ drawY = 0;
252
+ } else {
253
+ drawW = width;
254
+ drawH = width / imgRatio;
255
+ drawX = 0;
256
+ drawY = (height - drawH) / 2;
257
+ }
258
+ ctx.drawImage(img, drawX, drawY, drawW, drawH);
259
+ }
260
+
261
+ analyserRef.current.smoothingTimeConstant = smoothing;
262
+ analyserRef.current.fftSize = 2048;
263
+
264
+ const bufferLength = analyserRef.current.frequencyBinCount;
265
+ const dataArray = new Uint8Array(bufferLength);
266
+
267
+ if (vizType === 'bars' || vizType === 'circle') {
268
+ analyserRef.current.getByteFrequencyData(dataArray);
269
+ } else if (vizType === 'wave') {
270
+ analyserRef.current.getByteTimeDomainData(dataArray);
271
+ }
272
+
273
+ ctx.save();
274
+
275
+ const centerX = width / 2 + (width * (offsetX / 100));
276
+ const centerY = height / 2 + (height * (offsetY / 100));
277
+ ctx.translate(centerX, centerY);
278
+ ctx.scale(scale, scale);
279
+ ctx.rotate((rotation * Math.PI) / 180);
280
+
281
+ let activeColor = color;
282
+ if (colorMode === 'gradient') {
283
+ const grad = ctx.createLinearGradient(-width/2, -height/2, width/2, height/2);
284
+ grad.addColorStop(0, color);
285
+ grad.addColorStop(1, color2);
286
+ activeColor = grad;
287
+ } else if (colorMode === 'rainbow') {
288
+ const grad = ctx.createLinearGradient(-width/2, 0, width/2, 0);
289
+ grad.addColorStop(0, '#ff0000');
290
+ grad.addColorStop(0.16, '#ffff00');
291
+ grad.addColorStop(0.33, '#00ff00');
292
+ grad.addColorStop(0.5, '#00ffff');
293
+ grad.addColorStop(0.66, '#0000ff');
294
+ grad.addColorStop(0.83, '#ff00ff');
295
+ grad.addColorStop(1, '#ff0000');
296
+ activeColor = grad;
297
+ }
298
+
299
+ ctx.strokeStyle = activeColor;
300
+ ctx.fillStyle = activeColor;
301
+ ctx.lineCap = 'round';
302
+ ctx.lineJoin = 'round';
303
+
304
+ const drawVisualizerPath = () => {
305
+ ctx.beginPath();
306
+
307
+ if (vizType === 'bars') {
308
+ const step = thickness + spacing;
309
+ const maxBars = Math.floor((width / 2) / step);
310
+ const usefulLength = Math.floor(bufferLength * 0.75);
311
+ const numBars = Math.min(maxBars, usefulLength);
312
+
313
+ for (let i = 0; i < numBars; i++) {
314
+ const dataIndex = Math.floor((i / numBars) * usefulLength);
315
+ const boost = Math.pow(1 + (i / numBars), 1.5);
316
+ const value = dataArray[dataIndex] * boost * sensitivity;
317
+
318
+ const barHeight = Math.max(thickness / 2, (value / 255) * height * 0.8);
319
+ const xOffset = i * step + (step / 2);
320
+
321
+ ctx.moveTo(xOffset, height / 2 - (thickness / 2));
322
+ ctx.lineTo(xOffset, height / 2 - barHeight);
323
+
324
+ ctx.moveTo(-xOffset, height / 2 - (thickness / 2));
325
+ ctx.lineTo(-xOffset, height / 2 - barHeight);
326
+ }
327
+ } else if (vizType === 'wave') {
328
+ const sliceWidth = width / bufferLength;
329
+ let x = -width / 2;
330
+
331
+ for (let i = 0; i < bufferLength; i++) {
332
+ const normalized = (dataArray[i] / 128.0) - 1;
333
+ const y = normalized * sensitivity * (height / 2);
334
+
335
+ if (i === 0) {
336
+ ctx.moveTo(x, y);
337
+ } else {
338
+ ctx.lineTo(x, y);
339
+ }
340
+ x += sliceWidth;
341
+ }
342
+ } else if (vizType === 'circle') {
343
+ const radius = height / 4;
344
+ const circumference = 2 * Math.PI * radius;
345
+ const stepSize = thickness + spacing;
346
+ const bars = Math.min(180, Math.floor(circumference / stepSize));
347
+ const step = (Math.PI * 2) / bars;
348
+
349
+ for (let i = 0; i < bars; i++) {
350
+ const dataIndex = Math.floor((i / bars) * (bufferLength / 2));
351
+ const value = (dataArray[dataIndex] / 255) * sensitivity;
352
+ const barHeight = Math.max(thickness / 2, value * (height / 3));
353
+ const angle = i * step;
354
+
355
+ const x1 = Math.cos(angle) * radius;
356
+ const y1 = Math.sin(angle) * radius;
357
+ const x2 = Math.cos(angle) * (radius + barHeight);
358
+ const y2 = Math.sin(angle) * (radius + barHeight);
359
+
360
+ ctx.moveTo(x1, y1);
361
+ ctx.lineTo(x2, y2);
362
+ }
363
+ }
364
+
365
+ ctx.stroke();
366
+
367
+ if (vizType === 'circle') {
368
+ const radius = height / 4;
369
+ ctx.beginPath();
370
+ ctx.arc(0, 0, radius - thickness, 0, Math.PI * 2);
371
+ const originalWidth = ctx.lineWidth;
372
+ ctx.lineWidth = originalWidth / 2;
373
+ ctx.stroke();
374
+ ctx.lineWidth = originalWidth;
375
+ }
376
+ };
377
+
378
+ if (glow) {
379
+ ctx.globalCompositeOperation = 'lighter';
380
+
381
+ ctx.lineWidth = thickness * 3;
382
+ ctx.globalAlpha = 0.15;
383
+ drawVisualizerPath();
384
+
385
+ ctx.lineWidth = thickness * 1.5;
386
+ ctx.globalAlpha = 0.4;
387
+ drawVisualizerPath();
388
+
389
+ ctx.lineWidth = thickness;
390
+ ctx.globalAlpha = 1.0;
391
+ drawVisualizerPath();
392
+
393
+ ctx.globalCompositeOperation = 'source-over';
394
+ } else {
395
+ ctx.lineWidth = thickness;
396
+ ctx.globalAlpha = 1.0;
397
+ drawVisualizerPath();
398
+ }
399
+
400
+ ctx.restore();
401
+ reqIdRef.current = requestAnimationFrame(draw);
402
+ }, [vizType, color, thickness, spacing, sensitivity, smoothing, colorMode, color2, glow, resolution, bgType, bgColor, offsetX, offsetY, scale, rotation]);
403
+
404
+ useEffect(() => {
405
+ reqIdRef.current = requestAnimationFrame(draw);
406
+ return () => cancelAnimationFrame(reqIdRef.current);
407
+ }, [draw]);
408
+
409
+ const handleAudioEnded = () => {
410
+ setIsPlaying(false);
411
+ if (isExportingVideo) stopVideoExport();
412
+ };
413
+
414
+ const exportImage = () => {
415
+ if (!canvasRef.current) return;
416
+ const link = document.createElement('a');
417
+ link.download = `visualizer_${Date.now()}.png`;
418
+ link.href = canvasRef.current.toDataURL('image/png');
419
+ link.click();
420
+ };
421
+
422
+ const startVideoExport = async () => {
423
+ if (!audioSrc || !canvasRef.current || !audioCtxRef.current) {
424
+ alert("Please upload an audio file and press play at least once to initialize.");
425
+ return;
426
+ }
427
+
428
+ setIsExportingVideo(true);
429
+ setExportProgress(0);
430
+ chunksRef.current = [];
431
+
432
+ audioRef.current.pause();
433
+ audioRef.current.currentTime = 0;
434
+
435
+ const canvasStream = canvasRef.current.captureStream(60);
436
+ const audioStream = destRef.current.stream;
437
+
438
+ const combinedTracks = [...canvasStream.getTracks(), ...audioStream.getAudioTracks()];
439
+ const combinedStream = new MediaStream(combinedTracks);
440
+
441
+ let options = {};
442
+ let ext = 'webm';
443
+
444
+ if (exportFormat === 'mp4') {
445
+ // Check if browser supports MP4 recording natively
446
+ if (MediaRecorder.isTypeSupported('video/mp4')) {
447
+ options = { mimeType: 'video/mp4' };
448
+ ext = 'mp4';
449
+ } else {
450
+ alert("Your browser doesn't natively support MP4 export. Falling back to WebM.");
451
+ options = { mimeType: 'video/webm; codecs=vp9' };
452
+ }
453
+ } else {
454
+ // WebM fallback chain
455
+ options = { mimeType: 'video/webm; codecs=vp9' };
456
+ if (!MediaRecorder.isTypeSupported(options.mimeType)) {
457
+ options = { mimeType: 'video/webm; codecs=vp8' };
458
+ }
459
+ if (!MediaRecorder.isTypeSupported(options.mimeType)) {
460
+ options = {};
461
+ }
462
+ }
463
+
464
+ try {
465
+ mediaRecorderRef.current = new MediaRecorder(combinedStream, options);
466
+ } catch (e) {
467
+ console.error(e);
468
+ alert("Error starting video recorder. See console.");
469
+ setIsExportingVideo(false);
470
+ return;
471
+ }
472
+
473
+ mediaRecorderRef.current.ondataavailable = (e) => {
474
+ if (e.data && e.data.size > 0) chunksRef.current.push(e.data);
475
+ };
476
+
477
+ mediaRecorderRef.current.onstop = () => {
478
+ const blob = new Blob(chunksRef.current, { type: mediaRecorderRef.current.mimeType });
479
+ const url = URL.createObjectURL(blob);
480
+ const link = document.createElement('a');
481
+ link.download = `visualizer_${resolution}_${Date.now()}.${ext}`;
482
+ link.href = url;
483
+ link.click();
484
+ URL.revokeObjectURL(url);
485
+ setIsExportingVideo(false);
486
+ setExportProgress(0);
487
+ };
488
+
489
+ const duration = audioRef.current.duration;
490
+ const progressInterval = setInterval(() => {
491
+ if (audioRef.current && !audioRef.current.paused) {
492
+ setExportProgress((audioRef.current.currentTime / duration) * 100);
493
+ } else {
494
+ clearInterval(progressInterval);
495
+ }
496
+ }, 500);
497
+
498
+ mediaRecorderRef.current.start(100);
499
+ await audioRef.current.play();
500
+ setIsPlaying(true);
501
+ };
502
+
503
+ const stopVideoExport = () => {
504
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
505
+ mediaRecorderRef.current.stop();
506
+ }
507
+ audioRef.current.pause();
508
+ setIsPlaying(false);
509
+ };
510
+
511
+ return (
512
+ <div className="min-h-screen bg-slate-950 text-slate-200 font-sans selection:bg-cyan-500/30 font-sans">
513
+ <header className="border-b border-slate-800 bg-slate-900/50 p-6 flex items-center justify-between">
514
+ <div className="flex items-center gap-3">
515
+ <div className="bg-cyan-500/20 p-2 rounded-lg">
516
+ <Video className="w-6 h-6 text-cyan-400" />
517
+ </div>
518
+ <h1 className="text-xl font-bold tracking-tight text-white">4K Transparent Visualizer</h1>
519
+ </div>
520
+ <div className="text-sm text-slate-400 hidden sm:block">
521
+ All processing is strictly local.
522
+ </div>
523
+ </header>
524
+
525
+ <main className="container mx-auto p-6 grid lg:grid-cols-12 gap-8">
526
+
527
+ <div className="lg:col-span-4 space-y-6">
528
+
529
+ <section className="bg-slate-900 p-6 rounded-2xl border border-slate-800 shadow-xl">
530
+ <h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-4 flex items-center gap-2">
531
+ <Upload className="w-4 h-4" /> Audio Input
532
+ </h2>
533
+ <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">
534
+ <input
535
+ type="file"
536
+ accept="audio/*"
537
+ onChange={handleFileUpload}
538
+ className="hidden"
539
+ disabled={isExportingVideo}
540
+ />
541
+ <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">
542
+ <Upload className="w-6 h-6 text-cyan-400" />
543
+ </div>
544
+ <p className="font-medium text-slate-300">
545
+ {fileName ? fileName : 'Click to browse audio file'}
546
+ </p>
547
+ <p className="text-xs text-slate-500 mt-2">MP3, WAV, FLAC</p>
548
+ </label>
549
+
550
+ <audio
551
+ ref={audioRef}
552
+ src={audioSrc}
553
+ onEnded={handleAudioEnded}
554
+ onPlay={() => setIsPlaying(true)}
555
+ onPause={() => setIsPlaying(false)}
556
+ onTimeUpdate={handleTimeUpdate}
557
+ onLoadedMetadata={handleLoadedMetadata}
558
+ />
559
+ </section>
560
+
561
+ <section className="bg-slate-900 p-6 rounded-2xl border border-slate-800 shadow-xl">
562
+ <h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-4 flex items-center gap-2">
563
+ <Settings2 className="w-4 h-4" /> Visual Settings
564
+ </h2>
565
+
566
+ <div className="space-y-5">
567
+ <div>
568
+ <label className="block text-sm font-medium text-slate-400 mb-2">Style</label>
569
+ <div className="grid grid-cols-3 gap-2">
570
+ {['bars', 'wave', 'circle'].map(type => (
571
+ <button
572
+ key={type}
573
+ onClick={() => setVizType(type)}
574
+ className={`py-2 px-3 rounded-lg text-sm font-medium capitalize transition-all ${
575
+ vizType === type
576
+ ? 'bg-slate-700 text-white shadow-inner border border-slate-600'
577
+ : 'bg-slate-950 text-slate-400 border border-slate-800 hover:border-slate-600'
578
+ }`}
579
+ >
580
+ {type}
581
+ </button>
582
+ ))}
583
+ </div>
584
+ </div>
585
+
586
+ <div>
587
+ <div className="flex justify-between items-center mb-2">
588
+ <label className="text-sm font-medium text-slate-400">Color Style</label>
589
+ <select
590
+ value={colorMode}
591
+ onChange={(e) => setColorMode(e.target.value)}
592
+ className="bg-slate-950 border border-slate-700 text-slate-300 text-xs rounded px-2 py-1 outline-none"
593
+ >
594
+ <option value="solid">Solid</option>
595
+ <option value="gradient">Gradient</option>
596
+ <option value="rainbow">Rainbow</option>
597
+ </select>
598
+ </div>
599
+
600
+ <div className="flex items-center gap-3">
601
+ {colorMode !== 'rainbow' && (
602
+ <input
603
+ type="color"
604
+ value={color}
605
+ onChange={(e) => setColor(e.target.value)}
606
+ className="h-10 w-14 rounded cursor-pointer bg-slate-950 border border-slate-700 shrink-0"
607
+ />
608
+ )}
609
+ {colorMode === 'gradient' && (
610
+ <>
611
+ <span className="text-slate-500 text-xs font-medium">to</span>
612
+ <input
613
+ type="color"
614
+ value={color2}
615
+ onChange={(e) => setColor2(e.target.value)}
616
+ className="h-10 w-14 rounded cursor-pointer bg-slate-950 border border-slate-700 shrink-0"
617
+ />
618
+ </>
619
+ )}
620
+ {colorMode === 'solid' && (
621
+ <input
622
+ type="text"
623
+ value={color}
624
+ onChange={(e) => setColor(e.target.value)}
625
+ 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"
626
+ />
627
+ )}
628
+ </div>
629
+ </div>
630
+
631
+ <div className="flex items-center justify-between bg-slate-950 p-3 rounded-xl border border-slate-800">
632
+ <div className="flex items-center gap-2">
633
+ <Sparkles className="w-4 h-4 text-amber-400" />
634
+ <span className="text-sm font-medium text-slate-300">Neon Glow Effect</span>
635
+ </div>
636
+ <label className="relative inline-flex items-center cursor-pointer">
637
+ <input type="checkbox" checked={glow} onChange={(e) => setGlow(e.target.checked)} className="sr-only peer" />
638
+ <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>
639
+ </label>
640
+ </div>
641
+
642
+ <div>
643
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
644
+ <span>Line Thickness</span>
645
+ <span className="text-slate-500">{thickness}px</span>
646
+ </label>
647
+ <input
648
+ type="range"
649
+ min="2" max="64"
650
+ value={thickness}
651
+ onChange={(e) => setThickness(Number(e.target.value))}
652
+ className="w-full accent-cyan-500 cursor-pointer"
653
+ />
654
+ </div>
655
+
656
+ <div>
657
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
658
+ <span>Space Between Lines</span>
659
+ <span className="text-slate-500">{spacing}px</span>
660
+ </label>
661
+ <input
662
+ type="range"
663
+ min="0" max="64"
664
+ value={spacing}
665
+ onChange={(e) => setSpacing(Number(e.target.value))}
666
+ className="w-full accent-cyan-500 cursor-pointer"
667
+ />
668
+ </div>
669
+
670
+ <div>
671
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
672
+ <span>Amplitude (Height)</span>
673
+ <span className="text-slate-500">{sensitivity.toFixed(1)}x</span>
674
+ </label>
675
+ <input
676
+ type="range"
677
+ min="0.5" max="3.0" step="0.1"
678
+ value={sensitivity}
679
+ onChange={(e) => setSensitivity(Number(e.target.value))}
680
+ className="w-full accent-cyan-500 cursor-pointer"
681
+ />
682
+ </div>
683
+
684
+ <div>
685
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
686
+ <span>Motion Smoothing</span>
687
+ <span className="text-slate-500">{Math.round(smoothing * 100)}%</span>
688
+ </label>
689
+ <input
690
+ type="range"
691
+ min="0.1" max="0.99" step="0.01"
692
+ value={smoothing}
693
+ onChange={(e) => setSmoothing(Number(e.target.value))}
694
+ className="w-full accent-cyan-500 cursor-pointer"
695
+ />
696
+ </div>
697
+
698
+ <div className="pt-4 mt-4 border-t border-slate-800 space-y-5">
699
+ <h3 className="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">Transform & Position</h3>
700
+
701
+ <div>
702
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
703
+ <span>Size (Scale)</span>
704
+ <span className="text-slate-500">{scale.toFixed(2)}x</span>
705
+ </label>
706
+ <input
707
+ type="range"
708
+ min="0.1" max="3.0" step="0.1"
709
+ value={scale}
710
+ onChange={(e) => setScale(Number(e.target.value))}
711
+ className="w-full accent-cyan-500 cursor-pointer"
712
+ />
713
+ </div>
714
+
715
+ <div>
716
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
717
+ <span>Rotation</span>
718
+ <span className="text-slate-500">{rotation}°</span>
719
+ </label>
720
+ <input
721
+ type="range"
722
+ min="0" max="360" step="1"
723
+ value={rotation}
724
+ onChange={(e) => setRotation(Number(e.target.value))}
725
+ className="w-full accent-cyan-500 cursor-pointer"
726
+ />
727
+ </div>
728
+
729
+ <div>
730
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
731
+ <span>Horizontal Position</span>
732
+ <span className="text-slate-500">{Math.round(offsetX)}%</span>
733
+ </label>
734
+ <input
735
+ type="range"
736
+ min="-50" max="50" step="1"
737
+ value={offsetX}
738
+ onChange={(e) => setOffsetX(Number(e.target.value))}
739
+ className="w-full accent-cyan-500 cursor-pointer"
740
+ />
741
+ </div>
742
+
743
+ <div>
744
+ <label className="flex justify-between text-sm font-medium text-slate-400 mb-2">
745
+ <span>Vertical Position</span>
746
+ <span className="text-slate-500">{Math.round(offsetY)}%</span>
747
+ </label>
748
+ <input
749
+ type="range"
750
+ min="-50" max="50" step="1"
751
+ value={offsetY}
752
+ onChange={(e) => setOffsetY(Number(e.target.value))}
753
+ className="w-full accent-cyan-500 cursor-pointer"
754
+ />
755
+ </div>
756
+ </div>
757
+
758
+ </div>
759
+ </section>
760
+
761
+ <section className="bg-slate-900 p-6 rounded-2xl border border-slate-800 shadow-xl">
762
+ <h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-4 flex items-center gap-2">
763
+ <Monitor className="w-4 h-4" /> Output Setup
764
+ </h2>
765
+
766
+ <div className="space-y-5">
767
+ <div>
768
+ <label className="block text-sm font-medium text-slate-400 mb-2">Aspect Ratio & Resolution</label>
769
+ <select
770
+ value={resolution}
771
+ onChange={(e) => setResolution(e.target.value)}
772
+ 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"
773
+ >
774
+ <option value="4k_16_9">4K Landscape (3840x2160)</option>
775
+ <option value="1080p_16_9">1080p Landscape (1920x1080)</option>
776
+ <option value="4k_9_16">4K Vertical / Reels (2160x3840)</option>
777
+ <option value="1080p_9_16">1080p Vertical / Reels (1080x1920)</option>
778
+ </select>
779
+ </div>
780
+
781
+ <div>
782
+ <label className="block text-sm font-medium text-slate-400 mb-2">Background Type</label>
783
+ <div className="flex gap-2 mb-3">
784
+ {['transparent', 'color', 'image'].map(type => (
785
+ <button
786
+ key={type}
787
+ onClick={() => setBgType(type)}
788
+ className={`flex-1 py-2 px-2 rounded-lg text-xs font-medium capitalize transition-all ${
789
+ bgType === type
790
+ ? 'bg-slate-700 text-white shadow-inner border border-slate-600'
791
+ : 'bg-slate-950 text-slate-400 border border-slate-800 hover:border-slate-600'
792
+ }`}
793
+ >
794
+ {type}
795
+ </button>
796
+ ))}
797
+ </div>
798
+
799
+ {bgType === 'color' && (
800
+ <div className="flex items-center gap-3 mt-2 bg-slate-950 p-2 rounded-lg border border-slate-800">
801
+ <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" />
802
+ <span className="text-sm font-mono text-slate-400 uppercase">{bgColor}</span>
803
+ </div>
804
+ )}
805
+
806
+ {bgType === 'image' && (
807
+ <div className="mt-2">
808
+ <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">
809
+ <ImagePlus className="w-4 h-4" />
810
+ {bgImageSrc ? 'Change Image' : 'Upload Background Image'}
811
+ <input type="file" accept="image/*" onChange={handleBgUpload} className="hidden" />
812
+ </label>
813
+ </div>
814
+ )}
815
+ </div>
816
+ </div>
817
+ </section>
818
+
819
+ <section className="bg-slate-900 p-6 rounded-2xl border border-slate-800 shadow-xl">
820
+ <h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 mb-4">Export Options</h2>
821
+
822
+ <div className="flex flex-col gap-3">
823
+ <div className="bg-slate-950 p-4 rounded-xl border border-slate-800 mb-2">
824
+ <label className="block text-sm font-medium text-slate-400 mb-2">Video Format</label>
825
+ <select
826
+ value={exportFormat}
827
+ onChange={(e) => setExportFormat(e.target.value)}
828
+ className="w-full bg-slate-900 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"
829
+ >
830
+ <option value="webm">WebM (Transparent / Premiere Pro)</option>
831
+ <option value="mp4">MP4 (Solid Background / DaVinci Resolve)</option>
832
+ </select>
833
+
834
+ {exportFormat === 'mp4' && (
835
+ <div className="mt-3 p-3 bg-amber-500/10 border border-amber-500/20 rounded-lg">
836
+ <p className="text-xs text-amber-500/90 leading-relaxed">
837
+ <strong>DaVinci Resolve Tip:</strong> MP4s cannot be transparent. Set your Background Type to <strong>Color (Black)</strong>, export the MP4, and in DaVinci, set your clip's Composite Mode to <strong>Screen</strong> or <strong>Add</strong>.
838
+ </p>
839
+ </div>
840
+ )}
841
+ </div>
842
+
843
+ <button
844
+ onClick={exportImage}
845
+ disabled={isExportingVideo}
846
+ className="w-full 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"
847
+ >
848
+ <ImageIcon className="w-4 h-4" />
849
+ Save Snapshot (PNG)
850
+ </button>
851
+
852
+ {isExportingVideo ? (
853
+ <div className="w-full space-y-3">
854
+ <button
855
+ onClick={stopVideoExport}
856
+ 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"
857
+ >
858
+ <StopCircle className="w-5 h-5" />
859
+ Stop & Save
860
+ </button>
861
+ <div className="w-full bg-slate-950 rounded-full h-2.5 border border-slate-800 overflow-hidden">
862
+ <div className="bg-cyan-500 h-2.5 rounded-full transition-all duration-300" style={{ width: `${exportProgress}%` }}></div>
863
+ </div>
864
+ <p className="text-xs text-center text-slate-400">Recording video... {Math.round(exportProgress)}%</p>
865
+ </div>
866
+ ) : (
867
+ <button
868
+ onClick={startVideoExport}
869
+ disabled={!audioSrc}
870
+ className="w-full 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"
871
+ >
872
+ <Video className="w-5 h-5" />
873
+ Export Video ({exportFormat.toUpperCase()})
874
+ </button>
875
+ )}
876
+ </div>
877
+ </section>
878
+
879
+ </div>
880
+
881
+ <div className="lg:col-span-8 flex flex-col gap-4 lg:sticky lg:top-6 lg:h-[calc(100vh-3rem)]">
882
+ <div className="bg-slate-900 rounded-2xl border border-slate-800 shadow-xl overflow-hidden flex-1 relative flex flex-col min-h-0">
883
+
884
+ <div className="p-4 border-b border-slate-800 bg-slate-900/80 flex justify-between items-center z-10 shrink-0">
885
+ <span className="text-sm font-semibold text-slate-300 flex items-center gap-2">
886
+ Live Preview
887
+ <span className="bg-slate-800 text-xs px-2 py-0.5 rounded text-slate-400 border border-slate-700">
888
+ {RESOLUTIONS[resolution]?.w}x{RESOLUTIONS[resolution]?.h}
889
+ </span>
890
+ </span>
891
+ <span className="text-xs text-slate-500">
892
+ {bgType === 'transparent' ? 'Checkerboard denotes transparency' : 'Background included in export'}
893
+ </span>
894
+ </div>
895
+
896
+ <div
897
+ className="flex-1 w-full relative flex items-center justify-center p-4 sm:p-6 overflow-hidden bg-black/50 min-h-0"
898
+ style={ bgType === 'transparent' ? {
899
+ 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)',
900
+ backgroundPosition: '0 0, 10px 10px',
901
+ backgroundSize: '20px 20px'
902
+ } : {}}
903
+ >
904
+ <canvas
905
+ ref={canvasRef}
906
+ width={RESOLUTIONS[resolution]?.w || 3840}
907
+ height={RESOLUTIONS[resolution]?.h || 2160}
908
+ onMouseDown={handleMouseDown}
909
+ onMouseMove={handleMouseMove}
910
+ onMouseUp={handleMouseUpOrLeave}
911
+ onMouseLeave={handleMouseUpOrLeave}
912
+ 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'} ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
913
+ />
914
+
915
+ {!audioSrc && (
916
+ <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none bg-slate-900/80 backdrop-blur-sm z-20">
917
+ <Loader2 className="w-12 h-12 text-slate-500 animate-spin mb-4 opacity-50" />
918
+ <p className="text-slate-400 font-medium">Awaiting Audio Input</p>
919
+ </div>
920
+ )}
921
+ </div>
922
+ </div>
923
+
924
+ <div className="bg-slate-900 p-4 sm:p-5 rounded-2xl border border-slate-800 shadow-xl shrink-0 flex flex-col gap-3">
925
+ <div className="flex items-center gap-4">
926
+ <button
927
+ onClick={togglePlay}
928
+ disabled={!audioSrc || isExportingVideo}
929
+ className="w-12 h-12 shrink-0 bg-cyan-500 hover:bg-cyan-400 text-slate-950 rounded-full flex items-center justify-center transition-colors disabled:opacity-50 disabled:hover:bg-cyan-500"
930
+ >
931
+ {isPlaying ? <Pause className="w-6 h-6 fill-current" /> : <Play className="w-6 h-6 fill-current ml-1" />}
932
+ </button>
933
+
934
+ <div className="flex-1 flex flex-col gap-2">
935
+ <div className="flex justify-between text-xs font-mono text-slate-400">
936
+ <span>{formatTime(audioTime)}</span>
937
+ <span className="text-slate-300 font-sans truncate px-4">{fileName || 'No audio selected'}</span>
938
+ <span>{formatTime(audioDuration)}</span>
939
+ </div>
940
+ <input
941
+ type="range"
942
+ min="0"
943
+ max={audioDuration || 100}
944
+ value={audioTime}
945
+ onChange={handleSeek}
946
+ disabled={!audioSrc || isExportingVideo}
947
+ className="w-full accent-cyan-500 cursor-pointer h-2 bg-slate-800 rounded-lg appearance-none [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:bg-cyan-400 [&::-webkit-slider-thumb]:rounded-full"
948
+ />
949
+ </div>
950
+ </div>
951
+ </div>
952
+
953
+ </div>
954
+
955
+ </main>
956
+ </div>
957
+ );
958
+ }
959
+
960
+ const root = ReactDOM.createRoot(document.getElementById('root'));
961
+ root.render(<App />);
962
+ </script>
963
+ </body>
964
+ </html>