4kaudio / m4pro_v3_circle-fix.html
trysem's picture
Upload m4pro_v3_circle-fix.html
2c5a9c8 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sujata Studios</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: { studio: { 500: '#8b5cf6', 600: '#7c3aed' } }
}
}
}
</script>
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
body {
margin: 0; padding: 0;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background-color: #fafafa;
color: #18181b;
transition: background-color 0.3s ease, color 0.3s ease;
background-image:
radial-gradient(circle at 15% 50%, rgba(139, 92, 246, 0.08), transparent 25%),
radial-gradient(circle at 85% 30%, rgba(217, 70, 239, 0.08), transparent 25%);
}
.dark body {
background-color: #09090b;
color: #e4e4e7;
background-image:
radial-gradient(circle at 15% 50%, rgba(139, 92, 246, 0.04), transparent 25%),
radial-gradient(circle at 85% 30%, rgba(217, 70, 239, 0.04), transparent 25%);
}
.custom-scrollbar::-webkit-scrollbar { width: 5px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 10px; }
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.2); }
.dark .custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); }
.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f4f4f5; }
::-webkit-scrollbar-thumb { background: #d4d4d8; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #a1a1aa; }
.dark ::-webkit-scrollbar-track { background: #09090b; }
.dark ::-webkit-scrollbar-thumb { background: #27272a; }
.dark ::-webkit-scrollbar-thumb:hover { background: #3f3f46; }
input[type=range] {
-webkit-appearance: none;
background: transparent;
width: 100%;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 12px;
width: 12px;
border-radius: 50%;
background: #8b5cf6;
cursor: pointer;
margin-top: -5px;
box-shadow: 0 0 10px rgba(139, 92, 246, 0.4);
transition: transform 0.1s;
}
.dark input[type=range]::-webkit-slider-thumb {
background: #a78bfa;
box-shadow: 0 0 10px rgba(139, 92, 246, 0.6);
}
input[type=range]::-webkit-slider-thumb:hover {
transform: scale(1.2);
background: #a78bfa;
}
.dark input[type=range]::-webkit-slider-thumb:hover {
background: #c4b5fd;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 2px;
cursor: pointer;
background: rgba(0, 0, 0, 0.1);
border-radius: 2px;
}
.dark input[type=range]::-webkit-slider-runnable-track {
background: rgba(255, 255, 255, 0.1);
}
select option {
background-color: #ffffff;
color: #18181b;
}
.dark select option {
background-color: #18181b;
color: #e4e4e7;
}
input[type="color"] {
-webkit-appearance: none;
border: none;
padding: 0;
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 6px;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useRef, useEffect, useCallback } = React;
// --- SVG Icons ---
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>;
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>;
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>;
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>;
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>;
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 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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
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>;
const Sun = ({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="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>;
const Moon = ({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 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>;
const Trash2 = ({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 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>;
const Info = ({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"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>;
const Droplet = ({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 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z"/></svg>;
const Layers = ({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="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>;
const X = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>;
const Sliders = ({className}) => <svg className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="2" y1="14" x2="6" y2="14"></line><line x1="10" y1="8" x2="14" y2="8"></line><line x1="18" y1="16" x2="22" y2="16"></line></svg>;
// --- Brand Logo Component ---
const SujataLogo = ({className}) => (
<svg className={className} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<clipPath id="circleClip"><circle cx="50" cy="50" r="50" /></clipPath>
<g clipPath="url(#circleClip)">
<rect x="0" y="0" width="100" height="38" fill="#FFB000"/>
<rect x="0" y="38" width="100" height="12" fill="#FA8243"/>
<rect x="0" y="50" width="100" height="12" fill="#FF6351"/>
<rect x="0" y="62" width="100" height="12" fill="#FA4066"/>
<rect x="0" y="74" width="100" height="26" fill="#E81C6C"/>
</g>
</svg>
);
// --- Pro UI Components ---
const ControlHeader = ({ label, valueDisplay, onReset, extra }) => (
<div className="flex justify-between items-center mb-2 group">
<div className="flex items-center gap-2">
<span className="text-[13px] font-semibold text-zinc-600 dark:text-zinc-400">{label}</span>
{extra}
</div>
<div className="flex items-center gap-2">
{valueDisplay !== undefined && (
<span className="text-[11px] text-violet-700 dark:text-violet-300 font-mono tracking-wider bg-violet-500/10 px-2 py-0.5 rounded-full border border-violet-500/20 shrink-0">
{valueDisplay}
</span>
)}
{onReset && (
<button onClick={onReset} className="text-zinc-400 dark:text-zinc-600 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-black/5 dark:hover:bg-white/5 p-1 rounded-md transition-all focus:outline-none opacity-80 hover:opacity-100 shrink-0" title={`Reset ${label}`}>
<RotateCcw className="w-3.5 h-3.5" />
</button>
)}
</div>
</div>
);
const SectionHeader = ({ title, icon: Icon, sectionKey, minStates, toggleMin }) => (
<div className={`flex justify-between items-center ${minStates[sectionKey] ? '' : 'mb-4'}`}>
<h2 className="text-xs font-bold tracking-widest uppercase text-zinc-600 dark:text-zinc-400 flex items-center gap-3">
<div className="p-1.5 bg-violet-500/10 rounded-lg text-violet-600 dark:text-violet-400">
<Icon className="w-4 h-4" />
</div>
{title}
</h2>
<button onClick={function() { toggleMin(sectionKey); }} className="p-1 text-zinc-400 dark:text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200 hover:bg-black/5 dark:hover:bg-white/5 rounded transition-colors" title={minStates[sectionKey] ? "Expand" : "Minimize"}>
{minStates[sectionKey] ? <ChevronDown className="w-4 h-4" /> : <ChevronUp className="w-4 h-4" />}
</button>
</div>
);
// --- Waveform Timeline Component ---
const WaveformTimeline = ({ waveformData, audioTime, audioDuration, audioRef, setAudioTime }) => {
const containerRef = useRef(null);
const handleClick = (e) => {
if (!audioRef.current || !audioDuration) return;
const rect = containerRef.current.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
const newTime = percent * audioDuration;
audioRef.current.currentTime = newTime;
setAudioTime(newTime);
};
const svgPath = waveformData.map((val, i) => {
const x = (i / waveformData.length) * 100;
const h = val * 100;
return `M ${x} ${50 - h/2} L ${x} ${50 + h/2}`;
}).join(" ");
return (
<div ref={containerRef} onClick={handleClick} className="relative w-full h-10 bg-black/5 dark:bg-white/5 rounded-lg overflow-hidden cursor-pointer border border-black/5 dark:border-white/10 transition-colors hover:bg-black/10 dark:hover:bg-white/10">
{waveformData.length > 0 ? (
<svg viewBox="0 0 100 100" preserveAspectRatio="none" className="absolute inset-0 w-full h-full text-violet-500/50 dark:text-violet-400/50">
<path d={svgPath} stroke="currentColor" strokeWidth="0.5" strokeLinecap="round" />
</svg>
) : (
<div className="flex h-full items-center justify-center text-[10px] tracking-widest text-zinc-500 font-mono opacity-50">NO WAVEFORM DATA</div>
)}
<div className="absolute top-0 left-0 h-full bg-violet-500/20 pointer-events-none transition-all duration-75" style={{width: `${audioDuration ? (audioTime/audioDuration)*100 : 0}%`}}></div>
<div className="absolute top-0 w-0.5 h-full bg-violet-600 dark:bg-white shadow-[0_0_8px_rgba(139,92,246,0.8)] dark:shadow-[0_0_8px_rgba(255,255,255,1)] pointer-events-none transition-all duration-75" style={{left: `${audioDuration ? (audioTime/audioDuration)*100 : 0}%`}}></div>
</div>
);
};
function App() {
const [isDark, setIsDark] = useState(true);
useEffect(() => {
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [isDark]);
const canvasRef = useRef(null);
const audioRef = useRef(null);
const audioCtxRef = useRef(null);
const analyserRef = useRef(null);
const sourceRef = useRef(null);
const destRef = useRef(null);
const reqIdRef = useRef(null);
const mediaRecorderRef = useRef(null);
const chunksRef = useRef([]);
const bgImgRef = useRef(null);
const watermarkImgRef = useRef(null);
const dataArrayRef = useRef(new Uint8Array(2048));
const bassArrayRef = useRef(new Uint8Array(10));
const peakArrayRef = useRef(new Float32Array(2048));
const particlesRef = useRef([]);
const lastDrawTimeRef = useRef(0);
// --- Audio Player State ---
const [audioSrc, setAudioSrc] = useState(null);
const [fileName, setFileName] = useState('');
const [isPlaying, setIsPlaying] = useState(false);
const [isExportingVideo, setIsExportingVideo] = useState(false);
const [exportProgress, setExportProgress] = useState(0);
const [audioTime, setAudioTime] = useState(0);
const [audioDuration, setAudioDuration] = useState(0);
const [isDraggingFile, setIsDraggingFile] = useState(false);
const [waveformData, setWaveformData] = useState([]);
// --- Visual Settings ---
const [vizType, setVizType] = useState('bars');
const [barShape, setBarShape] = useState('pill');
const [thickness, setThickness] = useState(12);
const [spacing, setSpacing] = useState(8);
const [sensitivity, setSensitivity] = useState(1.5);
const [smoothing, setSmoothing] = useState(0.85);
// --- Advanced Features States ---
const [showPeaks, setShowPeaks] = useState(false);
const [peakDecay, setPeakDecay] = useState(2.0);
const [colorMode, setColorMode] = useState('gradient');
const [gradStops, setGradStops] = useState(2);
const [color, setColor] = useState('#8b5cf6');
const [color2, setColor2] = useState('#d946ef');
const [color3, setColor3] = useState('#3b82f6');
const [color4, setColor4] = useState('#ec4899');
const [glow, setGlow] = useState(false);
const [taperEdges, setTaperEdges] = useState(false);
const [glitchEffect, setGlitchEffect] = useState(false);
const [glitchThreshold, setGlitchThreshold] = useState(240);
// --- Background Environment ---
const [bgType, setBgType] = useState('transparent');
const [bgColor, setBgColor] = useState('#09090b');
const [bgImageSrc, setBgImageSrc] = useState(null);
const [bgImageFit, setBgImageFit] = useState('contain');
const [bgBeatReactive, setBgBeatReactive] = useState(false);
const [showParticles, setShowParticles] = useState(false);
const [particleCount, setParticleCount] = useState(150);
// --- Transform Settings ---
const [offsetX, setOffsetX] = useState(0);
const [offsetY, setOffsetY] = useState(0);
const [scale, setScale] = useState(1.0);
const [scaleX, setScaleX] = useState(1.0);
const [scaleY, setScaleY] = useState(1.0);
const [rotation, setRotation] = useState(0);
const offsetRef = useRef({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const dragRef = useRef({ startX: 0, startY: 0, initX: 0, initY: 0 });
const [scaleLock, setScaleLock] = useState(false);
const scaleRatioRef = useRef(1.0);
const [mirrorX, setMirrorX] = useState(false);
const [mirrorY, setMirrorY] = useState(false);
const [isFilled, setIsFilled] = useState(true);
// --- Overlays ---
const [overlayText1, setOverlayText1] = useState('');
const [overlayText2, setOverlayText2] = useState('');
const [overlayTextSize, setOverlayTextSize] = useState(60);
const [overlayTextColor, setOverlayTextColor] = useState('#ffffff');
const [overlayTextPos, setOverlayTextPos] = useState('bottom');
const [watermarkSrc, setWatermarkSrc] = useState(null);
const [watermarkSize, setWatermarkSize] = useState(15);
const [watermarkOpacity, setWatermarkOpacity] = useState(0.8);
const [watermarkPosX, setWatermarkPosX] = useState(0);
const [watermarkPosY, setWatermarkPosY] = useState(35);
// --- Export & Config ---
const [freqBand, setFreqBand] = useState('all');
const [flashOnBeat, setFlashOnBeat] = useState(false);
const [flashColor, setFlashColor] = useState('#ffffff');
const [flashThreshold, setFlashThreshold] = useState(230);
const [resolution, setResolution] = useState('4k_16_9');
const [exportFormat, setExportFormat] = useState('mp4');
const [exportFps, setExportFps] = useState(30);
const [exportStart, setExportStart] = useState(0);
const [exportEnd, setExportEnd] = useState(0);
const [safeZone, setSafeZone] = useState('none');
// --- UI States ---
const [showExportInfo, setShowExportInfo] = useState(false);
const [minStates, setMinStates] = useState({
audio: false, visual: false, transform: false, text: false, output: false, export: false
});
const toggleMin = function(key) {
setMinStates(function(prev) { return {...prev, [key]: !prev[key]}; });
};
const playButtonRef = useRef(null);
const RESOLUTIONS = {
'4k_16_9': { w: 3840, h: 2160, label: '4K (16:9)', isVertical: false },
'1080p_16_9': { w: 1920, h: 1080, label: '1080p (16:9)', isVertical: false },
'4k_9_16': { w: 2160, h: 3840, label: '4K Vertical (9:16)', isVertical: true },
'1080p_9_16': { w: 1080, h: 1920, label: '1080p Vertical (9:16)', isVertical: true }
};
const savePreset = function() {
const preset = {
vizType, barShape, color, color2, color3, color4, gradStops, thickness, spacing, sensitivity, smoothing, colorMode,
glow, taperEdges, showPeaks, peakDecay, glitchEffect, glitchThreshold, resolution, bgType, bgColor, bgImageFit,
scale, scaleX, scaleY, rotation, offsetX, offsetY, mirrorX, mirrorY, isFilled, bgBeatReactive, showParticles, particleCount,
freqBand, flashOnBeat, flashColor, flashThreshold, overlayText1, overlayText2, overlayTextSize, overlayTextColor, overlayTextPos,
watermarkSize, watermarkOpacity, watermarkPosX, watermarkPosY
};
localStorage.setItem('viz_preset_studio', JSON.stringify(preset));
alert('Studio Settings saved as Preset!');
};
const loadPreset = function() {
const str = localStorage.getItem('viz_preset_studio');
if (str) {
try {
const p = JSON.parse(str);
if (p.vizType !== undefined) setVizType(p.vizType);
if (p.barShape !== undefined) setBarShape(p.barShape);
if (p.color !== undefined) setColor(p.color);
if (p.color2 !== undefined) setColor2(p.color2);
if (p.color3 !== undefined) setColor3(p.color3);
if (p.color4 !== undefined) setColor4(p.color4);
if (p.gradStops !== undefined) setGradStops(p.gradStops);
if (p.thickness !== undefined) setThickness(p.thickness);
if (p.spacing !== undefined) setSpacing(p.spacing);
if (p.sensitivity !== undefined) setSensitivity(p.sensitivity);
if (p.smoothing !== undefined) setSmoothing(p.smoothing);
if (p.colorMode !== undefined) setColorMode(p.colorMode);
if (p.glow !== undefined) setGlow(p.glow);
if (p.taperEdges !== undefined) setTaperEdges(p.taperEdges);
if (p.showPeaks !== undefined) setShowPeaks(p.showPeaks);
if (p.peakDecay !== undefined) setPeakDecay(p.peakDecay);
if (p.glitchEffect !== undefined) setGlitchEffect(p.glitchEffect);
if (p.glitchThreshold !== undefined) setGlitchThreshold(p.glitchThreshold);
if (p.resolution !== undefined) setResolution(p.resolution);
if (p.bgType !== undefined) setBgType(p.bgType);
if (p.bgColor !== undefined) setBgColor(p.bgColor);
if (p.bgImageFit !== undefined) setBgImageFit(p.bgImageFit);
if (p.scale !== undefined) setScale(p.scale);
if (p.scaleX !== undefined) setScaleX(p.scaleX);
if (p.scaleY !== undefined) setScaleY(p.scaleY);
if (p.rotation !== undefined) setRotation(p.rotation);
if (p.offsetX !== undefined) { setOffsetX(p.offsetX); offsetRef.current.x = p.offsetX; }
if (p.offsetY !== undefined) { setOffsetY(p.offsetY); offsetRef.current.y = p.offsetY; }
if (p.mirrorX !== undefined) setMirrorX(p.mirrorX);
if (p.mirrorY !== undefined) setMirrorY(p.mirrorY);
if (p.isFilled !== undefined) setIsFilled(p.isFilled);
if (p.bgBeatReactive !== undefined) setBgBeatReactive(p.bgBeatReactive);
if (p.showParticles !== undefined) setShowParticles(p.showParticles);
if (p.particleCount !== undefined) setParticleCount(p.particleCount);
if (p.freqBand !== undefined) setFreqBand(p.freqBand);
if (p.flashOnBeat !== undefined) setFlashOnBeat(p.flashOnBeat);
if (p.flashColor !== undefined) setFlashColor(p.flashColor);
if (p.flashThreshold !== undefined) setFlashThreshold(p.flashThreshold);
if (p.overlayText1 !== undefined) setOverlayText1(p.overlayText1);
if (p.overlayText2 !== undefined) setOverlayText2(p.overlayText2);
if (p.overlayTextSize !== undefined) setOverlayTextSize(p.overlayTextSize);
if (p.overlayTextColor !== undefined) setOverlayTextColor(p.overlayTextColor);
if (p.overlayTextPos !== undefined) setOverlayTextPos(p.overlayTextPos);
if (p.watermarkSize !== undefined) setWatermarkSize(p.watermarkSize);
if (p.watermarkOpacity !== undefined) setWatermarkOpacity(p.watermarkOpacity);
if (p.watermarkPosX !== undefined) setWatermarkPosX(p.watermarkPosX);
if (p.watermarkPosY !== undefined) setWatermarkPosY(p.watermarkPosY);
} catch (e) { console.error("Failed to load preset"); }
} else { alert('No preset found!'); }
};
const handleResetAll = function() {
if (confirm("Are you sure you want to reset all settings to Studio defaults?")) {
setVizType('bars'); setBarShape('pill'); setColor('#8b5cf6'); setColor2('#d946ef'); setColor3('#3b82f6'); setColor4('#ec4899'); setGradStops(2);
setThickness(12); setSpacing(8); setSensitivity(1.5); setSmoothing(0.85); setColorMode('gradient');
setGlow(false); setTaperEdges(false); setShowPeaks(false); setPeakDecay(2.0); setGlitchEffect(false); setGlitchThreshold(240);
setResolution('4k_16_9'); setBgType('transparent'); setBgColor('#09090b'); setBgImageSrc(null); setBgImageFit('contain');
setShowParticles(false); setParticleCount(150);
setScale(1.0); setScaleX(1.0); setScaleY(1.0); setRotation(0); setOffsetX(0); setOffsetY(0); offsetRef.current = {x: 0, y: 0};
setMirrorX(false); setMirrorY(false); setIsFilled(true); setBgBeatReactive(false);
setOverlayText1(''); setOverlayText2(''); setOverlayTextSize(60); setOverlayTextColor('#ffffff'); setOverlayTextPos('bottom');
if(watermarkSrc) URL.revokeObjectURL(watermarkSrc); setWatermarkSrc(null); setWatermarkSize(15); setWatermarkOpacity(0.8); setWatermarkPosX(0); setWatermarkPosY(35);
setFreqBand('all'); setFlashOnBeat(false); setFlashColor('#ffffff'); setFlashThreshold(230); setExportStart(0); setExportEnd(0);
}
};
useEffect(() => {
if (bgImageSrc) {
const img = new Image();
img.onload = () => { bgImgRef.current = img; };
img.src = bgImageSrc;
} else { bgImgRef.current = null; }
}, [bgImageSrc]);
useEffect(() => {
if (watermarkSrc) {
const img = new Image();
img.onload = () => { watermarkImgRef.current = img; };
img.src = watermarkSrc;
} else { watermarkImgRef.current = null; }
}, [watermarkSrc]);
const handleBgUpload = function(e) {
const file = e.target.files[0];
if (file) {
if (bgImageSrc) URL.revokeObjectURL(bgImageSrc);
setBgImageSrc(URL.createObjectURL(file));
setBgType('image');
}
};
const handleWatermarkUpload = function(e) {
const file = e.target.files[0];
if (file) {
if (watermarkSrc) URL.revokeObjectURL(watermarkSrc);
setWatermarkSrc(URL.createObjectURL(file));
}
};
const generateWaveform = async (file) => {
try {
const arrayBuffer = await file.arrayBuffer();
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
const channelData = audioBuffer.getChannelData(0);
const points = 200;
const step = Math.ceil(channelData.length / points);
const peaks = [];
let maxPeak = 0;
for (let i = 0; i < points; i++) {
let min = 1.0;
let max = -1.0;
for (let j = 0; j < step; j++) {
const val = channelData[i * step + j];
if (val < min) min = val;
if (val > max) max = val;
}
const amplitude = Math.max(Math.abs(min), Math.abs(max));
if (amplitude > maxPeak) maxPeak = amplitude;
peaks.push(amplitude);
}
const normalized = peaks.map(p => (maxPeak ? p / maxPeak : 0));
setWaveformData(normalized);
} catch (err) {
console.error("Waveform generation failed", err);
setWaveformData([]);
}
};
const initAudio = useCallback(() => {
if (!audioCtxRef.current) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtxRef.current = new AudioContext();
analyserRef.current = audioCtxRef.current.createAnalyser();
destRef.current = audioCtxRef.current.createMediaStreamDestination();
if (!sourceRef.current && audioRef.current) {
sourceRef.current = audioCtxRef.current.createMediaElementSource(audioRef.current);
sourceRef.current.connect(analyserRef.current);
analyserRef.current.connect(audioCtxRef.current.destination);
analyserRef.current.connect(destRef.current);
}
}
if (audioCtxRef.current.state === 'suspended') {
audioCtxRef.current.resume();
}
}, []);
const processAudioFile = function(file) {
if (file) {
if (audioSrc) URL.revokeObjectURL(audioSrc);
const url = URL.createObjectURL(file);
setAudioSrc(url);
setFileName(file.name);
setIsPlaying(false);
setAudioTime(0);
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
generateWaveform(file);
}
};
const handleFileUpload = function(e) {
processAudioFile(e.target.files[0]);
};
const handleRemoveAudio = useCallback(() => {
if (audioSrc) URL.revokeObjectURL(audioSrc);
setAudioSrc(null);
setFileName('');
setIsPlaying(false);
setAudioTime(0);
setWaveformData([]);
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
}, [audioSrc]);
const handleDragOver = function(e) {
e.preventDefault();
e.stopPropagation();
if (!isDraggingFile && !isExportingVideo) setIsDraggingFile(true);
};
const handleDragLeave = function(e) {
e.preventDefault();
e.stopPropagation();
setIsDraggingFile(false);
};
const handleDrop = function(e) {
e.preventDefault();
e.stopPropagation();
setIsDraggingFile(false);
if (isExportingVideo) return;
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('audio/')) {
processAudioFile(file);
} else if (file) {
alert("Please drop a valid audio file (MP3, WAV, etc).");
}
};
const togglePlay = function() {
if (!audioSrc) return;
initAudio();
if (isPlaying) {
audioRef.current.pause();
setIsPlaying(false);
} else {
audioRef.current.play().then(() => setIsPlaying(true)).catch(e => console.error("Playback prevented.", e));
}
};
const handleTimeUpdate = function() { if (audioRef.current) setAudioTime(audioRef.current.currentTime); };
const handleLoadedMetadata = function() { if (audioRef.current) setAudioDuration(audioRef.current.duration); };
const handleSeek = function(e) {
const time = Number(e.target.value);
if (audioRef.current) audioRef.current.currentTime = time;
setAudioTime(time);
};
const formatTime = function(time) {
if (isNaN(time)) return "0:00";
const m = Math.floor(time / 60);
const s = Math.floor(time % 60).toString().padStart(2, '0');
return `${m}:${s}`;
};
const handleMouseDown = function(e) {
setIsDragging(true);
dragRef.current = { startX: e.clientX, startY: e.clientY, initX: offsetRef.current.x, initY: offsetRef.current.y };
};
const handleMouseMove = function(e) {
if (!isDragging || !canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
const deltaX = e.clientX - dragRef.current.startX;
const deltaY = e.clientY - dragRef.current.startY;
const percentX = (deltaX / rect.width) * 100;
const percentY = (deltaY / rect.height) * 100;
const newX = Math.max(-50, Math.min(50, dragRef.current.initX + percentX));
const newY = Math.max(-50, Math.min(50, dragRef.current.initY + percentY));
offsetRef.current.x = newX; offsetRef.current.y = newY;
setOffsetX(newX); setOffsetY(newY);
};
const handleMouseUpOrLeave = function() { setIsDragging(false); };
const handleScaleXChange = function(val) {
setScaleX(val);
if (scaleLock) setScaleY(Math.max(0.1, Math.min(5.0, val / scaleRatioRef.current)));
};
const handleScaleYChange = function(val) {
setScaleY(val);
if (scaleLock) setScaleX(Math.max(0.1, Math.min(5.0, val * scaleRatioRef.current)));
};
const toggleScaleLock = function() {
if (!scaleLock) scaleRatioRef.current = scaleX / scaleY;
setScaleLock(!scaleLock);
};
useEffect(() => {
const handleKeyDown = (e) => {
if (e.code === 'Space' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
e.preventDefault();
playButtonRef.current?.click();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
const getSafeZoneStyle = function(zone, canvasW, canvasH) {
const canvasRatio = canvasW / canvasH;
let targetRatio = canvasRatio;
if (zone === '1:1') targetRatio = 1;
else if (zone === '4:5') targetRatio = 4/5;
else if (zone === '9:16') targetRatio = 9/16;
else if (zone === '16:9') targetRatio = 16/9;
else if (zone === 'title_safe') return { width: '90%', height: '90%' };
if (targetRatio > canvasRatio) return { width: '100%', aspectRatio: `${targetRatio}` };
else return { height: '100%', aspectRatio: `${targetRatio}` };
};
// --- CORE DRAW LOOP ---
const draw = useCallback(() => {
if (!canvasRef.current) {
reqIdRef.current = requestAnimationFrame(draw);
return;
}
const now = performance.now();
if (isExportingVideo && exportFps < 60) {
const msPerFrame = 1000 / exportFps;
if (now - lastDrawTimeRef.current < msPerFrame) {
reqIdRef.current = requestAnimationFrame(draw);
return;
}
}
lastDrawTimeRef.current = now;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d', { alpha: bgType === 'transparent', willReadFrequently: false });
const res = RESOLUTIONS[resolution] || RESOLUTIONS['4k_16_9'];
if (canvas.width !== res.w || canvas.height !== res.h) {
canvas.width = res.w; canvas.height = res.h;
}
const width = canvas.width;
const height = canvas.height;
ctx.clearRect(0, 0, width, height);
let bassAvg = 0;
if (analyserRef.current) {
analyserRef.current.smoothingTimeConstant = smoothing;
analyserRef.current.fftSize = 2048;
const dataArray = dataArrayRef.current;
const bassArray = bassArrayRef.current;
analyserRef.current.getByteFrequencyData(bassArray);
for(let i=0; i<10; i++) bassAvg += bassArray[i];
bassAvg /= 10;
if (vizType === 'bars' || vizType === 'circle' || vizType === 'symmetric_wave') {
analyserRef.current.getByteFrequencyData(dataArray);
} else if (vizType === 'wave') {
analyserRef.current.getByteTimeDomainData(dataArray);
}
}
// Draw Background & Particles
if (bgType === 'color') {
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
} else if (bgType === 'image' && bgImgRef.current) {
const img = bgImgRef.current;
const imgRatio = img.width / img.height;
const canvasRatio = width / height;
let drawW, drawH, drawX, drawY;
if (bgImageFit === 'stretch') { drawW = width; drawH = height; drawX = 0; drawY = 0; }
else if (bgImageFit === 'cover') {
if (imgRatio > canvasRatio) { drawH = height; drawW = height * imgRatio; drawX = (width - drawW) / 2; drawY = 0; }
else { drawW = width; drawH = width / imgRatio; drawX = 0; drawY = (height - drawH) / 2; }
} else if (bgImageFit === 'fit-width') {
drawW = width; drawH = width / imgRatio; drawX = 0; drawY = (height - drawH) / 2;
} else {
if (imgRatio > canvasRatio) { drawW = width; drawH = width / imgRatio; drawX = 0; drawY = (height - drawH) / 2; }
else { drawH = height; drawW = height * imgRatio; drawX = (width - drawW) / 2; drawY = 0; }
}
if (bgBeatReactive) {
const bgScale = 1.0 + (bassAvg / 255) * 0.08;
const scaledW = drawW * bgScale;
const scaledH = drawH * bgScale;
const scaledX = drawX - (scaledW - drawW) / 2;
const scaledY = drawY - (scaledH - drawH) / 2;
ctx.drawImage(img, scaledX, scaledY, scaledW, scaledH);
} else {
ctx.drawImage(img, drawX, drawY, drawW, drawH);
}
}
if (showParticles) {
if (particlesRef.current.length !== particleCount) {
particlesRef.current = Array.from({length: particleCount}, () => ({
x: Math.random() * width, y: Math.random() * height,
vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2,
size: Math.random() * 2 + 1, hue: Math.random() * 360
}));
}
ctx.save();
ctx.globalCompositeOperation = 'screen';
const boost = 1 + (bassAvg / 100);
particlesRef.current.forEach(p => {
p.x += p.vx * boost; p.y += p.vy * boost;
if (p.x < 0) p.x = width; if (p.x > width) p.x = 0;
if (p.y < 0) p.y = height; if (p.y > height) p.y = 0;
ctx.fillStyle = `hsla(${colorMode==='rainbow'? p.hue : 260}, 80%, 70%, ${0.2 + (bassAvg/512)})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * (1 + bassAvg/255), 0, Math.PI*2);
ctx.fill();
});
ctx.restore();
}
if (analyserRef.current) {
const bufferLength = analyserRef.current.frequencyBinCount;
const dataArray = dataArrayRef.current;
ctx.save();
const centerX = width / 2 + (width * (offsetRef.current.x / 100));
const centerY = height / 2 + (height * (offsetRef.current.y / 100));
ctx.translate(centerX, centerY);
ctx.scale(scale * scaleX, scale * scaleY);
ctx.rotate((rotation * Math.PI) / 180);
let activeColor = color;
if (colorMode === 'gradient') {
const grad = ctx.createLinearGradient(-width/2, -height/2, width/2, height/2);
grad.addColorStop(0, color);
if (gradStops >= 3) grad.addColorStop(0.5, color3);
if (gradStops >= 4) grad.addColorStop(0.75, color4);
grad.addColorStop(1, color2);
activeColor = grad;
} else if (colorMode === 'rainbow') {
const grad = ctx.createLinearGradient(-width/2, 0, width/2, 0);
grad.addColorStop(0, '#ff0000'); grad.addColorStop(0.16, '#ffff00'); grad.addColorStop(0.33, '#00ff00');
grad.addColorStop(0.5, '#00ffff'); grad.addColorStop(0.66, '#0000ff'); grad.addColorStop(0.83, '#ff00ff');
grad.addColorStop(1, '#ff0000'); activeColor = grad;
}
if (flashOnBeat && bassAvg > flashThreshold) { activeColor = flashColor; }
ctx.strokeStyle = activeColor;
ctx.fillStyle = activeColor;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
let startIndex = 0;
let bandLength = Math.floor(bufferLength * 0.75);
if (freqBand === 'bass') { startIndex = 0; bandLength = Math.floor(bufferLength * 0.08); }
else if (freqBand === 'mid') { startIndex = Math.floor(bufferLength * 0.08); bandLength = Math.floor(bufferLength * 0.3); }
else if (freqBand === 'treble') { startIndex = Math.floor(bufferLength * 0.38); bandLength = Math.floor(bufferLength * 0.37); }
const drawVisualizerPath = () => {
const buildShapes = () => {
ctx.beginPath();
ctx.lineCap = barShape === 'flat' ? 'butt' : 'round';
if (vizType === 'bars') {
const step = thickness + spacing;
const maxBars = Math.floor((width / 2) / step);
const numBars = Math.min(maxBars, bandLength);
for (let i = 0; i < numBars; i++) {
const dataIndex = startIndex + Math.floor((i / numBars) * bandLength);
const boost = Math.pow(1 + (i / numBars), 1.5);
let edgeMultiplier = 1;
if (taperEdges) edgeMultiplier = Math.pow(1 - (i / numBars), 2);
const value = dataArray[dataIndex] * boost * sensitivity * edgeMultiplier;
const barHeight = Math.max(thickness / 2, (value / 255) * height * 0.8);
const xOffset = i * step + (step / 2);
// Handle Peaks Data
if (showPeaks) {
if (barHeight > peakArrayRef.current[dataIndex]) peakArrayRef.current[dataIndex] = barHeight;
else peakArrayRef.current[dataIndex] = Math.max(0, peakArrayRef.current[dataIndex] - peakDecay);
}
if (barShape === 'dots') {
ctx.moveTo(xOffset + thickness/2, height/2 - barHeight); ctx.arc(xOffset, height/2 - barHeight, thickness/2, 0, Math.PI*2);
ctx.moveTo(-xOffset + thickness/2, height/2 - barHeight); ctx.arc(-xOffset, height/2 - barHeight, thickness/2, 0, Math.PI*2);
} else if (barShape === 'diamonds') {
const s = thickness;
ctx.moveTo(xOffset, height/2 - barHeight - s); ctx.lineTo(xOffset + s, height/2 - barHeight); ctx.lineTo(xOffset, height/2 - barHeight + s); ctx.lineTo(xOffset - s, height/2 - barHeight); ctx.closePath();
ctx.moveTo(-xOffset, height/2 - barHeight - s); ctx.lineTo(-xOffset + s, height/2 - barHeight); ctx.lineTo(-xOffset, height/2 - barHeight + s); ctx.lineTo(-xOffset - s, height/2 - barHeight); ctx.closePath();
} else if (isFilled && barShape === 'pill' && ctx.roundRect) {
ctx.roundRect(xOffset - thickness/2, height/2 - barHeight, thickness, barHeight, thickness/2);
ctx.roundRect(-xOffset - thickness/2, height/2 - barHeight, thickness, barHeight, thickness/2);
} else if (isFilled) {
ctx.rect(xOffset - thickness/2, height/2 - barHeight, thickness, barHeight);
ctx.rect(-xOffset - thickness/2, height/2 - barHeight, thickness, barHeight);
} else {
ctx.moveTo(xOffset, height / 2 - (thickness / 2)); ctx.lineTo(xOffset, height / 2 - barHeight);
ctx.moveTo(-xOffset, height / 2 - (thickness / 2)); ctx.lineTo(-xOffset, height / 2 - barHeight);
}
}
} else if (vizType === 'symmetric_wave') {
const step = thickness + spacing;
const maxBars = Math.floor((width / 2) / step);
const usefulLength = Math.floor(bandLength * 0.75);
const numBars = Math.min(maxBars, usefulLength);
for (let i = 0; i < numBars; i++) {
const dataIndex = startIndex + Math.floor((i / numBars) * usefulLength);
const boost = Math.pow(1 + (i / numBars), 1.5);
let edgeMultiplier = 1;
if (taperEdges) edgeMultiplier = Math.pow(1 - (i / numBars), 2);
const value = dataArray[dataIndex] * boost * sensitivity * edgeMultiplier;
const barHeight = Math.max(thickness / 2, (value / 255) * height * 0.4);
const xOffset = i * step + (step / 2);
if (showPeaks) {
if (barHeight > peakArrayRef.current[dataIndex]) peakArrayRef.current[dataIndex] = barHeight;
else peakArrayRef.current[dataIndex] = Math.max(0, peakArrayRef.current[dataIndex] - peakDecay);
}
if (barShape === 'dots') {
ctx.moveTo(xOffset + thickness/2, -barHeight); ctx.arc(xOffset, -barHeight, thickness/2, 0, Math.PI*2);
ctx.moveTo(xOffset + thickness/2, barHeight); ctx.arc(xOffset, barHeight, thickness/2, 0, Math.PI*2);
ctx.moveTo(-xOffset + thickness/2, -barHeight); ctx.arc(-xOffset, -barHeight, thickness/2, 0, Math.PI*2);
ctx.moveTo(-xOffset + thickness/2, barHeight); ctx.arc(-xOffset, barHeight, thickness/2, 0, Math.PI*2);
} else if (isFilled && barShape === 'pill' && ctx.roundRect) {
ctx.roundRect(xOffset - thickness/2, -barHeight, thickness, barHeight * 2, thickness/2);
ctx.roundRect(-xOffset - thickness/2, -barHeight, thickness, barHeight * 2, thickness/2);
} else if (isFilled) {
ctx.rect(xOffset - thickness/2, -barHeight, thickness, barHeight * 2);
ctx.rect(-xOffset - thickness/2, -barHeight, thickness, barHeight * 2);
} else {
ctx.moveTo(xOffset, -barHeight); ctx.lineTo(xOffset, barHeight);
ctx.moveTo(-xOffset, -barHeight); ctx.lineTo(-xOffset, barHeight);
}
}
} else if (vizType === 'wave') {
const sliceWidth = width / bandLength;
let x = -width / 2;
if (isFilled) ctx.moveTo(-width/2, height/2);
for (let i = 0; i < bandLength; i++) {
const dataIndex = startIndex + i;
const normalized = (dataArray[dataIndex] / 128.0) - 1;
let edgeMultiplier = 1;
if (taperEdges) edgeMultiplier = Math.pow(1 - (Math.abs(i - bandLength/2) / (bandLength/2)), 2);
const y = normalized * sensitivity * (height / 2) * edgeMultiplier;
if (i === 0 && !isFilled) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
x += sliceWidth;
}
if (isFilled) { ctx.lineTo(width/2, height/2); ctx.closePath(); }
} else if (vizType === 'circle') {
const radius = height / 4;
const circumference = 2 * Math.PI * radius;
const stepSize = Math.max(1, thickness + spacing);
const bars = Math.floor(circumference / stepSize);
if (bars > 0) {
const step = (Math.PI * 2) / bars;
const halfBars = Math.floor(bars / 2);
for (let i = 0; i < bars; i++) {
const iSymmetric = i <= halfBars ? i : bars - i;
const dataProgression = halfBars === 0 ? 0 : (iSymmetric / halfBars);
const dataIndex = startIndex + Math.floor(dataProgression * bandLength * 0.75);
const boost = Math.pow(1 + dataProgression, 1.5);
let edgeMultiplier = taperEdges ? Math.pow(1 - dataProgression, 2) : 1;
const value = dataArray[dataIndex] * boost * sensitivity * edgeMultiplier;
const barHeight = Math.max(thickness / 2, (value / 255) * (height * 0.35));
const angle = i * step + (Math.PI / 2);
if (showPeaks) {
if (barHeight > peakArrayRef.current[dataIndex]) peakArrayRef.current[dataIndex] = barHeight;
else peakArrayRef.current[dataIndex] = Math.max(0, peakArrayRef.current[dataIndex] - peakDecay);
}
const x1 = Math.cos(angle) * radius; const y1 = Math.sin(angle) * radius;
const x2 = Math.cos(angle) * (radius + barHeight); const y2 = Math.sin(angle) * (radius + barHeight);
if (barShape === 'dots') {
ctx.moveTo(x2 + thickness/2, y2); ctx.arc(x2, y2, thickness/2, 0, Math.PI*2);
} else if (barShape === 'diamonds') {
const s = thickness;
const dirX = Math.cos(angle); const dirY = Math.sin(angle);
const perpX = Math.cos(angle + Math.PI/2); const perpY = Math.sin(angle + Math.PI/2);
ctx.moveTo(x2 + dirX*s, y2 + dirY*s); ctx.lineTo(x2 + perpX*s, y2 + perpY*s);
ctx.lineTo(x2 - dirX*s, y2 - dirY*s); ctx.lineTo(x2 - perpX*s, y2 - perpY*s); ctx.closePath();
} else {
ctx.moveTo(x1, y1); ctx.lineTo(x2, y2);
}
}
}
}
};
const executeDraw = () => {
buildShapes();
if (mirrorX) { ctx.save(); ctx.scale(-1, 1); buildShapes(); ctx.restore(); }
if (mirrorY) { ctx.save(); ctx.scale(1, -1); buildShapes(); ctx.restore(); }
if (mirrorX && mirrorY) { ctx.save(); ctx.scale(-1, -1); buildShapes(); ctx.restore(); }
if (isFilled && (vizType === 'bars' || vizType === 'symmetric_wave' || vizType === 'wave')) ctx.fill(); else ctx.stroke();
if (vizType === 'circle' && !isFilled && barShape !== 'dots' && barShape !== 'diamonds') {
const radius = height / 4;
ctx.beginPath(); ctx.arc(0, 0, radius - thickness, 0, Math.PI * 2);
ctx.lineWidth = Math.max(1, thickness / 3); ctx.stroke();
}
// Draw Ghost Peaks
if (showPeaks && (vizType === 'bars' || vizType === 'symmetric_wave' || vizType === 'circle')) {
ctx.beginPath();
ctx.lineCap = 'butt';
if (vizType === 'bars' || vizType === 'symmetric_wave') {
const step = thickness + spacing;
const maxBars = Math.floor((width / 2) / step);
const usefulLength = Math.floor(bandLength * 0.75);
const numBars = Math.min(maxBars, usefulLength);
for (let i = 0; i < numBars; i++) {
const dataIndex = startIndex + Math.floor((i / numBars) * usefulLength);
const pHeight = peakArrayRef.current[dataIndex];
if (pHeight <= 0) continue;
const xOffset = i * step + (step / 2);
if (vizType === 'bars') {
ctx.moveTo(xOffset - thickness/2, height/2 - pHeight - 8); ctx.lineTo(xOffset + thickness/2, height/2 - pHeight - 8);
ctx.moveTo(-xOffset - thickness/2, height/2 - pHeight - 8); ctx.lineTo(-xOffset + thickness/2, height/2 - pHeight - 8);
} else {
ctx.moveTo(xOffset - thickness/2, -pHeight - 8); ctx.lineTo(xOffset + thickness/2, -pHeight - 8);
ctx.moveTo(xOffset - thickness/2, pHeight + 8); ctx.lineTo(xOffset + thickness/2, pHeight + 8);
ctx.moveTo(-xOffset - thickness/2, -pHeight - 8); ctx.lineTo(-xOffset + thickness/2, -pHeight - 8);
ctx.moveTo(-xOffset - thickness/2, pHeight + 8); ctx.lineTo(-xOffset + thickness/2, pHeight + 8);
}
}
} else if (vizType === 'circle') {
const radius = height / 4;
const circumference = 2 * Math.PI * radius;
const stepSize = Math.max(1, thickness + spacing);
const bars = Math.floor(circumference / stepSize);
if (bars > 0) {
const step = (Math.PI * 2) / bars;
const halfBars = Math.floor(bars / 2);
for (let i = 0; i < bars; i++) {
const iSymmetric = i <= halfBars ? i : bars - i;
const prog = halfBars === 0 ? 0 : (iSymmetric / halfBars);
const dataIndex = startIndex + Math.floor(prog * bandLength * 0.75);
const pHeight = peakArrayRef.current[dataIndex];
if (pHeight <= 0) continue;
const angle = i * step + (Math.PI / 2);
const px1 = Math.cos(angle) * (radius + pHeight);
const py1 = Math.sin(angle) * (radius + pHeight);
const offset = Math.max(2, thickness*0.2);
const px2 = Math.cos(angle) * (radius + pHeight + offset);
const py2 = Math.sin(angle) * (radius + pHeight + offset);
ctx.moveTo(px1, py1); ctx.lineTo(px2, py2);
}
}
}
ctx.stroke();
}
};
if (glitchEffect && bassAvg > glitchThreshold) {
const offset = (bassAvg - glitchThreshold) * 0.3;
ctx.globalCompositeOperation = 'screen';
ctx.lineWidth = thickness;
ctx.save(); ctx.translate(-offset, 0); ctx.strokeStyle='#ff4444'; ctx.fillStyle='#ff4444'; executeDraw(); ctx.restore();
ctx.save(); ctx.translate(offset, offset/2); ctx.strokeStyle='#44ff44'; ctx.fillStyle='#44ff44'; executeDraw(); ctx.restore();
ctx.save(); ctx.translate(0, -offset); ctx.strokeStyle='#4444ff'; ctx.fillStyle='#4444ff'; executeDraw(); ctx.restore();
ctx.globalCompositeOperation = 'source-over';
} else if (glow) {
ctx.globalCompositeOperation = 'lighter';
ctx.lineWidth = thickness * 3; ctx.globalAlpha = 0.15; executeDraw();
ctx.lineWidth = thickness * 1.5; ctx.globalAlpha = 0.4; executeDraw();
ctx.lineWidth = thickness; ctx.globalAlpha = 1.0; executeDraw();
ctx.globalCompositeOperation = 'source-over';
} else {
ctx.lineWidth = thickness;
executeDraw();
}
};
drawVisualizerPath();
ctx.restore();
}
if (watermarkImgRef.current) {
ctx.save();
ctx.globalAlpha = watermarkOpacity;
const ww = (watermarkSize / 100) * width;
const wh = ww * (watermarkImgRef.current.height / watermarkImgRef.current.width);
const wx = (watermarkPosX / 100) * width + (width/2 - ww/2);
const wy = (watermarkPosY / 100) * height + (height/2 - wh/2);
ctx.drawImage(watermarkImgRef.current, wx, wy, ww, wh);
ctx.restore();
}
if (overlayText1 || overlayText2) {
ctx.save(); ctx.fillStyle = overlayTextColor; ctx.textAlign = 'center';
const scaleFactor = res.w / 3840;
const fSize1 = overlayTextSize * scaleFactor * 3;
const fSize2 = fSize1 * 0.6;
let startY = res.h / 2;
if (overlayTextPos === 'top') startY = res.h * 0.15;
if (overlayTextPos === 'bottom') startY = res.h * 0.85;
ctx.shadowColor = 'rgba(0,0,0,0.8)'; ctx.shadowBlur = 15 * scaleFactor;
ctx.shadowOffsetX = 3 * scaleFactor; ctx.shadowOffsetY = 3 * scaleFactor;
if (overlayText1) { ctx.font = `bold ${fSize1}px sans-serif`; ctx.fillText(overlayText1, res.w / 2, startY); }
if (overlayText2) { ctx.font = `600 ${fSize2}px sans-serif`; ctx.fillText(overlayText2, res.w / 2, startY + fSize1 * 1.2); }
ctx.restore();
}
reqIdRef.current = requestAnimationFrame(draw);
}, [vizType, barShape, color, color2, color3, color4, gradStops, thickness, spacing, sensitivity, smoothing, colorMode, glow, taperEdges, showPeaks, peakDecay, glitchEffect, glitchThreshold, resolution, bgType, bgColor, bgImageFit, scale, scaleX, scaleY, rotation, offsetX, offsetY, isExportingVideo, exportFps, mirrorX, mirrorY, isFilled, bgBeatReactive, showParticles, particleCount, freqBand, flashOnBeat, flashColor, flashThreshold, overlayText1, overlayText2, overlayTextSize, overlayTextColor, overlayTextPos, watermarkSize, watermarkOpacity, watermarkPosX, watermarkPosY]);
useEffect(() => {
reqIdRef.current = requestAnimationFrame(draw);
return () => cancelAnimationFrame(reqIdRef.current);
}, [draw]);
const handleAudioEnded = function() {
setIsPlaying(false);
if (isExportingVideo) stopVideoExport();
};
const exportImage = function() {
if (!canvasRef.current) return;
const link = document.createElement('a');
link.download = `visualizer_${Date.now()}.png`;
link.href = canvasRef.current.toDataURL('image/png');
link.click();
};
const startVideoExport = async function() {
if (!audioSrc || !canvasRef.current || !audioCtxRef.current) {
alert("Please upload an audio file and press play at least once to initialize.");
return;
}
setIsExportingVideo(true); setExportProgress(0); chunksRef.current = [];
audioRef.current.pause();
audioRef.current.currentTime = exportStart > 0 ? exportStart : 0;
const canvasStream = canvasRef.current.captureStream(exportFps);
const audioStream = destRef.current.stream;
const combinedStream = new MediaStream([...canvasStream.getTracks(), ...audioStream.getAudioTracks()]);
let options = {}; let ext = 'webm';
const targetBitrate = resolution.startsWith('4k') ? 15000000 : 8000000;
if (exportFormat === 'mp4') {
if (MediaRecorder.isTypeSupported('video/mp4; codecs=h264')) { options = { mimeType: 'video/mp4; codecs=h264', videoBitsPerSecond: targetBitrate }; ext = 'mp4'; }
else if (MediaRecorder.isTypeSupported('video/mp4')) { options = { mimeType: 'video/mp4', videoBitsPerSecond: targetBitrate }; ext = 'mp4'; }
else { alert("Browser doesn't support MP4. Falling back to WebM."); options = { mimeType: 'video/webm; codecs=vp9', videoBitsPerSecond: targetBitrate }; }
} else {
options = { mimeType: 'video/webm; codecs=vp9', videoBitsPerSecond: targetBitrate };
if (!MediaRecorder.isTypeSupported(options.mimeType)) options = { mimeType: 'video/webm; codecs=vp8', videoBitsPerSecond: targetBitrate };
if (!MediaRecorder.isTypeSupported(options.mimeType)) options = { videoBitsPerSecond: targetBitrate };
}
try { mediaRecorderRef.current = new MediaRecorder(combinedStream, options); } catch (e) { alert("Recorder error."); setIsExportingVideo(false); return; }
mediaRecorderRef.current.ondataavailable = function(e) { if (e.data && e.data.size > 0) chunksRef.current.push(e.data); };
mediaRecorderRef.current.onstop = function() {
const blob = new Blob(chunksRef.current, { type: mediaRecorderRef.current.mimeType || 'video/mp4' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a'); link.download = `viz_${Date.now()}.${ext}`; link.href = url; link.click(); URL.revokeObjectURL(url);
setIsExportingVideo(false); setExportProgress(0);
};
const duration = audioRef.current.duration;
const progressInterval = setInterval(function() {
if (audioRef.current && !audioRef.current.paused) {
const current = audioRef.current.currentTime;
const end = (exportEnd > 0 && exportEnd < duration) ? exportEnd : duration;
const start = exportStart > 0 ? exportStart : 0;
setExportProgress(Math.min(100, Math.max(0, ((current - start) / (end - start)) * 100)));
if (current >= end) stopVideoExport();
} else { clearInterval(progressInterval); }
}, 250);
mediaRecorderRef.current.start(1000);
audioRef.current.play().then(()=>setIsPlaying(true)).catch(e => console.error(e));
};
const stopVideoExport = function() {
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') mediaRecorderRef.current.stop();
audioRef.current.pause(); setIsPlaying(false);
};
return (
<div className="min-h-screen font-sans selection:bg-violet-500/30 pb-10">
<header className="border-b border-black/5 dark:border-white/5 bg-white/80 dark:bg-zinc-950/80 backdrop-blur-xl p-5 sticky top-0 z-50 flex flex-col sm:flex-row items-center justify-between gap-4 transition-colors">
<div className="flex items-center gap-3">
<SujataLogo className="w-9 h-9 shadow-sm rounded-full drop-shadow-md" />
<h1 className="text-2xl sm:text-3xl font-black tracking-tighter text-zinc-900 dark:text-zinc-50 drop-shadow-sm">Sujata Studios</h1>
</div>
<div className="flex items-center gap-3">
<button onClick={handleResetAll} className="flex items-center gap-2 bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 border border-black/5 dark:border-white/5 px-4 py-2 rounded-xl text-sm text-zinc-700 dark:text-zinc-300 font-medium transition-all active:scale-95" title="Reset all settings to default">
<RefreshCw className="w-4 h-4" /> Reset All
</button>
<button onClick={function(){setIsDark(!isDark);}} className="flex items-center justify-center w-10 h-10 bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 border border-black/5 dark:border-white/5 rounded-xl text-zinc-600 dark:text-zinc-300 transition-all active:scale-95" title="Toggle Theme">
{isDark ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
</button>
</div>
</header>
<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 relative">
<div className="order-3 lg:order-1 lg:col-span-3 space-y-6 overflow-y-auto pr-2 custom-scrollbar lg:sticky lg:top-28 lg:h-[calc(100vh-8rem)]">
<section className="bg-white/60 dark:bg-zinc-900/40 backdrop-blur-2xl p-5 sm:p-6 rounded-3xl border border-black/5 dark:border-white/5 shadow-xl shadow-black/5 dark:shadow-2xl transition-all">
<SectionHeader title="Visual Settings" icon={Settings2} sectionKey="visual" minStates={minStates} toggleMin={toggleMin} />
{!minStates.visual && (
<div className="space-y-6 animate-in fade-in slide-in-from-top-2 duration-200">
<div>
<ControlHeader label="Style & Fill"
extra={
<label className="flex items-center gap-2 cursor-pointer group ml-4">
<span className="text-[11px] font-medium uppercase tracking-widest text-zinc-500 group-hover:text-violet-600 dark:group-hover:text-violet-400 transition-colors">Fill Shapes</span>
<div className={`w-8 h-4 rounded-full transition-colors relative ${isFilled ? 'bg-violet-500' : 'bg-zinc-300 dark:bg-zinc-800'}`}>
<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>
</div>
<input type="checkbox" className="hidden" checked={isFilled} onChange={function(e) { setIsFilled(e.target.checked); }} />
</label>
}
/>
<div className="grid grid-cols-2 gap-2 mb-3">
{[ {id: 'bars', label: 'Bars (Freq)'}, {id: 'symmetric_wave', label: 'Wave (Bars)'}, {id: 'wave', label: 'Wave (Line)'}, {id: 'circle', label: 'Circle'} ].map(function(type) {
return <button key={type.id} onClick={function() { setVizType(type.id); }} className={`py-2 px-3 rounded-xl text-sm font-medium transition-all ${vizType === type.id ? 'bg-violet-600 text-white shadow-lg shadow-violet-500/25 border border-violet-500/50' : 'bg-black/5 dark:bg-black/40 text-zinc-600 dark:text-zinc-400 border border-black/5 dark:border-white/5 hover:border-black/10 dark:hover:border-white/10 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-black/10 dark:hover:bg-black/60'}`}>{type.label}</button>;
})}
</div>
{(vizType === 'bars' || vizType === 'symmetric_wave' || vizType === 'circle') && (
<div className="flex items-center justify-between border-t border-black/5 dark:border-white/5 pt-3 mt-1 group">
<span className="text-[11px] font-bold tracking-widest uppercase text-zinc-500">Bar Shape</span>
<select value={barShape} onChange={function(e) { setBarShape(e.target.value); }} className="bg-white dark:bg-black/40 border border-black/10 dark:border-white/10 text-zinc-800 dark:text-zinc-200 text-xs rounded-lg px-2 py-1 outline-none focus:border-violet-500/50 cursor-pointer">
<option value="pill">Pill (Rounded)</option>
<option value="flat">Flat (Box)</option>
<option value="diamonds">Diamonds</option>
<option value="dots">Dots Only</option>
</select>
</div>
)}
</div>
<div>
<ControlHeader label="Frequency Band Isolation" />
<select value={freqBand} onChange={function(e) { setFreqBand(e.target.value); }} className="w-full bg-white dark:bg-black/40 border border-black/10 dark:border-white/10 text-zinc-800 dark:text-zinc-200 text-sm rounded-xl px-4 py-2.5 outline-none focus:border-violet-500/50 focus:ring-1 focus:ring-violet-500/50 transition-all cursor-pointer">
<option value="all">All Frequencies (Default)</option>
<option value="bass">Bass Only (Kick/Sub)</option>
<option value="mid">Mids Only (Vocals/Melody)</option>
<option value="treble">Treble Only (Hi-Hats)</option>
</select>
</div>
<div className="p-4 bg-zinc-100/50 dark:bg-black/20 rounded-2xl border border-black/5 dark:border-white/5 space-y-4 shadow-inner">
<ControlHeader label="Color Style" />
<div className="flex flex-wrap items-center gap-2 mb-2 w-full">
<select value={colorMode} onChange={function(e) { setColorMode(e.target.value); }} className="w-auto flex-1 min-w-[80px] bg-white dark:bg-black/60 border border-black/10 dark:border-white/10 text-zinc-800 dark:text-zinc-200 text-xs rounded-xl px-2 py-2 outline-none focus:border-violet-500/50 cursor-pointer">
<option value="solid">Solid</option><option value="gradient">Gradient</option><option value="rainbow">Rainbow</option>
</select>
{colorMode === 'gradient' && (
<select value={gradStops} onChange={function(e) { setGradStops(Number(e.target.value)); }} className="w-auto min-w-[60px] bg-white dark:bg-black/60 border border-black/10 dark:border-white/10 text-zinc-800 dark:text-zinc-200 text-xs rounded-xl px-1 py-2 outline-none focus:border-violet-500/50 cursor-pointer">
<option value={2}>2x</option><option value={3}>3x</option><option value={4}>4x</option>
</select>
)}
{colorMode !== 'rainbow' && <input type="color" value={color} onChange={function(e) { setColor(e.target.value); }} className="h-9 w-10 shrink-0 rounded-lg cursor-pointer bg-white dark:bg-black border border-black/10 dark:border-white/10" />}
{colorMode === 'gradient' && <input type="color" value={color2} onChange={function(e) { setColor2(e.target.value); }} className="h-9 w-10 shrink-0 rounded-lg cursor-pointer bg-white dark:bg-black border border-black/10 dark:border-white/10" />}
{colorMode === 'gradient' && gradStops >= 3 && <input type="color" value={color3} onChange={function(e) { setColor3(e.target.value); }} className="h-9 w-10 shrink-0 rounded-lg cursor-pointer bg-white dark:bg-black border border-black/10 dark:border-white/10 animate-in zoom-in-95" />}
{colorMode === 'gradient' && gradStops >= 4 && <input type="color" value={color4} onChange={function(e) { setColor4(e.target.value); }} className="h-9 w-10 shrink-0 rounded-lg cursor-pointer bg-white dark:bg-black border border-black/10 dark:border-white/10 animate-in zoom-in-95" />}
{colorMode === 'solid' && <input type="text" value={color} onChange={function(e) { setColor(e.target.value); }} className="flex-1 min-w-[80px] bg-white dark:bg-black/60 border border-black/10 dark:border-white/10 rounded-xl px-2 py-1.5 text-sm focus:ring-1 focus:ring-violet-500/50 outline-none uppercase font-mono text-zinc-800 dark:text-zinc-200" />}
</div>
{/* Chromatic Glitch */}
<div className="pt-4 border-t border-black/5 dark:border-white/5">
<div className="flex justify-between items-center group mb-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Chromatic Glitch</span>
<Layers className="w-4 h-4 text-cyan-500 dark:text-cyan-400" />
</div>
<label className="relative inline-flex items-center cursor-pointer shrink-0">
<input type="checkbox" checked={glitchEffect} onChange={function(e) { setGlitchEffect(e.target.checked); }} className="sr-only peer" />
<div className="w-10 h-5 bg-zinc-300 dark:bg-zinc-800 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-white/10 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyan-500"></div>
</label>
</div>
<span className="text-xs text-zinc-500 leading-snug">RGB split on heavy bass hits. (Overrides Glow)</span>
{glitchEffect && (
<div className="flex items-center gap-4 animate-in fade-in slide-in-from-top-2 mt-3 bg-white/80 dark:bg-black/40 p-3 rounded-xl border border-black/5 dark:border-white/5">
<div className="flex-1">
<div className="flex justify-between items-center text-xs text-zinc-600 dark:text-zinc-400 mb-2">
<span>Glitch Threshold</span>
<button onClick={function() { setGlitchThreshold(240); }} className="hover:text-violet-600 dark:hover:text-violet-400 transition-colors focus:outline-none"><RotateCcw className="w-3 h-3" /></button>
</div>
<input type="range" min="150" max="250" value={250 - (glitchThreshold - 150)} onChange={function(e) { setGlitchThreshold(250 - (Number(e.target.value) - 150)); }} className="w-full accent-cyan-500" />
</div>
</div>
)}
</div>
{/* Beat Flashes */}
<div className="pt-4 border-t border-black/5 dark:border-white/5">
<div className="flex justify-between items-center group mb-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Beat Flash</span>
<Zap className="w-4 h-4 text-fuchsia-500 dark:text-fuchsia-400" />
</div>
<label className="relative inline-flex items-center cursor-pointer shrink-0">
<input type="checkbox" checked={flashOnBeat} onChange={function(e) { setFlashOnBeat(e.target.checked); }} className="sr-only peer" />
<div className="w-10 h-5 bg-zinc-300 dark:bg-zinc-800 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-white/10 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-fuchsia-500"></div>
</label>
</div>
<span className="text-xs text-zinc-500 leading-snug">Enable rhythmic flashing on heavy bass.</span>
{flashOnBeat && (
<div className="flex items-center gap-4 animate-in fade-in slide-in-from-top-2 mt-3 bg-white/80 dark:bg-black/40 p-3 rounded-xl border border-black/5 dark:border-white/5">
<input type="color" value={flashColor} onChange={function(e) { setFlashColor(e.target.value); }} className="h-10 w-12 rounded-lg cursor-pointer bg-white dark:bg-black border border-black/10 dark:border-white/10 shrink-0" title="Flash Color" />
<div className="flex-1">
<div className="flex justify-between items-center text-xs text-zinc-600 dark:text-zinc-400 mb-2">
<span>Sensitivity</span>
<button onClick={function() { setFlashThreshold(230); }} className="hover:text-violet-600 dark:hover:text-violet-400 transition-colors focus:outline-none" title="Reset Sensitivity"><RotateCcw className="w-3 h-3" /></button>
</div>
<input type="range" min="150" max="250" value={250 - (flashThreshold - 150)} onChange={function(e) { setFlashThreshold(250 - (Number(e.target.value) - 150)); }} className="w-full" />
</div>
</div>
)}
</div>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center py-2 border-b border-black/5 dark:border-white/5">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Neon Glow Effect</span>
<Sparkles className="w-4 h-4 text-amber-500 dark:text-amber-400 ml-1" />
</div>
<label className="relative inline-flex items-center cursor-pointer shrink-0">
<input type="checkbox" checked={glow} onChange={function(e) { setGlow(e.target.checked); }} className="sr-only peer" />
<div className="w-11 h-6 bg-zinc-300 dark:bg-zinc-800 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-white/10 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-violet-500"></div>
</label>
</div>
<div className="flex justify-between items-center py-2 border-b border-black/5 dark:border-white/5">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Taper Edges</span>
<Activity className="w-4 h-4 text-emerald-500 dark:text-emerald-400 ml-1" />
</div>
<label className="relative inline-flex items-center cursor-pointer shrink-0">
<input type="checkbox" checked={taperEdges} onChange={function(e) { setTaperEdges(e.target.checked); }} className="sr-only peer" />
<div className="w-11 h-6 bg-zinc-300 dark:bg-zinc-800 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-white/10 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-violet-500"></div>
</label>
</div>
{(vizType === 'bars' || vizType === 'symmetric_wave' || vizType === 'circle') && (
<div className="py-2 space-y-4">
<div className="flex justify-between items-center group">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Peak Hold (Ghost Bars)</span>
</div>
<label className="relative inline-flex items-center cursor-pointer shrink-0">
<input type="checkbox" checked={showPeaks} onChange={function(e) { setShowPeaks(e.target.checked); }} className="sr-only peer" />
<div className="w-11 h-6 bg-zinc-300 dark:bg-zinc-800 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-white/10 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-violet-500"></div>
</label>
</div>
{showPeaks && (
<div className="pl-4 border-l-2 border-violet-500/20 animate-in fade-in slide-in-from-top-2">
<ControlHeader label="Peak Decay Gravity" valueDisplay={`${peakDecay.toFixed(1)}`} onReset={function() { setPeakDecay(2.0); }} />
<input type="range" min="0.1" max="10.0" step="0.1" value={peakDecay} onChange={function(e) { setPeakDecay(Number(e.target.value)); }} className="w-full" />
</div>
)}
</div>
)}
</div>
<div className="space-y-5 pt-2 border-t border-black/5 dark:border-white/5">
<div>
<ControlHeader label="Line Thickness" valueDisplay={`${thickness}px`} onReset={function() { setThickness(12); }} />
<input type="range" min="2" max="64" value={thickness} onChange={function(e) { setThickness(Number(e.target.value)); }} className="w-full" />
</div>
<div className={vizType === 'wave' ? 'opacity-30 pointer-events-none' : ''}>
<ControlHeader label="Space Between Lines" valueDisplay={`${spacing}px`} onReset={function() { setSpacing(8); }} />
<input type="range" min="0" max="64" value={spacing} onChange={function(e) { setSpacing(Number(e.target.value)); }} className="w-full" disabled={vizType === 'wave'} />
</div>
<div>
<ControlHeader label="Amplitude (Height)" valueDisplay={`${sensitivity.toFixed(1)}x`} onReset={function() { setSensitivity(1.5); }} />
<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" />
</div>
<div>
<ControlHeader label="Motion Smoothing" valueDisplay={`${Math.round(smoothing * 100)}%`} onReset={function() { setSmoothing(0.85); }} />
<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" />
</div>
</div>
</div>
)}
</section>
</div>
{/* --- CENTER CANVAS PREVIEW SECTION --- */}
<div className="order-2 lg:order-2 lg:col-span-6 flex flex-col gap-5 lg:sticky lg:top-28 lg:h-[calc(100vh-8rem)]">
<div className="bg-white/60 dark:bg-zinc-900/40 backdrop-blur-2xl rounded-3xl border border-black/5 dark:border-white/5 shadow-xl shadow-black/5 dark:shadow-2xl overflow-hidden flex-1 relative flex flex-col min-h-0 group">
<div className="p-4 border-b border-black/5 dark:border-white/5 bg-zinc-100/80 dark:bg-black/20 flex flex-wrap justify-between items-center z-10 shrink-0 gap-3 backdrop-blur-md">
<span className="text-sm font-bold tracking-wide uppercase text-zinc-800 dark:text-zinc-300 flex items-center gap-2">
Live Preview
<span className="bg-white dark:bg-black/50 text-[10px] px-2.5 py-1 rounded-full text-zinc-600 dark:text-zinc-400 border border-black/10 dark:border-white/5 tracking-wider">
{RESOLUTIONS[resolution]?.w}x{RESOLUTIONS[resolution]?.h}
</span>
</span>
<div className="flex items-center gap-3">
<span className="text-[11px] uppercase tracking-widest text-zinc-500 font-bold">Safe Zone</span>
<select
value={safeZone}
onChange={function(e) { setSafeZone(e.target.value); }}
className="bg-white dark:bg-black/40 border border-black/10 dark:border-white/10 text-zinc-800 dark:text-zinc-300 text-xs rounded-lg px-3 py-1.5 outline-none cursor-pointer focus:border-violet-500/50"
>
<option value="none">Off</option>
<option value="1:1">1:1 (Square)</option>
<option value="4:5">4:5 (Portrait)</option>
<option value="9:16">9:16 (Vertical)</option>
<option value="16:9">16:9 (Landscape)</option>
<option value="title_safe">Title Safe (10%)</option>
</select>
</div>
</div>
<div className="flex-1 w-full relative flex items-center justify-center p-4 sm:p-8 overflow-hidden bg-zinc-100 dark:bg-black/60 min-h-0"
style={ bgType === 'transparent' ? {
backgroundImage: isDark
? 'repeating-linear-gradient(45deg, #09090b 25%, transparent 25%, transparent 75%, #09090b 75%, #09090b), repeating-linear-gradient(45deg, #09090b 25%, #18181b 25%, #18181b 75%, #09090b 75%, #09090b)'
: 'repeating-linear-gradient(45deg, #f4f4f5 25%, transparent 25%, transparent 75%, #f4f4f5 75%, #f4f4f5), repeating-linear-gradient(45deg, #f4f4f5 25%, #e4e4e7 25%, #e4e4e7 75%, #f4f4f5 75%, #f4f4f5)',
backgroundPosition: '0 0, 10px 10px',
backgroundSize: '20px 20px'
} : {}}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* Drag & Drop Feedback Overlay */}
{isDraggingFile && (
<div className="absolute inset-4 z-50 bg-violet-500/20 backdrop-blur-sm border-4 border-dashed border-violet-500/50 rounded-2xl flex flex-col items-center justify-center pointer-events-none transition-all animate-in fade-in zoom-in-95">
<Upload className="w-16 h-16 text-violet-500 mb-4 animate-bounce" />
<h2 className="text-2xl font-bold text-violet-700 dark:text-violet-400 drop-shadow-lg">Drop Audio File Here</h2>
</div>
)}
<div className="relative flex items-center justify-center shadow-2xl shadow-black/10 dark:shadow-black/50 ring-1 ring-black/5 dark:ring-white/10 transition-transform duration-500 ease-out"
style={{
aspectRatio: `${RESOLUTIONS[resolution]?.w} / ${RESOLUTIONS[resolution]?.h}`,
maxHeight: '100%',
maxWidth: '100%'
}}>
{/* Floating Audio Removal Button */}
{audioSrc && (
<button
onClick={handleRemoveAudio}
className="absolute top-4 right-4 z-40 bg-white/80 dark:bg-black/60 hover:bg-red-500 dark:hover:bg-red-500 text-zinc-600 dark:text-zinc-300 hover:text-white p-2.5 rounded-xl backdrop-blur-md border border-black/10 dark:border-white/10 transition-all active:scale-95 group shadow-lg opacity-0 group-hover:opacity-100"
title="Remove Audio"
>
<Trash2 className="w-4 h-4" />
</button>
)}
<canvas
key={bgType === 'transparent' ? 'alpha' : 'solid'}
ref={canvasRef}
width={RESOLUTIONS[resolution]?.w || 3840}
height={RESOLUTIONS[resolution]?.h || 2160}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUpOrLeave}
onMouseLeave={handleMouseUpOrLeave}
className={`w-full h-full bg-transparent ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
/>
{/* Pixel Perfect Bound Safe Zones */}
{safeZone !== 'none' && (
<div
className="absolute pointer-events-none border-[3px] border-dashed border-red-500/80 bg-red-500/5 flex items-center justify-center z-30"
style={getSafeZoneStyle(safeZone, RESOLUTIONS[resolution]?.w || 3840, RESOLUTIONS[resolution]?.h || 2160)}
>
<span className="absolute top-3 left-3 text-[10px] font-bold tracking-wider text-red-600 dark:text-red-300 bg-white/80 dark:bg-black/80 px-2 py-1 rounded-md border border-red-500/30 shadow-lg backdrop-blur-md uppercase">
{safeZone === 'title_safe' ? 'Title' : safeZone} Safe Area
</span>
<div className="w-4 h-[1px] bg-red-500/50 absolute"></div>
<div className="h-4 w-[1px] bg-red-500/50 absolute"></div>
</div>
)}
</div>
{!audioSrc && !isDraggingFile && (
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none bg-white/80 dark:bg-zinc-950/80 backdrop-blur-sm z-20 transition-opacity">
<Loader2 className="w-12 h-12 text-zinc-400 dark:text-zinc-600 animate-spin mb-4" />
<p className="text-zinc-500 dark:text-zinc-400 font-medium tracking-wide uppercase text-sm">Awaiting Audio Input</p>
</div>
)}
</div>
</div>
{/* Playback Controls & Waveform */}
<div className="bg-white/60 dark:bg-zinc-900/40 backdrop-blur-2xl p-5 sm:p-6 rounded-3xl border border-black/5 dark:border-white/5 shadow-xl shadow-black/5 dark:shadow-2xl shrink-0 flex flex-col gap-4 transition-all">
{/* Interactive Waveform Timeline Component */}
<WaveformTimeline
waveformData={waveformData}
audioTime={audioTime}
audioDuration={audioDuration}
audioRef={audioRef}
setAudioTime={setAudioTime}
/>
<div className="flex items-center gap-5">
<button ref={playButtonRef} onClick={togglePlay} disabled={!audioSrc || isExportingVideo} className="w-14 h-14 shrink-0 bg-violet-600 hover:bg-violet-500 dark:bg-zinc-100 dark:hover:bg-white text-white dark:text-zinc-950 rounded-full flex items-center justify-center transition-transform hover:scale-105 active:scale-95 disabled:opacity-50 disabled:hover:scale-100 disabled:hover:bg-violet-600 shadow-xl shadow-violet-500/30 dark:shadow-white/10">
{isPlaying ? <Pause className="w-6 h-6 fill-current" /> : <Play className="w-6 h-6 fill-current ml-1" />}
</button>
<div className="flex-1 flex justify-between items-center text-xs font-mono text-zinc-600 dark:text-zinc-400">
<span className="bg-black/5 dark:bg-black/40 px-3 py-1.5 rounded-lg border border-black/5 dark:border-white/5">{formatTime(audioTime)}</span>
<span className="text-zinc-800 dark:text-zinc-300 font-sans truncate px-4 font-medium max-w-[200px] sm:max-w-xs text-center">{fileName || 'No audio selected'}</span>
<span className="bg-black/5 dark:bg-black/40 px-3 py-1.5 rounded-lg border border-black/5 dark:border-white/5">{formatTime(audioDuration)}</span>
</div>
</div>
</div>
</div>
{/* --- RIGHT SIDEBAR (Input & Setup) --- */}
<div className="order-1 lg:order-3 lg:col-span-3 space-y-6 overflow-y-auto pr-2 custom-scrollbar lg:sticky lg:top-28 lg:h-[calc(100vh-8rem)]">
<section className="bg-white/60 dark:bg-zinc-900/40 backdrop-blur-2xl p-5 sm:p-6 rounded-3xl border border-black/5 dark:border-white/5 shadow-xl shadow-black/5 dark:shadow-2xl transition-all">
<SectionHeader title="Audio Input" icon={Upload} sectionKey="audio" minStates={minStates} toggleMin={toggleMin} />
{!minStates.audio && (
<div className="animate-in fade-in slide-in-from-top-2 duration-200">
<ControlHeader label="Upload File" />
<label className="block w-full cursor-pointer bg-white/50 dark:bg-black/20 hover:bg-white/80 dark:hover:bg-black/40 transition-colors border-2 border-dashed border-black/10 dark:border-white/10 hover:border-violet-500/50 rounded-2xl p-6 sm:p-8 text-center group relative overflow-hidden">
<div className="absolute inset-0 bg-violet-500/5 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<input type="file" accept="audio/*" onChange={handleFileUpload} className="hidden" disabled={isExportingVideo} />
<div className="mx-auto w-12 h-12 bg-white dark:bg-white/5 border border-black/5 dark:border-white/10 rounded-full flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300 ease-out shadow-sm dark:shadow-lg">
<Upload className="w-5 h-5 text-violet-600 dark:text-violet-400" />
</div>
<p className="font-semibold text-sm text-zinc-800 dark:text-zinc-300">{fileName ? fileName : 'Click to browse audio file'}</p>
<p className="text-xs text-zinc-500 mt-2 tracking-wide">MP3, WAV, FLAC</p>
</label>
</div>
)}
{/* AUDIO ELEMENT: Moved OUTSIDE the collapsible block so playback never breaks when minimized */}
<audio
ref={audioRef}
src={audioSrc}
crossOrigin="anonymous"
onEnded={handleAudioEnded}
onPlay={function() { setIsPlaying(true); }}
onPause={function() { setIsPlaying(false); }}
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
className="hidden"
/>
</section>
<section className="bg-white/60 dark:bg-zinc-900/40 backdrop-blur-2xl p-5 sm:p-6 rounded-3xl border border-black/5 dark:border-white/5 shadow-xl shadow-black/5 dark:shadow-2xl transition-all">
<SectionHeader title="Transform & Symmetry" icon={RotateCcw} sectionKey="transform" minStates={minStates} toggleMin={toggleMin} />
{!minStates.transform && (
<div className="space-y-6 animate-in fade-in slide-in-from-top-2 duration-200">
<div>
<ControlHeader label="Symmetry / Mirror" />
<div className="grid grid-cols-2 gap-3 mt-2">
<label className={`flex items-center justify-center gap-2 py-2.5 px-3 rounded-xl border cursor-pointer transition-all ${mirrorX ? 'bg-violet-500/10 border-violet-500/30 text-violet-700 dark:text-violet-300 shadow-inner' : 'bg-white dark:bg-black/40 border-black/5 dark:border-white/5 text-zinc-600 dark:text-zinc-400 hover:border-black/10 dark:hover:border-white/10 hover:text-zinc-900 dark:hover:text-zinc-200'}`}>
<input type="checkbox" className="hidden" checked={mirrorX} onChange={function(e){setMirrorX(e.target.checked);}} />
<FlipHorizontal className="w-4 h-4" /> <span className="text-sm font-medium">Mirror X</span>
</label>
<label className={`flex items-center justify-center gap-2 py-2.5 px-3 rounded-xl border cursor-pointer transition-all ${mirrorY ? 'bg-violet-500/10 border-violet-500/30 text-violet-700 dark:text-violet-300 shadow-inner' : 'bg-white dark:bg-black/40 border-black/5 dark:border-white/5 text-zinc-600 dark:text-zinc-400 hover:border-black/10 dark:hover:border-white/10 hover:text-zinc-900 dark:hover:text-zinc-200'}`}>
<input type="checkbox" className="hidden" checked={mirrorY} onChange={function(e){setMirrorY(e.target.checked);}} />
<FlipVertical className="w-4 h-4" /> <span className="text-sm font-medium">Mirror Y</span>
</label>
</div>
</div>
<div className="space-y-5 pt-2">
<div>
<ControlHeader label="Size (Global Scale)" valueDisplay={`${scale.toFixed(2)}x`} onReset={function() { setScale(1.0); }} />
<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" />
</div>
<div>
<ControlHeader label="Horizontal Size (Width)" valueDisplay={`${scaleX.toFixed(2)}x`} onReset={function() { handleScaleXChange(1.0); if(scaleLock) handleScaleYChange(1.0); }}
extra={
<button onClick={toggleScaleLock} title={scaleLock ? "Unlock Aspect Ratio" : "Lock Aspect Ratio"} className={`p-1.5 ml-2 rounded-md transition-colors ${scaleLock ? 'text-violet-600 dark:text-violet-400 bg-violet-500/10' : 'text-zinc-400 dark:text-zinc-500 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-black/5 dark:hover:bg-white/5'}`}>
{scaleLock ? <Lock className="w-3.5 h-3.5" /> : <Unlock className="w-3.5 h-3.5" />}
</button>
}
/>
<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" />
</div>
<div>
<ControlHeader label="Vertical Size (Height)" valueDisplay={`${scaleY.toFixed(2)}x`} onReset={function() { handleScaleYChange(1.0); if(scaleLock) handleScaleXChange(1.0); }}
extra={
<button onClick={toggleScaleLock} title={scaleLock ? "Unlock Aspect Ratio" : "Lock Aspect Ratio"} className={`p-1.5 ml-2 rounded-md transition-colors ${scaleLock ? 'text-violet-600 dark:text-violet-400 bg-violet-500/10' : 'text-zinc-400 dark:text-zinc-500 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-black/5 dark:hover:bg-white/5'}`}>
{scaleLock ? <Lock className="w-3.5 h-3.5" /> : <Unlock className="w-3.5 h-3.5" />}
</button>
}
/>
<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" />
</div>
<div>
<ControlHeader label="Rotation" valueDisplay={`${rotation}°`} onReset={function() { setRotation(0); }} />
<input type="range" min="0" max="360" step="1" value={rotation} onChange={function(e) { setRotation(Number(e.target.value)); }} className="w-full" />
</div>
<div>
<ControlHeader label="Horizontal Position" valueDisplay={`${Math.round(offsetX)}%`} onReset={function() { setOffsetX(0); offsetRef.current.x = 0; }} />
<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" />
</div>
<div>
<ControlHeader label="Vertical Position" valueDisplay={`${Math.round(offsetY)}%`} onReset={function() { setOffsetY(0); offsetRef.current.y = 0; }} />
<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" />
</div>
</div>
</div>
)}
</section>
<section className="bg-white/60 dark:bg-zinc-900/40 backdrop-blur-2xl p-5 sm:p-6 rounded-3xl border border-black/5 dark:border-white/5 shadow-xl shadow-black/5 dark:shadow-2xl transition-all">
<SectionHeader title="Output Setup" icon={Monitor} sectionKey="output" minStates={minStates} toggleMin={toggleMin} />
{!minStates.output && (
<div className="space-y-6 animate-in fade-in slide-in-from-top-2 duration-200">
<div>
<ControlHeader label="Resolution & Ratio" />
<select value={resolution} onChange={function(e) { setResolution(e.target.value); }} disabled={isExportingVideo} className="w-full bg-white dark:bg-black/40 border border-black/10 dark:border-white/10 text-zinc-800 dark:text-zinc-200 text-sm rounded-xl px-4 py-3 outline-none focus:border-violet-500/50 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer">
<option value="4k_16_9">4K Landscape (3840x2160)</option>
<option value="1080p_16_9">1080p Landscape (1920x1080)</option>
<option value="4k_9_16">4K Vertical / Reels (2160x3840)</option>
<option value="1080p_9_16">1080p Vertical / Reels (1080x1920)</option>
</select>
</div>
<div>
<ControlHeader label="Background Environment" />
<div className="flex gap-2 mb-3">
{['transparent', 'color', 'image'].map(function(type) {
return <button key={type} onClick={function() { setBgType(type); }} className={`flex-1 py-2 px-2 rounded-xl text-xs font-semibold uppercase tracking-wider transition-all ${bgType === type ? 'bg-violet-600 text-white shadow-lg shadow-violet-500/25 border border-violet-500/50' : 'bg-black/5 dark:bg-black/40 text-zinc-600 dark:text-zinc-400 border border-black/5 dark:border-white/5 hover:border-black/10 dark:hover:border-white/10 hover:text-zinc-900 dark:hover:text-zinc-200'}`}>{type}</button>;
})}
</div>
{bgType === 'color' && (
<div className="flex items-center gap-3 mt-3 bg-white dark:bg-black/30 p-2 rounded-xl border border-black/5 dark:border-white/5">
<input type="color" value={bgColor} onChange={function(e) { setBgColor(e.target.value); }} className="h-10 w-14 rounded-lg cursor-pointer bg-white dark:bg-black border border-black/10 dark:border-white/10" />
<span className="text-sm font-mono text-zinc-700 dark:text-zinc-300 uppercase tracking-widest truncate">{bgColor}</span>
</div>
)}
{bgType === 'image' && (
<div className="mt-3 space-y-3">
<label className="flex items-center justify-center gap-2 w-full cursor-pointer bg-white/50 dark:bg-black/30 hover:bg-white dark:hover:bg-black/50 transition-colors border border-dashed border-black/10 dark:border-white/10 hover:border-violet-500/50 rounded-xl p-4 text-center text-sm font-medium text-zinc-700 dark:text-zinc-300 group">
<ImagePlus className="w-5 h-5 text-zinc-400 dark:text-zinc-500 group-hover:text-violet-600 dark:group-hover:text-violet-400 transition-colors" /> {bgImageSrc ? 'Change Image' : 'Upload Background Image'}
<input type="file" accept="image/*" onChange={handleBgUpload} className="hidden" />
</label>
{bgImageSrc && (
<div className="space-y-3 bg-zinc-100/50 dark:bg-black/20 p-3 rounded-xl border border-black/5 dark:border-white/5">
<div className="flex justify-between items-center">
<span className="text-xs font-medium text-zinc-600 dark:text-zinc-400">Image Fit</span>
<select value={bgImageFit} onChange={function(e) { setBgImageFit(e.target.value); }} className="bg-white dark:bg-black/60 border border-black/10 dark:border-white/10 text-zinc-800 dark:text-zinc-200 text-xs rounded-lg px-3 py-1.5 outline-none focus:border-violet-500/50 cursor-pointer">
<option value="contain">Contain (No Crop)</option>
<option value="cover">Cover (Fill Canvas)</option>
<option value="fit-width">Fit to Width</option>
<option value="stretch">Stretch (Exact Fit)</option>
</select>
</div>
<label className="flex justify-between items-center cursor-pointer border-t border-black/5 dark:border-white/5 pt-3">
<span className="text-xs font-medium text-zinc-700 dark:text-zinc-300 flex items-center gap-2"><Zap className="w-3.5 h-3.5 text-fuchsia-500 dark:text-fuchsia-400" /> Beat Reactive Pulse</span>
<div className="relative inline-flex items-center">
<input type="checkbox" checked={bgBeatReactive} onChange={function(e) { setBgBeatReactive(e.target.checked); }} className="sr-only peer" />
<div className="w-9 h-5 bg-zinc-300 dark:bg-zinc-800 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-white/10 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-fuchsia-500"></div>
</div>
</label>
</div>
)}
</div>
)}
</div>
<div className="pt-4 border-t border-black/5 dark:border-white/5 space-y-4">
<div className="flex justify-between items-center py-2 border-b border-black/5 dark:border-white/5">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Particle Environment</span>
<Droplet className="w-4 h-4 text-cyan-500 dark:text-cyan-400 ml-1" />
</div>
<label className="relative inline-flex items-center cursor-pointer shrink-0">
<input type="checkbox" checked={showParticles} onChange={function(e) { setShowParticles(e.target.checked); }} className="sr-only peer" />
<div className="w-11 h-6 bg-zinc-300 dark:bg-zinc-800 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-white/10 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-cyan-500"></div>
</label>
</div>
{showParticles && (
<div className="pl-4 border-l-2 border-cyan-500/20 animate-in fade-in slide-in-from-top-2 pt-2">
<ControlHeader label="Particle Density" valueDisplay={`${particleCount}`} onReset={function() { setParticleCount(150); }} />
<input type="range" min="10" max="500" step="10" value={particleCount} onChange={function(e) { setParticleCount(Number(e.target.value)); }} className="w-full accent-cyan-500" />
</div>
)}
</div>
</div>
)}
</section>
<section className="bg-white/60 dark:bg-zinc-900/40 backdrop-blur-2xl p-5 sm:p-6 rounded-3xl border border-black/5 dark:border-white/5 shadow-xl shadow-black/5 dark:shadow-2xl transition-all">
<SectionHeader title="Save & Export" icon={Save} sectionKey="export" minStates={minStates} toggleMin={toggleMin} />
{!minStates.export && (
<div className="animate-in fade-in slide-in-from-top-2 duration-200">
<div className="flex flex-col gap-3 mb-6">
<ControlHeader label="Local Configuration" />
<div className="flex gap-3">
<button onClick={savePreset} className="flex-1 bg-white dark:bg-black/40 hover:bg-zinc-50 dark:hover:bg-white/5 text-zinc-700 dark:text-zinc-300 py-2.5 rounded-xl text-sm font-medium border border-black/10 dark:border-white/5 hover:border-black/20 dark:hover:border-white/10 transition-all active:scale-95 shadow-sm">Save Preset</button>
<button onClick={loadPreset} className="flex-1 bg-white dark:bg-black/40 hover:bg-zinc-50 dark:hover:bg-white/5 text-zinc-700 dark:text-zinc-300 py-2.5 rounded-xl text-sm font-medium border border-black/10 dark:border-white/5 hover:border-black/20 dark:hover:border-white/10 transition-all active:scale-95 shadow-sm">Load Preset</button>
</div>
</div>
<div className="flex flex-col gap-4">
<div className="bg-zinc-100/50 dark:bg-black/20 p-4 rounded-2xl border border-black/5 dark:border-white/5 shadow-inner">
<ControlHeader label="Video Format & Framerate" extra={
<button onClick={function(){ setShowExportInfo(!showExportInfo); }} className="text-zinc-400 hover:text-violet-500 transition-colors focus:outline-none ml-2" title="Export Information">
<Info className="w-4 h-4" />
</button>
} />
<div className="flex flex-col 2xl:flex-row gap-3">
<select value={exportFormat} onChange={function(e) { setExportFormat(e.target.value); }} className="w-full bg-white dark:bg-black/60 border border-black/10 dark:border-white/10 text-zinc-800 dark:text-zinc-200 text-sm rounded-xl px-3 py-3 outline-none focus:border-violet-500/50 cursor-pointer">
<option value="webm">WebM (Transparent)</option>
<option value="mp4">MP4 (Solid BG / DaVinci)</option>
</select>
<select value={exportFps} onChange={function(e) { setExportFps(Number(e.target.value)); }} className="w-full 2xl:w-32 bg-white dark:bg-black/60 border border-black/10 dark:border-white/10 text-zinc-800 dark:text-zinc-200 text-sm rounded-xl px-3 py-3 outline-none focus:border-violet-500/50 cursor-pointer">
<option value={30}>30 FPS</option>
<option value={60}>60 FPS</option>
</select>
</div>
{showExportInfo && (
<div className="mt-4 p-3.5 bg-violet-500/10 border border-violet-500/20 rounded-xl animate-in fade-in slide-in-from-top-2">
<p className="text-xs text-violet-700 dark:text-violet-300 leading-relaxed font-medium">
<strong>Video Freezing/Crashing?</strong> Real-time 4K encoding is heavy. To fix this:<br/>
1. Set Framerate to <strong>30 FPS</strong>.<br/>
2. Turn off <strong>Neon Glow Effect</strong>.<br/>
3. Lower Resolution to <strong>1080p</strong>.
</p>
</div>
)}
<div className="mt-5 pt-4 border-t border-black/5 dark:border-white/5">
<ControlHeader label="Time Range (Clipping)" extra={<Scissors className="w-4 h-4 ml-2 text-zinc-400 dark:text-zinc-500" />} />
<div className="flex gap-3 items-center">
<div className="flex-1">
<span className="text-[11px] font-semibold tracking-wider uppercase text-zinc-500 block mb-1.5">Start (Sec)</span>
<input type="number" min="0" value={exportStart} onChange={function(e) { setExportStart(Number(e.target.value)); }} className="w-full bg-white dark:bg-black/60 border border-black/10 dark:border-white/10 text-zinc-800 dark:text-zinc-200 text-sm rounded-xl px-3 py-2 outline-none focus:border-violet-500/50" />
</div>
<span className="text-zinc-400 dark:text-zinc-600 mt-5 font-medium">to</span>
<div className="flex-1">
<span className="text-[11px] font-semibold tracking-wider uppercase text-zinc-500 block mb-1.5">End (0=Full)</span>
<input type="number" min="0" value={exportEnd} onChange={function(e) { setExportEnd(Number(e.target.value)); }} className="w-full bg-white dark:bg-black/60 border border-black/10 dark:border-white/10 text-zinc-800 dark:text-zinc-200 text-sm rounded-xl px-3 py-2 outline-none focus:border-violet-500/50" />
</div>
</div>
</div>
</div>
<button onClick={exportImage} disabled={isExportingVideo} className="w-full bg-white dark:bg-white/5 hover:bg-zinc-50 dark:hover:bg-white/10 text-zinc-800 dark:text-zinc-200 border border-black/10 dark:border-white/10 font-semibold py-3.5 px-4 rounded-2xl flex items-center justify-center gap-2 transition-all active:scale-95 disabled:opacity-50 disabled:hover:scale-100 shadow-sm dark:shadow-none">
<ImageIcon className="w-4 h-4" /> Save Snapshot (PNG)
</button>
{isExportingVideo ? (
<div className="w-full space-y-3 mt-2 bg-white dark:bg-black/40 p-4 rounded-2xl border border-black/5 dark:border-white/5 shadow-sm">
<button onClick={stopVideoExport} className="w-full bg-red-500 hover:bg-red-600 text-white font-bold py-3.5 px-4 rounded-xl flex items-center justify-center gap-2 transition-all active:scale-95 shadow-lg shadow-red-500/25 border border-red-400">
<StopCircle className="w-5 h-5" /> Stop & Save Early
</button>
<div className="w-full bg-zinc-200 dark:bg-black/60 rounded-full h-3 border border-black/5 dark:border-white/10 overflow-hidden shadow-inner">
<div className="bg-gradient-to-r from-violet-500 to-fuchsia-500 h-3 rounded-full transition-all duration-300" style={{ width: `${exportProgress}%` }}></div>
</div>
<p className="text-xs font-bold tracking-widest uppercase text-center text-zinc-500 dark:text-zinc-400">Recording... {Math.round(exportProgress)}%</p>
</div>
) : (
<button onClick={startVideoExport} disabled={!audioSrc} className="w-full bg-gradient-to-tr from-violet-600 to-fuchsia-500 hover:from-violet-500 hover:to-fuchsia-400 text-white font-bold py-4 px-4 rounded-2xl flex items-center justify-center gap-2 transition-all active:scale-95 shadow-xl shadow-violet-500/25 border border-violet-400/50 disabled:opacity-50 disabled:active:scale-100 mt-2">
<Video className="w-5 h-5" /> Export Video ({exportFormat.toUpperCase()})
</button>
)}
</div>
</div>
)}
</section>
</div>
</main>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>