trysem commited on
Commit
5e6dfa9
·
verified ·
1 Parent(s): a52782c

Create m4pro_running_copy_best_rightside_allignment

Browse files
m4pro_running_copy_best_rightside_allignment ADDED
@@ -0,0 +1,1216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <script src="https://cdn.tailwindcss.com"></script>
9
+
10
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
11
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
12
+
13
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
14
+
15
+ <style>
16
+ body { margin: 0; padding: 0; background-color: #020617; color: #e2e8f0; }
17
+
18
+ .custom-scrollbar::-webkit-scrollbar { width: 6px; }
19
+ .custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
20
+ .custom-scrollbar::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
21
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #475569; }
22
+
23
+ ::-webkit-scrollbar { width: 8px; }
24
+ ::-webkit-scrollbar-track { background: #0f172a; }
25
+ ::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 4px; }
26
+ ::-webkit-scrollbar-thumb:hover { background: #334155; }
27
+
28
+ /* Range slider customizations */
29
+ input[type=range] {
30
+ -webkit-appearance: none;
31
+ background: transparent;
32
+ }
33
+ input[type=range]::-webkit-slider-thumb {
34
+ -webkit-appearance: none;
35
+ height: 14px;
36
+ width: 14px;
37
+ border-radius: 50%;
38
+ background: #06b6d4;
39
+ cursor: pointer;
40
+ margin-top: -5px;
41
+ box-shadow: 0 0 10px rgba(6, 182, 212, 0.5);
42
+ }
43
+ input[type=range]::-webkit-slider-runnable-track {
44
+ width: 100%;
45
+ height: 4px;
46
+ cursor: pointer;
47
+ background: #1e293b;
48
+ border-radius: 2px;
49
+ }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <div id="root"></div>
54
+
55
+ <script type="text/babel">
56
+ const { useState, useRef, useEffect, useCallback } = React;
57
+
58
+ // --- SVG Icons ---
59
+ 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>;
60
+ 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>;
61
+ 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>;
62
+ 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>;
63
+ 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>;
64
+ 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>;
65
+ 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>;
66
+ 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>;
67
+ 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>;
68
+ 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>;
69
+ 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>;
70
+ const RotateCcw = ({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="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>;
71
+ const Lock = ({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="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>;
72
+ const Unlock = ({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="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>;
73
+ const Activity = ({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"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>;
74
+ const Scissors = ({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="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg>;
75
+ const Type = ({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"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>;
76
+ const Zap = ({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="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>;
77
+ const Save = ({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="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>;
78
+ const Maximize = ({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="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>;
79
+ const FlipHorizontal = ({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="M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h3"/><path d="M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3"/><path d="M12 20v2"/><path d="M12 14v2"/><path d="M12 8v2"/><path d="M12 2v2"/></svg>;
80
+ const FlipVertical = ({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 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3"/><path d="M21 16v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/></svg>;
81
+ const RefreshCw = ({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="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>;
82
+ const ChevronDown = ({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"><polyline points="6 9 12 15 18 9"/></svg>;
83
+ const ChevronUp = ({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"><polyline points="18 15 12 9 6 15"/></svg>;
84
+
85
+ // UI Component for Slider Headers with Reset
86
+ const ControlHeader = ({ label, valueDisplay, onReset, extra }) => (
87
+ <div className="flex justify-between items-end mb-2">
88
+ <div className="flex items-center gap-2">
89
+ <span className="text-sm font-medium text-slate-400">{label}</span>
90
+ {extra}
91
+ </div>
92
+ <div className="flex items-center gap-2">
93
+ {valueDisplay !== undefined && <span className="text-xs text-slate-500 font-mono">{valueDisplay}</span>}
94
+ {onReset && (
95
+ <button onClick={onReset} className="text-slate-500 hover:text-cyan-400 transition-colors focus:outline-none" title={`Reset ${label}`}>
96
+ <RotateCcw className="w-3 h-3" />
97
+ </button>
98
+ )}
99
+ </div>
100
+ </div>
101
+ );
102
+
103
+ function App() {
104
+ const canvasRef = useRef(null);
105
+ const audioRef = useRef(null);
106
+ const audioCtxRef = useRef(null);
107
+ const analyserRef = useRef(null);
108
+ const sourceRef = useRef(null);
109
+ const destRef = useRef(null);
110
+ const reqIdRef = useRef(null);
111
+ const mediaRecorderRef = useRef(null);
112
+ const chunksRef = useRef([]);
113
+ const bgImgRef = useRef(null);
114
+
115
+ // Two separate arrays: one for visualizer (time or freq), one strictly for bass detection
116
+ const dataArrayRef = useRef(new Uint8Array(2048));
117
+ const bassArrayRef = useRef(new Uint8Array(10));
118
+ const lastDrawTimeRef = useRef(0);
119
+
120
+ // --- Audio Player State ---
121
+ const [audioSrc, setAudioSrc] = useState(null);
122
+ const [fileName, setFileName] = useState('');
123
+ const [isPlaying, setIsPlaying] = useState(false);
124
+ const [isExportingVideo, setIsExportingVideo] = useState(false);
125
+ const [exportProgress, setExportProgress] = useState(0);
126
+ const [audioTime, setAudioTime] = useState(0);
127
+ const [audioDuration, setAudioDuration] = useState(0);
128
+
129
+ // --- Visual Settings ---
130
+ const [vizType, setVizType] = useState('bars');
131
+ const [color, setColor] = useState('#00ffcc');
132
+ const [thickness, setThickness] = useState(12);
133
+ const [spacing, setSpacing] = useState(8);
134
+ const [sensitivity, setSensitivity] = useState(1.5);
135
+ const [smoothing, setSmoothing] = useState(0.85);
136
+
137
+ // --- Transform Settings ---
138
+ const [offsetX, setOffsetX] = useState(0);
139
+ const [offsetY, setOffsetY] = useState(0);
140
+ const [scale, setScale] = useState(1.0);
141
+ const [scaleX, setScaleX] = useState(1.0);
142
+ const [scaleY, setScaleY] = useState(1.0);
143
+ const [rotation, setRotation] = useState(0);
144
+ const offsetRef = useRef({ x: 0, y: 0 });
145
+
146
+ // Drag State
147
+ const [isDragging, setIsDragging] = useState(false);
148
+ const dragRef = useRef({ startX: 0, startY: 0, initX: 0, initY: 0 });
149
+
150
+ // --- Advanced Features States ---
151
+ const [colorMode, setColorMode] = useState('solid');
152
+ const [color2, setColor2] = useState('#b829ff');
153
+ const [glow, setGlow] = useState(false);
154
+ const [taperEdges, setTaperEdges] = useState(false);
155
+ const [resolution, setResolution] = useState('4k_16_9');
156
+ const [bgType, setBgType] = useState('transparent');
157
+ const [bgColor, setBgColor] = useState('#000000');
158
+ const [bgImageSrc, setBgImageSrc] = useState(null);
159
+ const [bgImageFit, setBgImageFit] = useState('contain');
160
+ const [exportFormat, setExportFormat] = useState('mp4');
161
+ const [exportFps, setExportFps] = useState(30);
162
+
163
+ const [scaleLock, setScaleLock] = useState(false);
164
+ const scaleRatioRef = useRef(1.0);
165
+
166
+ const [mirrorX, setMirrorX] = useState(false);
167
+ const [mirrorY, setMirrorY] = useState(false);
168
+ const [isFilled, setIsFilled] = useState(false);
169
+ const [bgBeatReactive, setBgBeatReactive] = useState(false);
170
+
171
+ const [overlayText1, setOverlayText1] = useState('');
172
+ const [overlayText2, setOverlayText2] = useState('');
173
+ const [overlayTextSize, setOverlayTextSize] = useState(60);
174
+ const [overlayTextColor, setOverlayTextColor] = useState('#ffffff');
175
+ const [overlayTextPos, setOverlayTextPos] = useState('bottom');
176
+
177
+ const [freqBand, setFreqBand] = useState('all');
178
+ const [flashOnBeat, setFlashOnBeat] = useState(false);
179
+ const [flashColor, setFlashColor] = useState('#ffffff');
180
+ const [flashThreshold, setFlashThreshold] = useState(230);
181
+ const [exportStart, setExportStart] = useState(0);
182
+ const [exportEnd, setExportEnd] = useState(0);
183
+ const [safeZone, setSafeZone] = useState('none');
184
+
185
+ // --- Minimize section states ---
186
+ const [minStates, setMinStates] = useState({
187
+ audio: false, visual: false, transform: false, text: false, output: false, export: false
188
+ });
189
+ const toggleMin = function(key) {
190
+ setMinStates(function(prev) { return {...prev, [key]: !prev[key]}; });
191
+ };
192
+
193
+ const playButtonRef = useRef(null);
194
+
195
+ const RESOLUTIONS = {
196
+ '4k_16_9': { w: 3840, h: 2160, label: '4K (16:9)', isVertical: false },
197
+ '1080p_16_9': { w: 1920, h: 1080, label: '1080p (16:9)', isVertical: false },
198
+ '4k_9_16': { w: 2160, h: 3840, label: '4K Vertical (9:16)', isVertical: true },
199
+ '1080p_9_16': { w: 1080, h: 1920, label: '1080p Vertical (9:16)', isVertical: true }
200
+ };
201
+
202
+ // --- Preset & Reset Logic ---
203
+ const savePreset = function() {
204
+ const preset = { vizType, color, thickness, spacing, sensitivity, smoothing, colorMode, color2, glow, taperEdges, resolution, bgType, bgColor, bgImageFit, scale, scaleX, scaleY, rotation, offsetX, offsetY, mirrorX, mirrorY, isFilled, bgBeatReactive, freqBand, flashOnBeat, flashColor, flashThreshold, overlayText1, overlayText2, overlayTextSize, overlayTextColor, overlayTextPos };
205
+ localStorage.setItem('viz_preset_v4', JSON.stringify(preset));
206
+ alert('Settings saved as Preset!');
207
+ };
208
+
209
+ const loadPreset = function() {
210
+ const str = localStorage.getItem('viz_preset_v4');
211
+ if (str) {
212
+ try {
213
+ const p = JSON.parse(str);
214
+ if (p.vizType !== undefined) setVizType(p.vizType);
215
+ if (p.color !== undefined) setColor(p.color);
216
+ if (p.thickness !== undefined) setThickness(p.thickness);
217
+ if (p.spacing !== undefined) setSpacing(p.spacing);
218
+ if (p.sensitivity !== undefined) setSensitivity(p.sensitivity);
219
+ if (p.smoothing !== undefined) setSmoothing(p.smoothing);
220
+ if (p.colorMode !== undefined) setColorMode(p.colorMode);
221
+ if (p.color2 !== undefined) setColor2(p.color2);
222
+ if (p.glow !== undefined) setGlow(p.glow);
223
+ if (p.taperEdges !== undefined) setTaperEdges(p.taperEdges);
224
+ if (p.resolution !== undefined) setResolution(p.resolution);
225
+ if (p.bgType !== undefined) setBgType(p.bgType);
226
+ if (p.bgColor !== undefined) setBgColor(p.bgColor);
227
+ if (p.bgImageFit !== undefined) setBgImageFit(p.bgImageFit);
228
+ if (p.scale !== undefined) setScale(p.scale);
229
+ if (p.scaleX !== undefined) setScaleX(p.scaleX);
230
+ if (p.scaleY !== undefined) setScaleY(p.scaleY);
231
+ if (p.rotation !== undefined) setRotation(p.rotation);
232
+ if (p.offsetX !== undefined) { setOffsetX(p.offsetX); offsetRef.current.x = p.offsetX; }
233
+ if (p.offsetY !== undefined) { setOffsetY(p.offsetY); offsetRef.current.y = p.offsetY; }
234
+ if (p.mirrorX !== undefined) setMirrorX(p.mirrorX);
235
+ if (p.mirrorY !== undefined) setMirrorY(p.mirrorY);
236
+ if (p.isFilled !== undefined) setIsFilled(p.isFilled);
237
+ if (p.bgBeatReactive !== undefined) setBgBeatReactive(p.bgBeatReactive);
238
+ if (p.freqBand !== undefined) setFreqBand(p.freqBand);
239
+ if (p.flashOnBeat !== undefined) setFlashOnBeat(p.flashOnBeat);
240
+ if (p.flashColor !== undefined) setFlashColor(p.flashColor);
241
+ if (p.flashThreshold !== undefined) setFlashThreshold(p.flashThreshold);
242
+ if (p.overlayText1 !== undefined) setOverlayText1(p.overlayText1);
243
+ if (p.overlayText2 !== undefined) setOverlayText2(p.overlayText2);
244
+ if (p.overlayTextSize !== undefined) setOverlayTextSize(p.overlayTextSize);
245
+ if (p.overlayTextColor !== undefined) setOverlayTextColor(p.overlayTextColor);
246
+ if (p.overlayTextPos !== undefined) setOverlayTextPos(p.overlayTextPos);
247
+ } catch (e) { console.error("Failed to load preset"); }
248
+ } else { alert('No preset found!'); }
249
+ };
250
+
251
+ const handleResetAll = function() {
252
+ if (confirm("Are you sure you want to reset all settings to defaults?")) {
253
+ setVizType('bars'); setColor('#00ffcc'); setThickness(12); setSpacing(8); setSensitivity(1.5); setSmoothing(0.85);
254
+ setColorMode('solid'); setColor2('#b829ff'); setGlow(false); setTaperEdges(false); setResolution('4k_16_9');
255
+ setBgType('transparent'); setBgColor('#000000'); setBgImageSrc(null); setBgImageFit('contain');
256
+ setScale(1.0); setScaleX(1.0); setScaleY(1.0); setRotation(0); setOffsetX(0); setOffsetY(0); offsetRef.current = {x: 0, y: 0};
257
+ setMirrorX(false); setMirrorY(false); setIsFilled(false); setBgBeatReactive(false);
258
+ setOverlayText1(''); setOverlayText2(''); setOverlayTextSize(60); setOverlayTextColor('#ffffff'); setOverlayTextPos('bottom');
259
+ setFreqBand('all'); setFlashOnBeat(false); setFlashColor('#ffffff'); setFlashThreshold(230); setExportStart(0); setExportEnd(0);
260
+ }
261
+ };
262
+
263
+ // --- Sidebar Section Header ---
264
+ const SectionHeader = ({ title, icon: Icon, sectionKey }) => (
265
+ <div className={`flex justify-between items-center ${minStates[sectionKey] ? '' : 'mb-4'}`}>
266
+ <h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 flex items-center gap-2">
267
+ <Icon className="w-4 h-4" /> {title}
268
+ </h2>
269
+ <button onClick={function() { toggleMin(sectionKey); }} className="p-1.5 text-slate-500 hover:text-slate-300 hover:bg-slate-800 rounded transition-colors" title={minStates[sectionKey] ? "Expand" : "Minimize"}>
270
+ {minStates[sectionKey] ? <ChevronDown className="w-4 h-4" /> : <ChevronUp className="w-4 h-4" />}
271
+ </button>
272
+ </div>
273
+ );
274
+
275
+ useEffect(() => {
276
+ if (bgImageSrc) {
277
+ const img = new Image();
278
+ img.onload = () => { bgImgRef.current = img; };
279
+ img.src = bgImageSrc;
280
+ } else { bgImgRef.current = null; }
281
+ }, [bgImageSrc]);
282
+
283
+ const handleBgUpload = function(e) {
284
+ const file = e.target.files[0];
285
+ if (file) {
286
+ if (bgImageSrc) URL.revokeObjectURL(bgImageSrc);
287
+ setBgImageSrc(URL.createObjectURL(file));
288
+ setBgType('image');
289
+ }
290
+ };
291
+
292
+ const initAudio = useCallback(() => {
293
+ if (!audioCtxRef.current) {
294
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
295
+ audioCtxRef.current = new AudioContext();
296
+ analyserRef.current = audioCtxRef.current.createAnalyser();
297
+ destRef.current = audioCtxRef.current.createMediaStreamDestination();
298
+
299
+ if (!sourceRef.current && audioRef.current) {
300
+ sourceRef.current = audioCtxRef.current.createMediaElementSource(audioRef.current);
301
+ sourceRef.current.connect(analyserRef.current);
302
+ analyserRef.current.connect(audioCtxRef.current.destination);
303
+ analyserRef.current.connect(destRef.current);
304
+ }
305
+ }
306
+ if (audioCtxRef.current.state === 'suspended') {
307
+ audioCtxRef.current.resume();
308
+ }
309
+ }, []);
310
+
311
+ const handleFileUpload = function(e) {
312
+ const file = e.target.files[0];
313
+ if (file) {
314
+ if (audioSrc) URL.revokeObjectURL(audioSrc);
315
+ const url = URL.createObjectURL(file);
316
+ setAudioSrc(url);
317
+ setFileName(file.name);
318
+ setIsPlaying(false);
319
+ setAudioTime(0);
320
+ if (audioRef.current) {
321
+ audioRef.current.pause();
322
+ audioRef.current.currentTime = 0;
323
+ }
324
+ }
325
+ };
326
+
327
+ const togglePlay = function() {
328
+ if (!audioSrc) return;
329
+ initAudio();
330
+ if (isPlaying) audioRef.current.pause();
331
+ else audioRef.current.play();
332
+ setIsPlaying(!isPlaying);
333
+ };
334
+
335
+ const handleTimeUpdate = function() { if (audioRef.current) setAudioTime(audioRef.current.currentTime); };
336
+ const handleLoadedMetadata = function() { if (audioRef.current) setAudioDuration(audioRef.current.duration); };
337
+ const handleSeek = function(e) {
338
+ const time = Number(e.target.value);
339
+ if (audioRef.current) audioRef.current.currentTime = time;
340
+ setAudioTime(time);
341
+ };
342
+
343
+ const formatTime = function(time) {
344
+ if (isNaN(time)) return "0:00";
345
+ const m = Math.floor(time / 60);
346
+ const s = Math.floor(time % 60).toString().padStart(2, '0');
347
+ return `${m}:${s}`;
348
+ };
349
+
350
+ // --- Drag Logic ---
351
+ const handleMouseDown = function(e) {
352
+ setIsDragging(true);
353
+ dragRef.current = { startX: e.clientX, startY: e.clientY, initX: offsetRef.current.x, initY: offsetRef.current.y };
354
+ };
355
+ const handleMouseMove = function(e) {
356
+ if (!isDragging || !canvasRef.current) return;
357
+ const rect = canvasRef.current.getBoundingClientRect();
358
+ const deltaX = e.clientX - dragRef.current.startX;
359
+ const deltaY = e.clientY - dragRef.current.startY;
360
+ const percentX = (deltaX / rect.width) * 100;
361
+ const percentY = (deltaY / rect.height) * 100;
362
+ const newX = Math.max(-50, Math.min(50, dragRef.current.initX + percentX));
363
+ const newY = Math.max(-50, Math.min(50, dragRef.current.initY + percentY));
364
+ offsetRef.current.x = newX; offsetRef.current.y = newY;
365
+ setOffsetX(newX); setOffsetY(newY);
366
+ };
367
+ const handleMouseUpOrLeave = function() { setIsDragging(false); };
368
+
369
+ const handleScaleXChange = function(val) {
370
+ setScaleX(val);
371
+ if (scaleLock) setScaleY(Math.max(0.1, Math.min(5.0, val / scaleRatioRef.current)));
372
+ };
373
+ const handleScaleYChange = function(val) {
374
+ setScaleY(val);
375
+ if (scaleLock) setScaleX(Math.max(0.1, Math.min(5.0, val * scaleRatioRef.current)));
376
+ };
377
+ const toggleScaleLock = function() {
378
+ if (!scaleLock) scaleRatioRef.current = scaleX / scaleY;
379
+ setScaleLock(!scaleLock);
380
+ };
381
+
382
+ useEffect(() => {
383
+ const handleKeyDown = (e) => {
384
+ if (e.code === 'Space' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
385
+ e.preventDefault();
386
+ playButtonRef.current?.click();
387
+ }
388
+ };
389
+ window.addEventListener('keydown', handleKeyDown);
390
+ return () => window.removeEventListener('keydown', handleKeyDown);
391
+ }, []);
392
+
393
+ const getSafeZoneStyle = function(zone, canvasW, canvasH) {
394
+ const canvasRatio = canvasW / canvasH;
395
+ let targetRatio = canvasRatio;
396
+
397
+ if (zone === '1:1') targetRatio = 1;
398
+ else if (zone === '4:5') targetRatio = 4/5;
399
+ else if (zone === '9:16') targetRatio = 9/16;
400
+ else if (zone === '16:9') targetRatio = 16/9;
401
+ else if (zone === 'title_safe') return { width: '90%', height: '90%' };
402
+
403
+ if (targetRatio > canvasRatio) return { width: '100%', aspectRatio: `${targetRatio}` };
404
+ else return { height: '100%', aspectRatio: `${targetRatio}` };
405
+ };
406
+
407
+ // --- CORE DRAW LOOP ---
408
+ const draw = useCallback(() => {
409
+ if (!canvasRef.current) {
410
+ reqIdRef.current = requestAnimationFrame(draw);
411
+ return;
412
+ }
413
+
414
+ const now = performance.now();
415
+ if (isExportingVideo && exportFps < 60) {
416
+ const msPerFrame = 1000 / exportFps;
417
+ if (now - lastDrawTimeRef.current < msPerFrame) {
418
+ reqIdRef.current = requestAnimationFrame(draw);
419
+ return;
420
+ }
421
+ }
422
+ lastDrawTimeRef.current = now;
423
+
424
+ const canvas = canvasRef.current;
425
+ const ctx = canvas.getContext('2d', { alpha: bgType === 'transparent', willReadFrequently: false });
426
+ const res = RESOLUTIONS[resolution] || RESOLUTIONS['4k_16_9'];
427
+
428
+ if (canvas.width !== res.w || canvas.height !== res.h) {
429
+ canvas.width = res.w; canvas.height = res.h;
430
+ }
431
+
432
+ const width = canvas.width;
433
+ const height = canvas.height;
434
+
435
+ ctx.clearRect(0, 0, width, height);
436
+
437
+ // Fetch Data First
438
+ let bassAvg = 0;
439
+ if (analyserRef.current) {
440
+ analyserRef.current.smoothingTimeConstant = smoothing;
441
+ analyserRef.current.fftSize = 2048;
442
+
443
+ const dataArray = dataArrayRef.current;
444
+ const bassArray = bassArrayRef.current;
445
+
446
+ // Always get frequency data for accurate bass detection regardless of visualizer mode
447
+ analyserRef.current.getByteFrequencyData(bassArray);
448
+ for(let i=0; i<10; i++) bassAvg += bassArray[i];
449
+ bassAvg /= 10;
450
+
451
+ // Get visualizer specific data
452
+ if (vizType === 'bars' || vizType === 'circle' || vizType === 'symmetric_wave') {
453
+ analyserRef.current.getByteFrequencyData(dataArray);
454
+ } else if (vizType === 'wave') {
455
+ analyserRef.current.getByteTimeDomainData(dataArray);
456
+ }
457
+ }
458
+
459
+ // Draw Background
460
+ if (bgType === 'color') {
461
+ ctx.fillStyle = bgColor;
462
+ ctx.fillRect(0, 0, width, height);
463
+ } else if (bgType === 'image' && bgImgRef.current) {
464
+ const img = bgImgRef.current;
465
+ const imgRatio = img.width / img.height;
466
+ const canvasRatio = width / height;
467
+ let drawW, drawH, drawX, drawY;
468
+
469
+ if (bgImageFit === 'stretch') { drawW = width; drawH = height; drawX = 0; drawY = 0; }
470
+ else if (bgImageFit === 'cover') {
471
+ if (imgRatio > canvasRatio) { drawH = height; drawW = height * imgRatio; drawX = (width - drawW) / 2; drawY = 0; }
472
+ else { drawW = width; drawH = width / imgRatio; drawX = 0; drawY = (height - drawH) / 2; }
473
+ } else if (bgImageFit === 'fit-width') {
474
+ drawW = width; drawH = width / imgRatio; drawX = 0; drawY = (height - drawH) / 2;
475
+ } else {
476
+ if (imgRatio > canvasRatio) { drawW = width; drawH = width / imgRatio; drawX = 0; drawY = (height - drawH) / 2; }
477
+ else { drawH = height; drawW = height * imgRatio; drawX = (width - drawW) / 2; drawY = 0; }
478
+ }
479
+
480
+ // Apply Beat-Reactive Background Scale
481
+ if (bgBeatReactive) {
482
+ const bgScale = 1.0 + (bassAvg / 255) * 0.08;
483
+ const scaledW = drawW * bgScale;
484
+ const scaledH = drawH * bgScale;
485
+ const scaledX = drawX - (scaledW - drawW) / 2;
486
+ const scaledY = drawY - (scaledH - drawH) / 2;
487
+ ctx.drawImage(img, scaledX, scaledY, scaledW, scaledH);
488
+ } else {
489
+ ctx.drawImage(img, drawX, drawY, drawW, drawH);
490
+ }
491
+ }
492
+
493
+ if (analyserRef.current) {
494
+ const bufferLength = analyserRef.current.frequencyBinCount;
495
+ const dataArray = dataArrayRef.current;
496
+
497
+ ctx.save();
498
+ const centerX = width / 2 + (width * (offsetRef.current.x / 100));
499
+ const centerY = height / 2 + (height * (offsetRef.current.y / 100));
500
+ ctx.translate(centerX, centerY);
501
+ ctx.scale(scale * scaleX, scale * scaleY);
502
+ ctx.rotate((rotation * Math.PI) / 180);
503
+
504
+ // Set Colors & Flashes
505
+ let activeColor = color;
506
+ if (colorMode === 'gradient') {
507
+ const grad = ctx.createLinearGradient(-width/2, -height/2, width/2, height/2);
508
+ grad.addColorStop(0, color); grad.addColorStop(1, color2);
509
+ activeColor = grad;
510
+ } else if (colorMode === 'rainbow') {
511
+ const grad = ctx.createLinearGradient(-width/2, 0, width/2, 0);
512
+ grad.addColorStop(0, '#ff0000'); grad.addColorStop(0.16, '#ffff00'); grad.addColorStop(0.33, '#00ff00');
513
+ grad.addColorStop(0.5, '#00ffff'); grad.addColorStop(0.66, '#0000ff'); grad.addColorStop(0.83, '#ff00ff');
514
+ grad.addColorStop(1, '#ff0000'); activeColor = grad;
515
+ }
516
+
517
+ if (flashOnBeat && bassAvg > flashThreshold) { activeColor = flashColor; }
518
+
519
+ ctx.strokeStyle = activeColor;
520
+ ctx.fillStyle = activeColor;
521
+ ctx.lineCap = 'round';
522
+ ctx.lineJoin = 'round';
523
+
524
+ // Frequency Band Isolation
525
+ let startIndex = 0;
526
+ let bandLength = Math.floor(bufferLength * 0.75);
527
+ if (freqBand === 'bass') { startIndex = 0; bandLength = Math.floor(bufferLength * 0.08); }
528
+ else if (freqBand === 'mid') { startIndex = Math.floor(bufferLength * 0.08); bandLength = Math.floor(bufferLength * 0.3); }
529
+ else if (freqBand === 'treble') { startIndex = Math.floor(bufferLength * 0.38); bandLength = Math.floor(bufferLength * 0.37); }
530
+
531
+ const drawVisualizerPath = () => {
532
+ const buildShapes = () => {
533
+ ctx.beginPath();
534
+
535
+ if (vizType === 'bars') {
536
+ const step = thickness + spacing;
537
+ const maxBars = Math.floor((width / 2) / step);
538
+ const numBars = Math.min(maxBars, bandLength);
539
+
540
+ for (let i = 0; i < numBars; i++) {
541
+ const dataIndex = startIndex + Math.floor((i / numBars) * bandLength);
542
+ const boost = Math.pow(1 + (i / numBars), 1.5);
543
+
544
+ let edgeMultiplier = 1;
545
+ if (taperEdges) edgeMultiplier = Math.pow(1 - (i / numBars), 2);
546
+
547
+ const value = dataArray[dataIndex] * boost * sensitivity * edgeMultiplier;
548
+ const barHeight = Math.max(thickness / 2, (value / 255) * height * 0.8);
549
+ const xOffset = i * step + (step / 2);
550
+
551
+ if (isFilled) {
552
+ ctx.rect(xOffset - thickness/2, height/2 - barHeight, thickness, barHeight);
553
+ ctx.rect(-xOffset - thickness/2, height/2 - barHeight, thickness, barHeight);
554
+ } else {
555
+ ctx.moveTo(xOffset, height / 2 - (thickness / 2)); ctx.lineTo(xOffset, height / 2 - barHeight);
556
+ ctx.moveTo(-xOffset, height / 2 - (thickness / 2)); ctx.lineTo(-xOffset, height / 2 - barHeight);
557
+ }
558
+ }
559
+ } else if (vizType === 'symmetric_wave') {
560
+ // Frequency domain discrete wave (symmetric top/bottom AND left/right)
561
+ const step = thickness + spacing;
562
+ const maxBars = Math.floor((width / 2) / step);
563
+ const usefulLength = Math.floor(bandLength * 0.75);
564
+ const numBars = Math.min(maxBars, usefulLength);
565
+
566
+ for (let i = 0; i < numBars; i++) {
567
+ const dataIndex = startIndex + Math.floor((i / numBars) * usefulLength);
568
+ const boost = Math.pow(1 + (i / numBars), 1.5);
569
+
570
+ let edgeMultiplier = 1;
571
+ if (taperEdges) edgeMultiplier = Math.pow(1 - (i / numBars), 2);
572
+
573
+ const value = dataArray[dataIndex] * boost * sensitivity * edgeMultiplier;
574
+ const barHeight = Math.max(thickness / 2, (value / 255) * height * 0.4);
575
+ const xOffset = i * step + (step / 2);
576
+
577
+ if (isFilled) {
578
+ ctx.rect(xOffset - thickness/2, -barHeight, thickness, barHeight * 2);
579
+ ctx.rect(-xOffset - thickness/2, -barHeight, thickness, barHeight * 2);
580
+ } else {
581
+ ctx.moveTo(xOffset, -barHeight); ctx.lineTo(xOffset, barHeight);
582
+ ctx.moveTo(-xOffset, -barHeight); ctx.lineTo(-xOffset, barHeight);
583
+ }
584
+ }
585
+ } else if (vizType === 'wave') {
586
+ const sliceWidth = width / bandLength;
587
+ let x = -width / 2;
588
+ if (isFilled) ctx.moveTo(-width/2, height/2);
589
+
590
+ for (let i = 0; i < bandLength; i++) {
591
+ const dataIndex = startIndex + i;
592
+ const normalized = (dataArray[dataIndex] / 128.0) - 1;
593
+
594
+ let edgeMultiplier = 1;
595
+ if (taperEdges) edgeMultiplier = Math.pow(1 - (Math.abs(i - bandLength/2) / (bandLength/2)), 2);
596
+
597
+ const y = normalized * sensitivity * (height / 2) * edgeMultiplier;
598
+
599
+ if (i === 0 && !isFilled) ctx.moveTo(x, y);
600
+ else ctx.lineTo(x, y);
601
+ x += sliceWidth;
602
+ }
603
+ if (isFilled) { ctx.lineTo(width/2, height/2); ctx.closePath(); }
604
+ } else if (vizType === 'circle') {
605
+ const radius = height / 4;
606
+ const circumference = 2 * Math.PI * radius;
607
+ const stepSize = thickness + spacing;
608
+ const bars = Math.min(180, Math.floor(circumference / stepSize));
609
+ const step = (Math.PI * 2) / bars;
610
+
611
+ for (let i = 0; i < bars; i++) {
612
+ const dataIndex = startIndex + Math.floor((i / bars) * bandLength);
613
+ const value = (dataArray[dataIndex] / 255) * sensitivity;
614
+ const barHeight = Math.max(thickness / 2, value * (height / 3));
615
+ const angle = i * step;
616
+
617
+ const x1 = Math.cos(angle) * radius; const y1 = Math.sin(angle) * radius;
618
+ const x2 = Math.cos(angle) * (radius + barHeight); const y2 = Math.sin(angle) * (radius + barHeight);
619
+ ctx.moveTo(x1, y1); ctx.lineTo(x2, y2);
620
+ }
621
+ }
622
+
623
+ if (isFilled && vizType !== 'circle') ctx.fill(); else ctx.stroke();
624
+
625
+ if (vizType === 'circle' && !isFilled) {
626
+ const radius = height / 4;
627
+ ctx.beginPath();
628
+ ctx.arc(0, 0, radius - thickness, 0, Math.PI * 2);
629
+ const originalWidth = ctx.lineWidth; ctx.lineWidth = originalWidth / 2; ctx.stroke(); ctx.lineWidth = originalWidth;
630
+ }
631
+ };
632
+
633
+ // Apply mirroring
634
+ buildShapes();
635
+ if (mirrorX) { ctx.save(); ctx.scale(-1, 1); buildShapes(); ctx.restore(); }
636
+ if (mirrorY) { ctx.save(); ctx.scale(1, -1); buildShapes(); ctx.restore(); }
637
+ if (mirrorX && mirrorY) { ctx.save(); ctx.scale(-1, -1); buildShapes(); ctx.restore(); }
638
+ };
639
+
640
+ // Glow logic
641
+ if (glow) {
642
+ ctx.globalCompositeOperation = 'lighter';
643
+ ctx.lineWidth = thickness * 3; ctx.globalAlpha = 0.15; drawVisualizerPath();
644
+ ctx.lineWidth = thickness * 1.5; ctx.globalAlpha = 0.4; drawVisualizerPath();
645
+ ctx.lineWidth = thickness; ctx.globalAlpha = 1.0; drawVisualizerPath();
646
+ ctx.globalCompositeOperation = 'source-over';
647
+ } else {
648
+ ctx.lineWidth = thickness; ctx.globalAlpha = 1.0; drawVisualizerPath();
649
+ }
650
+ ctx.restore();
651
+ }
652
+
653
+ // Overlay Text
654
+ if (overlayText1 || overlayText2) {
655
+ ctx.save(); ctx.fillStyle = overlayTextColor; ctx.textAlign = 'center';
656
+ const scaleFactor = res.w / 3840;
657
+ const fSize1 = overlayTextSize * scaleFactor * 3;
658
+ const fSize2 = fSize1 * 0.6;
659
+
660
+ let startY = res.h / 2;
661
+ if (overlayTextPos === 'top') startY = res.h * 0.15;
662
+ if (overlayTextPos === 'bottom') startY = res.h * 0.85;
663
+
664
+ ctx.shadowColor = 'rgba(0,0,0,0.8)'; ctx.shadowBlur = 15 * scaleFactor;
665
+ ctx.shadowOffsetX = 3 * scaleFactor; ctx.shadowOffsetY = 3 * scaleFactor;
666
+
667
+ if (overlayText1) { ctx.font = `bold ${fSize1}px sans-serif`; ctx.fillText(overlayText1, res.w / 2, startY); }
668
+ if (overlayText2) { ctx.font = `600 ${fSize2}px sans-serif`; ctx.fillText(overlayText2, res.w / 2, startY + fSize1 * 1.2); }
669
+ ctx.restore();
670
+ }
671
+
672
+ reqIdRef.current = requestAnimationFrame(draw);
673
+ }, [vizType, color, thickness, spacing, sensitivity, smoothing, colorMode, color2, glow, taperEdges, resolution, bgType, bgColor, bgImageFit, scale, scaleX, scaleY, rotation, isExportingVideo, exportFps, mirrorX, mirrorY, isFilled, bgBeatReactive, freqBand, flashOnBeat, flashColor, flashThreshold, overlayText1, overlayText2, overlayTextSize, overlayTextColor, overlayTextPos]);
674
+
675
+ useEffect(() => {
676
+ reqIdRef.current = requestAnimationFrame(draw);
677
+ return () => cancelAnimationFrame(reqIdRef.current);
678
+ }, [draw]);
679
+
680
+ const handleAudioEnded = function() {
681
+ setIsPlaying(false);
682
+ if (isExportingVideo) stopVideoExport();
683
+ };
684
+
685
+ const exportImage = function() {
686
+ if (!canvasRef.current) return;
687
+ const link = document.createElement('a');
688
+ link.download = `visualizer_${Date.now()}.png`;
689
+ link.href = canvasRef.current.toDataURL('image/png');
690
+ link.click();
691
+ };
692
+
693
+ const startVideoExport = async function() {
694
+ if (!audioSrc || !canvasRef.current || !audioCtxRef.current) {
695
+ alert("Please upload an audio file and press play at least once to initialize.");
696
+ return;
697
+ }
698
+ setIsExportingVideo(true); setExportProgress(0); chunksRef.current = [];
699
+ audioRef.current.pause();
700
+ audioRef.current.currentTime = exportStart > 0 ? exportStart : 0;
701
+
702
+ const canvasStream = canvasRef.current.captureStream(exportFps);
703
+ const audioStream = destRef.current.stream;
704
+ const combinedStream = new MediaStream([...canvasStream.getTracks(), ...audioStream.getAudioTracks()]);
705
+
706
+ let options = {}; let ext = 'webm';
707
+ const targetBitrate = resolution.startsWith('4k') ? 15000000 : 8000000;
708
+
709
+ if (exportFormat === 'mp4') {
710
+ if (MediaRecorder.isTypeSupported('video/mp4; codecs=h264')) { options = { mimeType: 'video/mp4; codecs=h264', videoBitsPerSecond: targetBitrate }; ext = 'mp4'; }
711
+ else if (MediaRecorder.isTypeSupported('video/mp4')) { options = { mimeType: 'video/mp4', videoBitsPerSecond: targetBitrate }; ext = 'mp4'; }
712
+ else { alert("Browser doesn't support MP4. Falling back to WebM."); options = { mimeType: 'video/webm; codecs=vp9', videoBitsPerSecond: targetBitrate }; }
713
+ } else {
714
+ options = { mimeType: 'video/webm; codecs=vp9', videoBitsPerSecond: targetBitrate };
715
+ if (!MediaRecorder.isTypeSupported(options.mimeType)) options = { mimeType: 'video/webm; codecs=vp8', videoBitsPerSecond: targetBitrate };
716
+ if (!MediaRecorder.isTypeSupported(options.mimeType)) options = { videoBitsPerSecond: targetBitrate };
717
+ }
718
+
719
+ try { mediaRecorderRef.current = new MediaRecorder(combinedStream, options); } catch (e) { alert("Recorder error."); setIsExportingVideo(false); return; }
720
+ mediaRecorderRef.current.ondataavailable = function(e) { if (e.data && e.data.size > 0) chunksRef.current.push(e.data); };
721
+ mediaRecorderRef.current.onstop = function() {
722
+ const blob = new Blob(chunksRef.current, { type: mediaRecorderRef.current.mimeType || 'video/mp4' });
723
+ const url = URL.createObjectURL(blob);
724
+ const link = document.createElement('a'); link.download = `viz_${Date.now()}.${ext}`; link.href = url; link.click(); URL.revokeObjectURL(url);
725
+ setIsExportingVideo(false); setExportProgress(0);
726
+ };
727
+
728
+ const duration = audioRef.current.duration;
729
+ const progressInterval = setInterval(function() {
730
+ if (audioRef.current && !audioRef.current.paused) {
731
+ const current = audioRef.current.currentTime;
732
+ const end = (exportEnd > 0 && exportEnd < duration) ? exportEnd : duration;
733
+ const start = exportStart > 0 ? exportStart : 0;
734
+ setExportProgress(Math.min(100, Math.max(0, ((current - start) / (end - start)) * 100)));
735
+ if (current >= end) stopVideoExport();
736
+ } else { clearInterval(progressInterval); }
737
+ }, 250);
738
+
739
+ mediaRecorderRef.current.start(1000);
740
+ await audioRef.current.play(); setIsPlaying(true);
741
+ };
742
+
743
+ const stopVideoExport = function() {
744
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') mediaRecorderRef.current.stop();
745
+ audioRef.current.pause(); setIsPlaying(false);
746
+ };
747
+
748
+ return (
749
+ <div className="min-h-screen bg-slate-950 text-slate-200 font-sans selection:bg-cyan-500/30">
750
+ <header className="border-b border-slate-800 bg-slate-900/50 p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
751
+ <div className="flex items-center gap-3">
752
+ <div className="bg-cyan-500/20 p-2 rounded-lg">
753
+ <Video className="w-6 h-6 text-cyan-400" />
754
+ </div>
755
+ <h1 className="text-xl font-bold tracking-tight text-white">4K Transparent Visualizer</h1>
756
+ </div>
757
+ <div className="flex items-center gap-3">
758
+ <button onClick={handleResetAll} className="flex items-center gap-2 bg-slate-800 hover:bg-slate-700 border border-slate-700 px-3 py-1.5 rounded-lg text-sm text-slate-300 font-medium transition-colors" title="Reset all settings to default">
759
+ <RefreshCw className="w-4 h-4" /> Reset All
760
+ </button>
761
+ </div>
762
+ </header>
763
+
764
+ <main className="w-full max-w-[1800px] mx-auto p-4 sm:p-6 flex flex-col lg:grid lg:grid-cols-12 gap-6 lg:gap-8">
765
+
766
+ {/* --- LEFT SIDEBAR (Settings) --- */}
767
+ <div className="order-3 lg:order-1 lg:col-span-3 space-y-6 overflow-y-auto pr-2 custom-scrollbar" style={{maxHeight: 'calc(100vh - 8rem)'}}>
768
+
769
+ <section className="bg-slate-900 p-5 sm:p-6 rounded-2xl border border-slate-800 shadow-xl transition-all">
770
+ <SectionHeader title="Visual Settings" icon={Settings2} sectionKey="visual" />
771
+ {!minStates.visual && (
772
+ <div className="space-y-5 animate-in fade-in slide-in-from-top-2 duration-200">
773
+
774
+ <div>
775
+ <ControlHeader label="Style & Fill"
776
+ extra={
777
+ <label className="flex items-center gap-2 cursor-pointer group ml-4">
778
+ <span className="text-xs font-medium text-slate-500 group-hover:text-cyan-400 transition-colors">Fill Shapes</span>
779
+ <div className={`w-8 h-4 rounded-full transition-colors relative ${isFilled ? 'bg-cyan-500' : 'bg-slate-700'}`}>
780
+ <div className={`w-3 h-3 bg-white rounded-full absolute top-0.5 transition-transform ${isFilled ? 'left-4.5 translate-x-[14px]' : 'left-0.5'}`}></div>
781
+ </div>
782
+ <input type="checkbox" className="hidden" checked={isFilled} onChange={function(e) { setIsFilled(e.target.checked); }} />
783
+ </label>
784
+ }
785
+ />
786
+ <div className="grid grid-cols-2 gap-2">
787
+ {[ {id: 'bars', label: 'Bars (Freq)'}, {id: 'symmetric_wave', label: 'Wave (Bars)'}, {id: 'wave', label: 'Wave (Line)'}, {id: 'circle', label: 'Circle'} ].map(function(type) {
788
+ return <button key={type.id} onClick={function() { setVizType(type.id); }} className={`py-2 px-3 rounded-lg text-sm font-medium transition-all ${vizType === type.id ? 'bg-slate-700 text-white shadow-inner border border-slate-600' : 'bg-slate-950 text-slate-400 border border-slate-800 hover:border-slate-600'}`}>{type.label}</button>;
789
+ })}
790
+ </div>
791
+ </div>
792
+
793
+ <div>
794
+ <ControlHeader label="Frequency Band Isolation" />
795
+ <select value={freqBand} onChange={function(e) { setFreqBand(e.target.value); }} className="w-full bg-slate-950 border border-slate-700 text-slate-300 text-sm rounded-lg px-3 py-2 outline-none">
796
+ <option value="all">All Frequencies (Default)</option>
797
+ <option value="bass">Bass Only (Kick/Sub)</option>
798
+ <option value="mid">Mids Only (Vocals/Melody)</option>
799
+ <option value="treble">Treble Only (Hi-Hats)</option>
800
+ </select>
801
+ </div>
802
+
803
+ <div className="p-3 bg-slate-950 rounded-xl border border-slate-800 space-y-3">
804
+ <ControlHeader label="Color Style" />
805
+ <div className="flex items-center gap-3 mb-4">
806
+ <select value={colorMode} onChange={function(e) { setColorMode(e.target.value); }} className="bg-slate-900 border border-slate-700 text-slate-300 text-xs rounded px-2 py-2 outline-none">
807
+ <option value="solid">Solid</option><option value="gradient">Gradient</option><option value="rainbow">Rainbow</option>
808
+ </select>
809
+ {colorMode !== 'rainbow' && <input type="color" value={color} onChange={function(e) { setColor(e.target.value); }} className="h-8 w-12 rounded cursor-pointer bg-slate-900 border border-slate-700 shrink-0" />}
810
+ {colorMode === 'gradient' && <><span className="text-slate-500 text-xs font-medium">to</span><input type="color" value={color2} onChange={function(e) { setColor2(e.target.value); }} className="h-8 w-12 rounded cursor-pointer bg-slate-900 border border-slate-700 shrink-0" /></>}
811
+ {colorMode === 'solid' && <input type="text" value={color} onChange={function(e) { setColor(e.target.value); }} className="flex-1 bg-slate-900 border border-slate-800 rounded-lg px-2 py-1.5 text-sm focus:ring-1 focus:ring-cyan-500 outline-none uppercase font-mono" />}
812
+ </div>
813
+
814
+ {/* Beat Flashes */}
815
+ <div className="pt-3 border-t border-slate-800">
816
+ <ControlHeader label="Beat Flash"
817
+ extra={<Zap className="w-4 h-4 text-yellow-400 ml-2" />}
818
+ />
819
+ <div className="flex items-center justify-between mb-2">
820
+ <span className="text-xs text-slate-500">Enable rhythmic flashing on heavy bass</span>
821
+ <label className="relative inline-flex items-center cursor-pointer">
822
+ <input type="checkbox" checked={flashOnBeat} onChange={function(e) { setFlashOnBeat(e.target.checked); }} className="sr-only peer" />
823
+ <div className="w-9 h-5 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-4 after:w-4 after:transition-all peer-checked:bg-yellow-500"></div>
824
+ </label>
825
+ </div>
826
+ {flashOnBeat && (
827
+ <div className="flex items-center gap-3 animate-in fade-in slide-in-from-top-2 mt-3 bg-slate-900 p-2 rounded-lg">
828
+ <input type="color" value={flashColor} onChange={function(e) { setFlashColor(e.target.value); }} className="h-8 w-10 rounded cursor-pointer bg-slate-950 border border-slate-700 shrink-0" title="Flash Color" />
829
+ <div className="flex-1">
830
+ <div className="flex justify-between items-center text-xs text-slate-500 mb-1">
831
+ <span>Sensitivity</span>
832
+ <button onClick={function() { setFlashThreshold(230); }} className="hover:text-cyan-400 transition-colors focus:outline-none" title="Reset Sensitivity"><RotateCcw className="w-3 h-3" /></button>
833
+ </div>
834
+ <input type="range" min="150" max="250" value={250 - (flashThreshold - 150)} onChange={function(e) { setFlashThreshold(250 - (Number(e.target.value) - 150)); }} className="w-full accent-yellow-500" />
835
+ </div>
836
+ </div>
837
+ )}
838
+ </div>
839
+ </div>
840
+
841
+ <div className="space-y-4">
842
+ <div>
843
+ <ControlHeader label="Neon Glow Effect" extra={<Sparkles className="w-4 h-4 text-amber-400 ml-2" />} />
844
+ <label className="relative inline-flex items-center cursor-pointer mt-1">
845
+ <input type="checkbox" checked={glow} onChange={function(e) { setGlow(e.target.checked); }} className="sr-only peer" />
846
+ <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>
847
+ </label>
848
+ </div>
849
+
850
+ <div>
851
+ <ControlHeader label="Taper Edges (Minimal Spikes)" extra={<Activity className="w-4 h-4 text-emerald-400 ml-2" />} />
852
+ <label className="relative inline-flex items-center cursor-pointer mt-1">
853
+ <input type="checkbox" checked={taperEdges} onChange={function(e) { setTaperEdges(e.target.checked); }} className="sr-only peer" />
854
+ <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-emerald-500"></div>
855
+ </label>
856
+ </div>
857
+ </div>
858
+
859
+ <div>
860
+ <ControlHeader label="Line Thickness" valueDisplay={`${thickness}px`} onReset={function() { setThickness(12); }} />
861
+ <input type="range" min="2" max="64" value={thickness} onChange={function(e) { setThickness(Number(e.target.value)); }} className="w-full" />
862
+ </div>
863
+ <div>
864
+ <ControlHeader label="Space Between Lines" valueDisplay={`${spacing}px`} onReset={function() { setSpacing(8); }} />
865
+ <input type="range" min="0" max="64" value={spacing} onChange={function(e) { setSpacing(Number(e.target.value)); }} className="w-full" />
866
+ </div>
867
+ <div>
868
+ <ControlHeader label="Amplitude (Height)" valueDisplay={`${sensitivity.toFixed(1)}x`} onReset={function() { setSensitivity(1.5); }} />
869
+ <input type="range" min="0.5" max="3.0" step="0.1" value={sensitivity} onChange={function(e) { setSensitivity(Number(e.target.value)); }} className="w-full" />
870
+ </div>
871
+ <div>
872
+ <ControlHeader label="Motion Smoothing" valueDisplay={`${Math.round(smoothing * 100)}%`} onReset={function() { setSmoothing(0.85); }} />
873
+ <input type="range" min="0.1" max="0.99" step="0.01" value={smoothing} onChange={function(e) { setSmoothing(Number(e.target.value)); }} className="w-full" />
874
+ </div>
875
+ </div>
876
+ )}
877
+ </section>
878
+
879
+ <section className="bg-slate-900 p-5 sm:p-6 rounded-2xl border border-slate-800 shadow-xl transition-all">
880
+ <SectionHeader title="Text Overlay" icon={Type} sectionKey="text" />
881
+ {!minStates.text && (
882
+ <div className="space-y-4 animate-in fade-in slide-in-from-top-2 duration-200">
883
+ <ControlHeader label="Custom Text Lines" />
884
+ <input type="text" placeholder="Artist Name (Line 1)" value={overlayText1} onChange={function(e) { setOverlayText1(e.target.value); }} 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" />
885
+ <input type="text" placeholder="Track Title (Line 2)" value={overlayText2} onChange={function(e) { setOverlayText2(e.target.value); }} 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" />
886
+
887
+ <div className="flex items-center gap-3">
888
+ <input type="color" value={overlayTextColor} onChange={function(e) { setOverlayTextColor(e.target.value); }} className="h-10 w-12 rounded cursor-pointer bg-slate-950 border border-slate-700 shrink-0" />
889
+ <select value={overlayTextPos} onChange={function(e) { setOverlayTextPos(e.target.value); }} className="flex-1 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">
890
+ <option value="top">Top Centered</option>
891
+ <option value="center">Middle Centered</option>
892
+ <option value="bottom">Bottom Centered</option>
893
+ </select>
894
+ </div>
895
+ </div>
896
+ )}
897
+ </section>
898
+
899
+ <section className="bg-slate-900 p-5 sm:p-6 rounded-2xl border border-slate-800 shadow-xl transition-all">
900
+ <SectionHeader title="Save & Export" icon={Save} sectionKey="export" />
901
+
902
+ {!minStates.export && (
903
+ <div className="animate-in fade-in slide-in-from-top-2 duration-200">
904
+ <div className="flex flex-col gap-3 mb-6">
905
+ <label className="block text-sm font-medium text-slate-400 mb-1">Local Configuration</label>
906
+ <div className="flex gap-2">
907
+ <button onClick={savePreset} className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 py-2 rounded-lg text-sm font-medium border border-slate-700 transition-colors">Save Preset</button>
908
+ <button onClick={loadPreset} className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 py-2 rounded-lg text-sm font-medium border border-slate-700 transition-colors">Load Preset</button>
909
+ </div>
910
+ </div>
911
+
912
+ <div className="flex flex-col gap-3">
913
+ <div className="bg-slate-950 p-4 rounded-xl border border-slate-800 mb-2">
914
+ <ControlHeader label="Video Format & Framerate" />
915
+ <div className="flex flex-col 2xl:flex-row gap-3">
916
+ <select value={exportFormat} onChange={function(e) { setExportFormat(e.target.value); }} 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">
917
+ <option value="webm">WebM (Transparent)</option>
918
+ <option value="mp4">MP4 (Solid BG / DaVinci)</option>
919
+ </select>
920
+ <select value={exportFps} onChange={function(e) { setExportFps(Number(e.target.value)); }} className="w-full 2xl:w-32 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">
921
+ <option value={30}>30 FPS</option>
922
+ <option value={60}>60 FPS</option>
923
+ </select>
924
+ </div>
925
+
926
+ <div className="mt-4 pt-4 border-t border-slate-800">
927
+ <ControlHeader label="Time Range (Clipping)" extra={<Scissors className="w-4 h-4 ml-2 text-slate-500" />} />
928
+ <div className="flex gap-3 items-center">
929
+ <div className="flex-1">
930
+ <span className="text-xs text-slate-500 block mb-1">Start (Seconds)</span>
931
+ <input type="number" min="0" value={exportStart} onChange={function(e) { setExportStart(Number(e.target.value)); }} className="w-full bg-slate-900 border border-slate-700 text-slate-300 text-sm rounded-lg px-2 py-2 outline-none" />
932
+ </div>
933
+ <span className="text-slate-600 mt-5">to</span>
934
+ <div className="flex-1">
935
+ <span className="text-xs text-slate-500 block mb-1">End (0 = Full Track)</span>
936
+ <input type="number" min="0" value={exportEnd} onChange={function(e) { setExportEnd(Number(e.target.value)); }} className="w-full bg-slate-900 border border-slate-700 text-slate-300 text-sm rounded-lg px-2 py-2 outline-none" />
937
+ </div>
938
+ </div>
939
+ </div>
940
+
941
+ <div className="mt-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
942
+ <p className="text-xs text-red-400 leading-relaxed">
943
+ <strong>Video Freezing/Crashing?</strong> Real-time encoding is heavy. To fix this:<br/>
944
+ 1. Set Framerate to <strong>30 FPS</strong>.<br/>
945
+ 2. Turn off <strong>Neon Glow Effect</strong>.<br/>
946
+ 3. Lower Resolution to <strong>1080p</strong>.
947
+ </p>
948
+ </div>
949
+ </div>
950
+
951
+ <button onClick={exportImage} disabled={isExportingVideo} 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">
952
+ <ImageIcon className="w-4 h-4" /> Save Snapshot (PNG)
953
+ </button>
954
+
955
+ {isExportingVideo ? (
956
+ <div className="w-full space-y-3 mt-2">
957
+ <button onClick={stopVideoExport} 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">
958
+ <StopCircle className="w-5 h-5" /> Stop & Save Early
959
+ </button>
960
+ <div className="w-full bg-slate-950 rounded-full h-2.5 border border-slate-800 overflow-hidden">
961
+ <div className="bg-cyan-500 h-2.5 rounded-full transition-all duration-300" style={{ width: `${exportProgress}%` }}></div>
962
+ </div>
963
+ <p className="text-xs text-center text-slate-400">Recording video... {Math.round(exportProgress)}%</p>
964
+ </div>
965
+ ) : (
966
+ <button onClick={startVideoExport} disabled={!audioSrc} 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 mt-2">
967
+ <Video className="w-5 h-5" /> Export Video ({exportFormat.toUpperCase()})
968
+ </button>
969
+ )}
970
+ </div>
971
+ </div>
972
+ )}
973
+ </section>
974
+
975
+ </div>
976
+
977
+ {/* --- CENTER CANVAS PREVIEW SECTION --- */}
978
+ <div className="order-2 lg:order-2 lg:col-span-6 flex flex-col gap-4 lg:sticky lg:top-6 lg:h-[calc(100vh-3rem)]">
979
+ <div className="bg-slate-900 rounded-2xl border border-slate-800 shadow-xl overflow-hidden flex-1 relative flex flex-col min-h-0">
980
+
981
+ <div className="p-4 border-b border-slate-800 bg-slate-900/80 flex flex-wrap justify-between items-center z-10 shrink-0 gap-3">
982
+ <span className="text-sm font-semibold text-slate-300 flex items-center gap-2">
983
+ Live Preview
984
+ <span className="bg-slate-800 text-xs px-2 py-0.5 rounded text-slate-400 border border-slate-700">
985
+ {RESOLUTIONS[resolution]?.w}x{RESOLUTIONS[resolution]?.h}
986
+ </span>
987
+ </span>
988
+ <div className="flex items-center gap-3">
989
+ <span className="text-xs text-slate-500 font-medium">Safe Zone:</span>
990
+ <select
991
+ value={safeZone}
992
+ onChange={function(e) { setSafeZone(e.target.value); }}
993
+ className="bg-slate-950 border border-slate-700 text-slate-300 text-xs rounded px-2 py-1 outline-none cursor-pointer"
994
+ >
995
+ <option value="none">Off</option>
996
+ <option value="1:1">1:1 (Square)</option>
997
+ <option value="4:5">4:5 (Portrait)</option>
998
+ <option value="9:16">9:16 (Vertical)</option>
999
+ <option value="16:9">16:9 (Landscape)</option>
1000
+ <option value="title_safe">Title Safe (10%)</option>
1001
+ </select>
1002
+ </div>
1003
+ </div>
1004
+
1005
+ <div className="flex-1 w-full relative flex items-center justify-center p-4 sm:p-6 overflow-hidden bg-black/50 min-h-0"
1006
+ style={ bgType === 'transparent' ? { 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)', backgroundPosition: '0 0, 10px 10px', backgroundSize: '20px 20px' } : {}}
1007
+ >
1008
+ <div className="relative flex items-center justify-center shadow-2xl ring-1 ring-white/10"
1009
+ style={{
1010
+ aspectRatio: `${RESOLUTIONS[resolution]?.w} / ${RESOLUTIONS[resolution]?.h}`,
1011
+ maxHeight: '100%',
1012
+ maxWidth: '100%'
1013
+ }}>
1014
+
1015
+ <canvas
1016
+ key={bgType === 'transparent' ? 'alpha' : 'solid'}
1017
+ ref={canvasRef}
1018
+ width={RESOLUTIONS[resolution]?.w || 3840}
1019
+ height={RESOLUTIONS[resolution]?.h || 2160}
1020
+ onMouseDown={handleMouseDown}
1021
+ onMouseMove={handleMouseMove}
1022
+ onMouseUp={handleMouseUpOrLeave}
1023
+ onMouseLeave={handleMouseUpOrLeave}
1024
+ className={`w-full h-full bg-transparent ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
1025
+ />
1026
+
1027
+ {/* Pixel Perfect Bound Safe Zones */}
1028
+ {safeZone !== 'none' && (
1029
+ <div
1030
+ className="absolute pointer-events-none border-[3px] border-dashed border-red-500/80 bg-red-500/10 flex items-center justify-center z-30"
1031
+ style={getSafeZoneStyle(safeZone, RESOLUTIONS[resolution]?.w || 3840, RESOLUTIONS[resolution]?.h || 2160)}
1032
+ >
1033
+ <span className="absolute top-2 left-2 text-[10px] font-bold text-red-300 bg-slate-900/90 px-2 py-1 rounded border border-red-500/30 shadow-lg">
1034
+ {safeZone === 'title_safe' ? 'Title' : safeZone} Safe Area
1035
+ </span>
1036
+ <div className="w-4 h-[1px] bg-red-500/50 absolute"></div>
1037
+ <div className="h-4 w-[1px] bg-red-500/50 absolute"></div>
1038
+ </div>
1039
+ )}
1040
+ </div>
1041
+
1042
+ {!audioSrc && (
1043
+ <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none bg-slate-900/80 backdrop-blur-sm z-20">
1044
+ <Loader2 className="w-12 h-12 text-slate-500 animate-spin mb-4 opacity-50" />
1045
+ <p className="text-slate-400 font-medium">Awaiting Audio Input</p>
1046
+ </div>
1047
+ )}
1048
+ </div>
1049
+ </div>
1050
+
1051
+ {/* Playback Controls */}
1052
+ <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">
1053
+ <div className="flex items-center gap-4">
1054
+ <button ref={playButtonRef} onClick={togglePlay} disabled={!audioSrc || isExportingVideo} 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">
1055
+ {isPlaying ? <Pause className="w-6 h-6 fill-current" /> : <Play className="w-6 h-6 fill-current ml-1" />}
1056
+ </button>
1057
+ <div className="flex-1 flex flex-col gap-2">
1058
+ <div className="flex justify-between text-xs font-mono text-slate-400">
1059
+ <span>{formatTime(audioTime)}</span>
1060
+ <span className="text-slate-300 font-sans truncate px-4">{fileName || 'No audio selected'}</span>
1061
+ <span>{formatTime(audioDuration)}</span>
1062
+ </div>
1063
+ <input type="range" min="0" max={audioDuration || 100} value={audioTime} onChange={handleSeek} disabled={!audioSrc || isExportingVideo} className="w-full" />
1064
+ </div>
1065
+ </div>
1066
+ </div>
1067
+
1068
+ </div>
1069
+
1070
+ {/* --- RIGHT SIDEBAR (Input & Setup) --- */}
1071
+ <div className="order-1 lg:order-3 lg:col-span-3 space-y-6 overflow-y-auto pr-2 custom-scrollbar" style={{maxHeight: 'calc(100vh - 8rem)'}}>
1072
+
1073
+ <section className="bg-slate-900 p-5 sm:p-6 rounded-2xl border border-slate-800 shadow-xl transition-all">
1074
+ <SectionHeader title="Audio Input" icon={Upload} sectionKey="audio" />
1075
+ {!minStates.audio && (
1076
+ <div className="animate-in fade-in slide-in-from-top-2 duration-200">
1077
+ <ControlHeader label="Upload File" />
1078
+ <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-6 sm:p-8 text-center group">
1079
+ <input type="file" accept="audio/*" onChange={handleFileUpload} className="hidden" disabled={isExportingVideo} />
1080
+ <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">
1081
+ <Upload className="w-6 h-6 text-cyan-400" />
1082
+ </div>
1083
+ <p className="font-medium text-slate-300">{fileName ? fileName : 'Click to browse audio file'}</p>
1084
+ <p className="text-xs text-slate-500 mt-2">MP3, WAV, FLAC</p>
1085
+ </label>
1086
+ <audio ref={audioRef} src={audioSrc} onEnded={handleAudioEnded} onPlay={function() { setIsPlaying(true); }} onPause={function() { setIsPlaying(false); }} onTimeUpdate={handleTimeUpdate} onLoadedMetadata={handleLoadedMetadata} />
1087
+ </div>
1088
+ )}
1089
+ </section>
1090
+
1091
+ <section className="bg-slate-900 p-5 sm:p-6 rounded-2xl border border-slate-800 shadow-xl transition-all">
1092
+ <SectionHeader title="Transform & Symmetry" icon={RotateCcw} sectionKey="transform" />
1093
+ {!minStates.transform && (
1094
+ <div className="space-y-5 animate-in fade-in slide-in-from-top-2 duration-200">
1095
+ <div>
1096
+ <ControlHeader label="Symmetry / Mirror" />
1097
+ <div className="grid grid-cols-2 gap-3 mt-2">
1098
+ <label className={`flex items-center justify-center gap-2 py-2 px-3 rounded-lg border cursor-pointer transition-colors ${mirrorX ? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-300' : 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-600'}`}>
1099
+ <input type="checkbox" className="hidden" checked={mirrorX} onChange={function(e){setMirrorX(e.target.checked);}} />
1100
+ <FlipHorizontal className="w-4 h-4" /> <span className="text-sm font-medium">Mirror X</span>
1101
+ </label>
1102
+ <label className={`flex items-center justify-center gap-2 py-2 px-3 rounded-lg border cursor-pointer transition-colors ${mirrorY ? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-300' : 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-600'}`}>
1103
+ <input type="checkbox" className="hidden" checked={mirrorY} onChange={function(e){setMirrorY(e.target.checked);}} />
1104
+ <FlipVertical className="w-4 h-4" /> <span className="text-sm font-medium">Mirror Y</span>
1105
+ </label>
1106
+ </div>
1107
+ </div>
1108
+
1109
+ <div>
1110
+ <ControlHeader label="Size (Global Scale)" valueDisplay={`${scale.toFixed(2)}x`} onReset={function() { setScale(1.0); }} />
1111
+ <input type="range" min="0.1" max="3.0" step="0.1" value={scale} onChange={function(e) { setScale(Number(e.target.value)); }} className="w-full" />
1112
+ </div>
1113
+ <div>
1114
+ <ControlHeader label="Horizontal Size (Width)" valueDisplay={`${scaleX.toFixed(2)}x`} onReset={function() { handleScaleXChange(1.0); if(scaleLock) handleScaleYChange(1.0); }}
1115
+ extra={
1116
+ <button onClick={toggleScaleLock} title={scaleLock ? "Unlock Aspect Ratio" : "Lock Aspect Ratio"} className={`p-1 ml-2 rounded transition-colors ${scaleLock ? 'text-cyan-400 bg-cyan-400/10' : 'text-slate-500 hover:text-cyan-400'}`}>
1117
+ {scaleLock ? <Lock className="w-3 h-3" /> : <Unlock className="w-3 h-3" />}
1118
+ </button>
1119
+ }
1120
+ />
1121
+ <input type="range" min="0.1" max="5.0" step="0.1" value={scaleX} onChange={function(e) { handleScaleXChange(Number(e.target.value)); }} className="w-full" />
1122
+ </div>
1123
+ <div>
1124
+ <ControlHeader label="Vertical Size (Height)" valueDisplay={`${scaleY.toFixed(2)}x`} onReset={function() { handleScaleYChange(1.0); if(scaleLock) handleScaleXChange(1.0); }}
1125
+ extra={
1126
+ <button onClick={toggleScaleLock} title={scaleLock ? "Unlock Aspect Ratio" : "Lock Aspect Ratio"} className={`p-1 ml-2 rounded transition-colors ${scaleLock ? 'text-cyan-400 bg-cyan-400/10' : 'text-slate-500 hover:text-cyan-400'}`}>
1127
+ {scaleLock ? <Lock className="w-3 h-3" /> : <Unlock className="w-3 h-3" />}
1128
+ </button>
1129
+ }
1130
+ />
1131
+ <input type="range" min="0.1" max="5.0" step="0.1" value={scaleY} onChange={function(e) { handleScaleYChange(Number(e.target.value)); }} className="w-full" />
1132
+ </div>
1133
+ <div>
1134
+ <ControlHeader label="Rotation" valueDisplay={`${rotation}°`} onReset={function() { setRotation(0); }} />
1135
+ <input type="range" min="0" max="360" step="1" value={rotation} onChange={function(e) { setRotation(Number(e.target.value)); }} className="w-full" />
1136
+ </div>
1137
+ <div>
1138
+ <ControlHeader label="Horizontal Position" valueDisplay={`${Math.round(offsetX)}%`} onReset={function() { setOffsetX(0); offsetRef.current.x = 0; }} />
1139
+ <input type="range" min="-50" max="50" step="1" value={offsetX} onChange={function(e) { const val = Number(e.target.value); setOffsetX(val); offsetRef.current.x = val; }} className="w-full" />
1140
+ </div>
1141
+ <div>
1142
+ <ControlHeader label="Vertical Position" valueDisplay={`${Math.round(offsetY)}%`} onReset={function() { setOffsetY(0); offsetRef.current.y = 0; }} />
1143
+ <input type="range" min="-50" max="50" step="1" value={offsetY} onChange={function(e) { const val = Number(e.target.value); setOffsetY(val); offsetRef.current.y = val; }} className="w-full" />
1144
+ </div>
1145
+ </div>
1146
+ )}
1147
+ </section>
1148
+
1149
+ <section className="bg-slate-900 p-5 sm:p-6 rounded-2xl border border-slate-800 shadow-xl transition-all">
1150
+ <SectionHeader title="Output Setup" icon={Monitor} sectionKey="output" />
1151
+ {!minStates.output && (
1152
+ <div className="space-y-5 animate-in fade-in slide-in-from-top-2 duration-200">
1153
+ <div>
1154
+ <ControlHeader label="Resolution & Ratio" />
1155
+ <select value={resolution} onChange={function(e) { setResolution(e.target.value); }} disabled={isExportingVideo} 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 disabled:opacity-50 disabled:cursor-not-allowed">
1156
+ <option value="4k_16_9">4K Landscape (3840x2160)</option>
1157
+ <option value="1080p_16_9">1080p Landscape (1920x1080)</option>
1158
+ <option value="4k_9_16">4K Vertical / Reels (2160x3840)</option>
1159
+ <option value="1080p_9_16">1080p Vertical / Reels (1080x1920)</option>
1160
+ </select>
1161
+ </div>
1162
+ <div>
1163
+ <ControlHeader label="Background Environment" />
1164
+ <div className="flex gap-2 mb-3">
1165
+ {['transparent', 'color', 'image'].map(function(type) {
1166
+ return <button key={type} onClick={function() { setBgType(type); }} className={`flex-1 py-2 px-2 rounded-lg text-xs font-medium capitalize transition-all ${bgType === type ? 'bg-slate-700 text-white shadow-inner border border-slate-600' : 'bg-slate-950 text-slate-400 border border-slate-800 hover:border-slate-600'}`}>{type}</button>;
1167
+ })}
1168
+ </div>
1169
+ {bgType === 'color' && (
1170
+ <div className="flex items-center gap-3 mt-2 bg-slate-950 p-2 rounded-lg border border-slate-800">
1171
+ <input type="color" value={bgColor} onChange={function(e) { setBgColor(e.target.value); }} className="h-8 w-12 rounded cursor-pointer bg-slate-950 border border-slate-700" />
1172
+ <span className="text-sm font-mono text-slate-400 uppercase">{bgColor}</span>
1173
+ </div>
1174
+ )}
1175
+ {bgType === 'image' && (
1176
+ <div className="mt-2 space-y-3">
1177
+ <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">
1178
+ <ImagePlus className="w-4 h-4" /> {bgImageSrc ? 'Change Image' : 'Upload Background Image'}
1179
+ <input type="file" accept="image/*" onChange={handleBgUpload} className="hidden" />
1180
+ </label>
1181
+ {bgImageSrc && (
1182
+ <div className="space-y-2">
1183
+ <div className="flex justify-between items-center bg-slate-950 p-2 rounded-lg border border-slate-800">
1184
+ <span className="text-xs font-medium text-slate-400">Image Fit</span>
1185
+ <select value={bgImageFit} onChange={function(e) { setBgImageFit(e.target.value); }} className="bg-slate-900 border border-slate-700 text-slate-300 text-xs rounded px-2 py-1 outline-none">
1186
+ <option value="contain">Contain (No Crop)</option>
1187
+ <option value="cover">Cover (Fill Canvas)</option>
1188
+ <option value="fit-width">Fit to Width</option>
1189
+ <option value="stretch">Stretch (Exact Fit)</option>
1190
+ </select>
1191
+ </div>
1192
+ <label className="flex justify-between items-center bg-slate-950 p-2 rounded-lg border border-slate-800 cursor-pointer">
1193
+ <span className="text-xs font-medium text-slate-400 flex items-center gap-2"><Zap className="w-3 h-3 text-cyan-400" /> Beat Reactive Pulse</span>
1194
+ <input type="checkbox" checked={bgBeatReactive} onChange={function(e) { setBgBeatReactive(e.target.checked); }} className="accent-cyan-500" />
1195
+ </label>
1196
+ </div>
1197
+ )}
1198
+ </div>
1199
+ )}
1200
+ </div>
1201
+ </div>
1202
+ )}
1203
+ </section>
1204
+
1205
+ </div>
1206
+
1207
+ </main>
1208
+ </div>
1209
+ );
1210
+ }
1211
+
1212
+ const root = ReactDOM.createRoot(document.getElementById('root'));
1213
+ root.render(<App />);
1214
+ </script>
1215
+ </body>
1216
+ </html>