From e28913a90615f2fa446c66035e9b57e323db4d13 Mon Sep 17 00:00:00 2001 From: orfelorfel23 Date: Sat, 8 Aug 2026 14:57:12 +0200 Subject: [PATCH] feat: upgrade to Viz 2.0 with multi-shape topologies, no-cover support, 3D grid and neon glow engine --- README.md | 63 ++-- public/app.js | 935 ++++++++++++++++++++++++++++++++-------------- public/index.html | 598 ++++++++++++++++++++--------- public/presets.js | 579 ++++++++++++++++++++-------- public/style.css | 802 +++++++++++++++++++++++---------------- 5 files changed, 2009 insertions(+), 968 deletions(-) diff --git a/README.md b/README.md index 5e4fc13..ed0709c 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,40 @@ -# 🎵 VIZ — Audio Visualizer Studio für DaVinci Resolve +# 🎵 VIZ 2.0 — Audio Visualizer Studio für DaVinci Resolve -Ein modernes, hochgradig anpassbares Audio-Visualizer Tool zur Erstellung professioneller Musikvideos und Social-Media-Clips (YouTube, TikTok, Shorts, Reels) mit direktem Export von **transparenten Videos (ProRes 4444 mit Alpha-Kanal)** für **DaVinci Resolve**. +Ein modernes, hochgradig anpassbares Audio-Visualizer Studio zur Erstellung professioneller Musikvideos und Social-Media-Clips (YouTube, TikTok, Shorts, Reels) mit direktem Export von **transparenten Videos (ProRes 4444 mit Alpha-Kanal)** für **DaVinci Resolve**. --- -## ✨ Features +## ✨ Neue Features in Version 2.0 -- **⭕ Zentraler Kreis & Artwork-Core**: - - Dynamischer Bass-Pulse & Bounce im Takt (sub-bass / kick-reaktiv). - - 0–360° Fortschrittsring um das Artwork. - - Vinyl-Drehung & Beat-Drehimpuls bei Drops. - - Randfarben, Stärken & Glow frei anpassbar. -- **📊 360° Frequenzspektrum**: - - 4 Modi: **Balken (Bars)**, **Flüssige Welle (Wave)**, **Punkte (Dots)** und **Dual Ring**. - - Anpassbare Balkenanzahl, Höhe, Frequenzempfindlichkeit & Symmetrie-Spiegelung. - - Eigener Gradient-Editor (Winkelverlauf, Radial, Linear, Regenbogen). -- **✨ Partikel- & Raum-Effekte**: - - Schwebender Sternenstaub & Warp-Explosion bei Drops. - - Dynamischer Bass-Shake & Cam-Zoom. - - RGB-Split / Chromatic Aberration & Beat-Flash. -- **📝 Typografie & Safe-Zones**: - - Titel, Artist und Live-Timecode Overlay. - - Umschaltung zwischen **16:9** (YouTube 1080p/4K), **9:16** (Shorts/TikTok) und **1:1** (Instagram). - - Einblendbares **Shorts Safe-Zone Overlay** zur optimalen Platzierung. -- **🎬 DaVinci Resolve Export**: +- **📐 Vielfältige Formen & Topologien**: + - **⭕ Kreis (Radial)**: Klassischer Look à la Trap Nation / Monstercat. + - **➖ Linear & Horizont**: Symmetrischer Horizont, Boden-Equalizer, Decken-Equalizer oder schwebende Wellenform. + - **⬡ Polygone**: **Hexagon (6-Eck)**, **Raute / Diamond**, **Dreieck** und **Achteck** mit passender Cover-Maske. + - **〰️ Oszilloskop / Laser-Welle**: Flüssige, reaktive Neon-Audiowellenform mit glühendem Laserkern. + - **🌀 3D Tunnel / Wormhole**: Konzentrische Tiefenringe mit kick-reaktiven Impulswellen. + +- **🎛️ Vollständige Deaktivierbarkeit (No-Cover Modus & Modularer Layer-Stack)**: + - **Cover-Artwork komplett ausblendbar**: Perfekt für minimalistische Wellenformen, futuristische Horizonte oder reine 3D-Grid Visualizer ohne Cover-Bild. + - **Jedes einzelne Feature ein-/ausschaltbar**: Cover, Spektrum, Rand, Fortschrittsring, 3D Grid, Partikel, Bass-Shake, Glitch, Beat-Flash, Bass-Zoom, Typography. + +- **🎨 Multi-Pass Neon Glow & Gradienten**: + - 2- und 3-Farben-Farbverläufe (Conic Angular, Radial, Horizontal, Vertikal) und dynamischer Rainbow/Chroma Beat-Loop. + - **1-Klick Farbpaletten**: *Cyberpunk, Synthwave Sunset, Toxic Emerald, Ice Blade, Fire Amber, Royal Violet, Monochrome*. + - Echter Multi-Pass Neon Glow mit Bloom Spread und hellem Laserkern. + +- **✨ 3D Atmosphäre & FX**: + - **3D Synthwave Grid Floor**: Audio-reaktiver Gitterboden mit Tiefenperspektive. + - **Partikel-System**: 3D Warp Starfield (Kameraflug), organischer schwebender Staub oder Audio-Funken. + - **Kamera-Dynamik**: Bass-Shake, Cam-Zoom, RGB-Split Glitch und Strobe Beat-Flash. + +- **🎬 DaVinci Resolve Alpha-Export**: - **QuickTime ProRes 4444 (.mov)** mit echtem 100% Alpha-Kanal für verlustfreie Studioqualität. - **WebM VP9 (.webm)** mit Alpha-Kanal für kompakte Dateigrößen. - **MP4 (H.264)** für fertige Standalone-Videos. -- **💾 Preset-Manager**: - - Vorgefertigte Styles (*Trap Nation Classic*, *Cyberpunk Neon*, *Minimal Clean*, *Fire & Amber*, *Matrix Glitch*, *Deep Violet*). - - Eigene Presets als JSON speichern, exportieren und importieren. + +- **💾 Presets & Formate**: + - 8 kuratierte Presets (*Trap Nation, Cyber Hexagon, Synthwave Horizon, Laser Oscilloscope, Diamond Bass, Cosmic Wormhole, Studio Graphic EQ, Minimal Ice Wave*). + - Seitenverhältnisse: **16:9** (YouTube 1080p/4K), **9:16** (Shorts/TikTok) und **1:1** (Instagram). --- @@ -55,28 +60,28 @@ Die Web-App ist nun unter **`http://localhost:849`** erreichbar. --- -## 🎬 DaVinci Resolve Workflow (So nutzt du das exportierte Video) +## 🎬 DaVinci Resolve Workflow 1. Öffne dein Projekt in **DaVinci Resolve**. 2. Ziehe die aus **VIZ** exportierte `.mov` Datei (ProRes 4444 mit Alpha) in deinen Media Pool. 3. Lege deinen Hintergrund (Video, B-Roll oder KI-Animation) auf **Video-Spur 1 (V1)**. 4. Ziehe den exportierten Visualizer-Clip auf **Video-Spur 2 (V2)** direkt darüber. -5. Da der Hintergrund der Datei transparent ist, legt sich der pulsierende Kreis und das Spektrum perfekt über dein Footage! +5. Da der Hintergrund der Datei transparent ist, legt sich der Visualizer perfekt über dein Footage! --- -## ☁️ Deployment in Jannik-Cloud (Docker Compose) +## ☁️ Deployment in Jannik-Cloud VIZ ist als nativer Service in **Jannik-Cloud** integriert: - **Subdomain**: `https://viz.orfel.de` - **Port**: `849` -- **Volume**: `/mnt/Jannik-Cloud-Volume-01/viz:/data` (Upload-Zwischenspeicher mit automatischem Cleanup bei jedem Rebuild). +- **Volume**: `/mnt/Jannik-Cloud-Volume-01/viz:/data` --- ## 🛠️ Technologien -- **Frontend**: HTML5 Canvas (60 FPS Multi-Layer Rendering), Web Audio API, Vanilla CSS (Glassmorphism Dark Theme). +- **Frontend**: HTML5 Canvas (60 FPS Multi-Layer Rendering Engine), Web Audio API, Vanilla CSS (Glassmorphism Dark Theme). - **Backend**: Node.js, Express, Multer (200 MB Upload-Limit), FFmpeg Video Rendering Engine. --- diff --git a/public/app.js b/public/app.js index cedcd15..1a53d4d 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,6 @@ /** - * VIZ Audio Visualizer Studio — Core Application & Rendering Engine + * VIZ 2.0 Audio Visualizer Studio — Core Application & Rendering Engine + * Next-Gen Multi-Shape, Neon Glow & Modular Layer Renderer */ (function () { @@ -33,7 +34,7 @@ shakeY: 0, shakeZoom: 1.0, beatEnergy: 0, - isBeatDrop: false, + gridOffset: 0, // Particles Pool particles: [], @@ -56,6 +57,7 @@ const imageFileInput = document.getElementById('imageFileInput'); const audioFileBadge = document.getElementById('audioFileBadge'); const imageFileBadge = document.getElementById('imageFileBadge'); + const coverUploadContainer = document.getElementById('coverUploadContainer'); const btnPlayPause = document.getElementById('btnPlayPause'); const iconPlay = document.getElementById('iconPlay'); @@ -91,33 +93,30 @@ const btnExportPresetJson = document.getElementById('btnExportPresetJson'); const importPresetInput = document.getElementById('importPresetInput'); - // Generate Default Stylish Synthwave Cover Placeholder + // Generate Default Synthwave Cover function generateDefaultCover() { const off = document.createElement('canvas'); off.width = 600; off.height = 600; const c = off.getContext('2d'); - // Gradient Background const grad = c.createLinearGradient(0, 0, 600, 600); - grad.addColorStop(0, '#101728'); - grad.addColorStop(0.5, '#1e1136'); - grad.addColorStop(1, '#0b0c16'); + grad.addColorStop(0, '#0d1322'); + grad.addColorStop(0.5, '#1e0d2b'); + grad.addColorStop(1, '#080a12'); c.fillStyle = grad; c.fillRect(0, 0, 600, 600); - // Glowing Sun - const sunGrad = c.createRadialGradient(300, 240, 10, 300, 240, 180); + const sunGrad = c.createRadialGradient(300, 240, 10, 300, 240, 190); sunGrad.addColorStop(0, '#ff007f'); sunGrad.addColorStop(0.6, '#ffaa00'); sunGrad.addColorStop(1, 'transparent'); c.fillStyle = sunGrad; c.beginPath(); - c.arc(300, 240, 180, 0, Math.PI * 2); + c.arc(300, 240, 190, 0, Math.PI * 2); c.fill(); - // Geometric Grid Lines - c.strokeStyle = 'rgba(0, 240, 255, 0.4)'; + c.strokeStyle = 'rgba(0, 240, 255, 0.35)'; c.lineWidth = 2; for (let i = 0; i <= 600; i += 40) { c.beginPath(); @@ -132,11 +131,10 @@ c.stroke(); } - // Modern V Icon / Logo in center c.fillStyle = '#ffffff'; c.shadowColor = '#00f0ff'; - c.shadowBlur = 20; - c.font = '900 120px Orbitron, sans-serif'; + c.shadowBlur = 24; + c.font = '900 110px Orbitron, sans-serif'; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText('VIZ', 300, 250); @@ -148,7 +146,7 @@ state.coverImage = generateDefaultCover(); - // --- Initialize Audio Context --- + // --- Audio Engine Setup --- function initAudioContext() { if (!state.audioCtx) { const AudioCtxClass = window.AudioContext || window.webkitAudioContext; @@ -159,7 +157,6 @@ state.gainNode = state.audioCtx.createGain(); state.gainNode.gain.value = parseFloat(volumeSlider.value); - state.gainNode.connect(state.audioCtx.destination); } if (state.audioCtx.state === 'suspended') { @@ -167,54 +164,67 @@ } } - // --- Audio Loading --- async function loadAudioFile(file) { + if (!file) return; initAudioContext(); - state.audioFile = file; - audioFileBadge.textContent = file.name; + stopPlayback(); + + audioFileBadge.textContent = 'Lade Audio-Datei...'; audioFileBadge.classList.remove('hidden'); + state.audioFile = file; + const arrayBuffer = await file.arrayBuffer(); - state.audioBuffer = await state.audioCtx.decodeAudioData(arrayBuffer); - state.duration = state.audioBuffer.duration; - state.audioLoaded = true; + state.audioCtx.decodeAudioData( + arrayBuffer, + (decoded) => { + state.audioBuffer = decoded; + state.duration = decoded.duration; + state.audioLoaded = true; - // Update track title default if empty - const rawName = file.name.replace(/\.[^/.]+$/, ''); - const titleInput = document.getElementById('trackTitle'); - if (titleInput && (titleInput.value === 'Dein Songtitel' || !titleInput.value)) { - titleInput.value = rawName; - } + durationTimeEl.textContent = formatTime(state.duration); + seekScrubber.max = state.duration; + seekScrubber.value = 0; + currentTimeEl.textContent = '00:00'; + document.getElementById('timelineProgressBar').style.width = '0%'; - durationTimeEl.textContent = formatTime(state.duration); - seekScrubber.max = state.duration; - seekScrubber.value = 0; - state.pauseOffset = 0; + audioFileBadge.textContent = `🎵 ${file.name} (${formatTime(state.duration)})`; - stopPlayback(); - startPlayback(0); + // Extract metadata if empty + const titleInput = document.getElementById('trackTitle'); + if (titleInput && (!titleInput.value || titleInput.value === 'Dein Songtitel')) { + const clean = file.name.replace(/\.[^/.]+$/, '').replace(/[_-]/g, ' '); + titleInput.value = clean; + } + + startPlayback(0); + }, + (err) => { + alert('Fehler beim Dekodieren der Audio-Datei: ' + err); + audioFileBadge.textContent = 'Fehler beim Laden'; + } + ); } - // --- Image Loading --- function loadImageFile(file) { + if (!file) return; state.imageFile = file; - imageFileBadge.textContent = file.name; - imageFileBadge.classList.remove('hidden'); const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { state.coverImage = img; + imageFileBadge.textContent = `🖼️ ${file.name}`; + imageFileBadge.classList.remove('hidden'); }; img.src = e.target.result; }; reader.readAsDataURL(file); } - // --- Playback Controls --- - function startPlayback(offset = 0) { - if (!state.audioBuffer) return; + function startPlayback(offsetSeconds = 0) { + if (!state.audioLoaded || !state.audioBuffer) return; initAudioContext(); if (state.audioSource) { @@ -227,22 +237,28 @@ state.audioSource = state.audioCtx.createBufferSource(); state.audioSource.buffer = state.audioBuffer; state.audioSource.loop = state.loop; - state.audioSource.connect(state.analyser); state.analyser.connect(state.gainNode); - state.startTime = state.audioCtx.currentTime - offset; - state.audioSource.start(0, offset); - state.isPlaying = true; + state.startTime = state.audioCtx.currentTime - offsetSeconds; + state.pauseOffset = offsetSeconds; + state.audioSource.start(0, offsetSeconds); + state.isPlaying = true; iconPlay.classList.add('hidden'); iconPause.classList.remove('hidden'); + + state.audioSource.onended = () => { + if (!state.loop && state.isPlaying) { + pausePlayback(); + state.pauseOffset = 0; + } + }; } function pausePlayback() { - if (!state.isPlaying) return; - if (state.audioSource) { - state.pauseOffset = (state.audioCtx.currentTime - state.startTime) % state.duration; + if (state.audioSource && state.isPlaying) { + state.pauseOffset = (state.audioCtx.currentTime - state.startTime) % (state.duration || 1); try { state.audioSource.stop(); state.audioSource.disconnect(); @@ -292,29 +308,25 @@ x: (Math.random() - 0.5) * state.width, y: (Math.random() - 0.5) * state.height, z: Math.random() * 1000 + 100, - radius: Math.random() * 2.5 + 1.0, + radius: Math.random() * 2.8 + 1.0, alpha: Math.random() * 0.7 + 0.3, speedMultiplier: Math.random() * 0.8 + 0.6, - colorOffset: Math.random() + vx: (Math.random() - 0.5) * 1.5, + vy: (Math.random() - 0.5) * 1.5 }); } } // --- Dynamic Color Gradient Builder --- - function createVisualizerGradient(ctx, style, c1, c2, radius, angle, time) { - if (style === 'angular') { - const grad = ctx.createConicGradient(angle, 0, 0); - grad.addColorStop(0, c1); - grad.addColorStop(0.5, c2); - grad.addColorStop(1, c1); - return grad; - } else if (style === 'radial') { - const grad = ctx.createRadialGradient(0, 0, radius * 0.8, 0, 0, radius * 2.2); - grad.addColorStop(0, c1); - grad.addColorStop(1, c2); - return grad; - } else if (style === 'rainbow') { - const grad = ctx.createConicGradient(time * 0.5, 0, 0); + function createVisualizerGradient(targetCtx, cfg, radius, angle, time) { + const mode = cfg.colorMode || 'gradient_2'; + const c1 = cfg.colorGrad1 || '#00f0ff'; + const c2 = cfg.colorGrad2 || '#ff007f'; + const c3 = cfg.colorGrad3 || '#7000ff'; + const style = cfg.gradientStyle || 'angular'; + + if (mode === 'rainbow') { + const grad = targetCtx.createConicGradient(time * 0.6, 0, 0); grad.addColorStop(0, '#ff0055'); grad.addColorStop(0.2, '#ffaa00'); grad.addColorStop(0.4, '#00ff88'); @@ -322,20 +334,90 @@ grad.addColorStop(0.8, '#8a2be2'); grad.addColorStop(1, '#ff0055'); return grad; + } + + if (mode === 'solid') { + return c1; + } + + if (style === 'angular') { + const grad = targetCtx.createConicGradient(angle, 0, 0); + if (mode === 'gradient_3') { + grad.addColorStop(0, c1); + grad.addColorStop(0.33, c2); + grad.addColorStop(0.66, c3); + grad.addColorStop(1, c1); + } else { + grad.addColorStop(0, c1); + grad.addColorStop(0.5, c2); + grad.addColorStop(1, c1); + } + return grad; + } else if (style === 'radial') { + const grad = targetCtx.createRadialGradient(0, 0, radius * 0.4, 0, 0, radius * 2.2); + if (mode === 'gradient_3') { + grad.addColorStop(0, c1); + grad.addColorStop(0.5, c2); + grad.addColorStop(1, c3); + } else { + grad.addColorStop(0, c1); + grad.addColorStop(1, c2); + } + return grad; + } else if (style === 'linear_v') { + const grad = targetCtx.createLinearGradient(0, -radius * 1.5, 0, radius * 1.5); + if (mode === 'gradient_3') { + grad.addColorStop(0, c1); + grad.addColorStop(0.5, c2); + grad.addColorStop(1, c3); + } else { + grad.addColorStop(0, c1); + grad.addColorStop(1, c2); + } + return grad; } else { - const grad = ctx.createLinearGradient(-radius * 1.5, 0, radius * 1.5, 0); - grad.addColorStop(0, c1); - grad.addColorStop(1, c2); + // Horizontal linear + const grad = targetCtx.createLinearGradient(-radius * 1.5, 0, radius * 1.5, 0); + if (mode === 'gradient_3') { + grad.addColorStop(0, c1); + grad.addColorStop(0.5, c2); + grad.addColorStop(1, c3); + } else { + grad.addColorStop(0, c1); + grad.addColorStop(1, c2); + } return grad; } } - // --- Main Render Frame Function (Pure & Frame-Accuracy Support) --- + // --- Helper: Polygon Path Generator --- + function drawPolygonPath(targetCtx, sides, radius, cornerRadius = 0) { + if (sides === 0) { + // Circle + targetCtx.arc(0, 0, radius, 0, Math.PI * 2); + return; + } + + const angleStep = (Math.PI * 2) / sides; + const startAngle = -Math.PI / 2; // Point top + + targetCtx.beginPath(); + for (let i = 0; i < sides; i++) { + const a = startAngle + i * angleStep; + const x = Math.cos(a) * radius; + const y = Math.sin(a) * radius; + if (i === 0) targetCtx.moveTo(x, y); + else targetCtx.lineTo(x, y); + } + targetCtx.closePath(); + } + + // --- Main Render Frame Function (Pure & Frame-Accurate) --- function renderFrame(targetCtx, width, height, freqData, currentTime, duration, cfg) { targetCtx.save(); targetCtx.clearRect(0, 0, width, height); - // Analyze Frequency Bands + // 1. Analyze Frequency Bands let subBassSum = 0; let midBassSum = 0; let trebleSum = 0; @@ -349,42 +431,45 @@ const midBassAvg = (midBassSum / 20) / 255; const trebleAvg = (trebleSum / 100) / 255; - const rawBassEnergy = Math.max(subBassAvg * 1.2, midBassAvg); + const rawBassEnergy = Math.max(subBassAvg * 1.25, midBassAvg); state.beatEnergy = rawBassEnergy; // Target Pulse calculation with smooth attack/decay const targetPulse = 1.0 + (rawBassEnergy * cfg.pulseIntensity * 0.6); const decay = cfg.pulseDecay || 0.8; if (targetPulse > state.currentPulse) { - state.currentPulse = targetPulse; // Fast attack + state.currentPulse = targetPulse; } else { - state.currentPulse = state.currentPulse * decay + targetPulse * (1 - decay); // Smooth decay + state.currentPulse = state.currentPulse * decay + targetPulse * (1 - decay); } - // Camera Shake & Zoom calculation + // Camera Shake & Bass Zoom calculation let shakeZoom = 1.0; let shakeX = 0; let shakeY = 0; - if (cfg.camShakeIntensity > 0 && rawBassEnergy > 0.4) { - const shakeFactor = (rawBassEnergy - 0.4) * cfg.camShakeIntensity * 35; + if (cfg.enableShake && cfg.camShakeIntensity > 0 && rawBassEnergy > 0.35) { + const shakeFactor = (rawBassEnergy - 0.35) * cfg.camShakeIntensity * 38; shakeX = (Math.random() - 0.5) * shakeFactor; shakeY = (Math.random() - 0.5) * shakeFactor; - shakeZoom = 1.0 + (rawBassEnergy - 0.4) * cfg.camShakeIntensity * 0.08; + } + + if (cfg.enableBassZoom && rawBassEnergy > 0.3) { + shakeZoom = 1.0 + (rawBassEnergy - 0.3) * 0.07; } // Rotation calculation const rpm = cfg.rotationSpeed || 0; state.currentRotation += (rpm * Math.PI * 2 / (60 * 60)); if (cfg.beatSpinImpulse && rawBassEnergy > 0.7) { - state.currentRotation += (rawBassEnergy - 0.7) * 0.04; + state.currentRotation += (rawBassEnergy - 0.7) * 0.045; } - // --- Layer 1: Background --- - if (cfg.backgroundMode === 'blurred_cover' && state.coverImage) { + // --- LAYER 1: Background Mode --- + if (cfg.backgroundMode === 'blurred_cover' && state.coverImage && cfg.showCover) { targetCtx.save(); - targetCtx.filter = 'blur(40px) brightness(0.4)'; - const scale = Math.max(width / state.coverImage.width, height / state.coverImage.height) * 1.2 * shakeZoom; + targetCtx.filter = 'blur(45px) brightness(0.35)'; + const scale = Math.max(width / state.coverImage.width, height / state.coverImage.height) * 1.25 * shakeZoom; targetCtx.drawImage( state.coverImage, width / 2 - (state.coverImage.width * scale) / 2 + shakeX, @@ -394,19 +479,63 @@ ); targetCtx.restore(); } else if (cfg.backgroundMode === 'dark_vignette') { - const vig = targetCtx.createRadialGradient(width / 2, height / 2, width * 0.2, width / 2, height / 2, width * 0.7); - vig.addColorStop(0, '#121622'); - vig.addColorStop(1, '#05070a'); + const vig = targetCtx.createRadialGradient(width / 2, height / 2, width * 0.2, width / 2, height / 2, width * 0.75); + vig.addColorStop(0, '#101524'); + vig.addColorStop(1, '#040609'); targetCtx.fillStyle = vig; targetCtx.fillRect(0, 0, width, height); } else if (cfg.backgroundMode === 'solid_black') { targetCtx.fillStyle = '#000000'; targetCtx.fillRect(0, 0, width, height); - } // If 'transparent', clearRect leaves it 100% alpha! + } // Transparent keeps pure alpha! - // --- Layer 2: Beat Flash FX --- + // --- LAYER 2: 3D Synthwave Grid Floor --- + if (cfg.enableGrid) { + targetCtx.save(); + const horizonY = height * (cfg.gridPerspective || 0.65); + const gridColor = cfg.colorGrid || '#ff007f'; + const speed = 120 * (1.0 + rawBassEnergy * 1.5); + state.gridOffset = (state.gridOffset + speed / 60) % 50; + + targetCtx.strokeStyle = gridColor; + targetCtx.shadowColor = gridColor; + targetCtx.shadowBlur = cfg.enableGlow ? (15 * (cfg.glowIntensity || 0.8)) : 0; + targetCtx.lineWidth = 1.5; + targetCtx.globalAlpha = 0.85; + + // Horizon glow line + targetCtx.beginPath(); + targetCtx.moveTo(0, horizonY); + targetCtx.lineTo(width, horizonY); + targetCtx.stroke(); + + // Perspective vertical lines (radiating to vanishing point) + const numLines = 26; + for (let i = 0; i <= numLines; i++) { + const xBottom = (width / numLines) * i; + const xVanishing = width / 2 + (xBottom - width / 2) * 0.15; + targetCtx.beginPath(); + targetCtx.moveTo(xVanishing, horizonY); + targetCtx.lineTo(xBottom, height); + targetCtx.stroke(); + } + + // Horizontal depth lines + for (let y = 0; y < 14; y++) { + const t = Math.pow((y + state.gridOffset / 50) / 14, 2.2); + const curY = horizonY + (height - horizonY) * t; + const wave = cfg.gridAudioReactive ? Math.sin(y * 0.8 + currentTime * 4) * (rawBassEnergy * 18) : 0; + targetCtx.beginPath(); + targetCtx.moveTo(0, curY + wave); + targetCtx.lineTo(width, curY + wave); + targetCtx.stroke(); + } + targetCtx.restore(); + } + + // --- LAYER 3: Beat Flash FX --- if (cfg.beatFlash && rawBassEnergy > 0.85) { - targetCtx.fillStyle = `rgba(255, 255, 255, ${(rawBassEnergy - 0.85) * 0.4})`; + targetCtx.fillStyle = `rgba(255, 255, 255, ${(rawBassEnergy - 0.85) * 0.45})`; targetCtx.fillRect(0, 0, width, height); } @@ -415,67 +544,73 @@ targetCtx.translate(width / 2 + shakeX, height / 2 + shakeY); targetCtx.scale(shakeZoom, shakeZoom); - // --- Layer 3: Particle System --- + // --- LAYER 4: Particle System (3D Starfield / Dust / Sparks) --- if (cfg.enableParticles && state.particles.length > 0) { targetCtx.save(); + const pType = cfg.particleType || 'starfield'; const speedMult = cfg.particleSpeed * (1.0 + (rawBassEnergy * cfg.particleWarp)); for (let i = 0; i < state.particles.length; i++) { const p = state.particles[i]; - p.z -= speedMult * p.speedMultiplier * 5; - // Warp loop - if (p.z <= 10) { - p.z = 1000; - p.x = (Math.random() - 0.5) * width; - p.y = (Math.random() - 0.5) * height; - } + if (pType === 'starfield') { + p.z -= speedMult * p.speedMultiplier * 6; + if (p.z <= 10) { + p.z = 1000; + p.x = (Math.random() - 0.5) * width; + p.y = (Math.random() - 0.5) * height; + } + const k = 400 / p.z; + const px = p.x * k; + const py = p.y * k; + const pSize = Math.max(0.5, p.radius * k); - const k = 400 / p.z; - const px = p.x * k; - const py = p.y * k; - const pSize = Math.max(0.5, p.radius * k); + if (Math.abs(px) < width / 2 && Math.abs(py) < height / 2) { + targetCtx.fillStyle = cfg.colorParticle || '#ffffff'; + targetCtx.globalAlpha = Math.min(1.0, p.alpha * (1 - p.z / 1000)); + targetCtx.beginPath(); + targetCtx.arc(px, py, pSize, 0, Math.PI * 2); + targetCtx.fill(); + } + } else if (pType === 'dust') { + p.x += p.vx * speedMult; + p.y += p.vy * speedMult; + if (p.x < -width / 2) p.x = width / 2; + if (p.x > width / 2) p.x = -width / 2; + if (p.y < -height / 2) p.y = height / 2; + if (p.y > height / 2) p.y = -height / 2; - if (Math.abs(px) < width / 2 && Math.abs(py) < height / 2) { targetCtx.fillStyle = cfg.colorParticle || '#ffffff'; - targetCtx.globalAlpha = Math.min(1.0, p.alpha * (1 - p.z / 1000)); + targetCtx.globalAlpha = p.alpha * 0.6; targetCtx.beginPath(); - targetCtx.arc(px, py, pSize, 0, Math.PI * 2); + targetCtx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); + targetCtx.fill(); + } else if (pType === 'sparks') { + p.x += p.vx * speedMult * (1 + rawBassEnergy * 3); + p.y += p.vy * speedMult * (1 + rawBassEnergy * 3); + if (Math.hypot(p.x, p.y) > width * 0.6) { + p.x = (Math.random() - 0.5) * (cfg.circleRadius || 100); + p.y = (Math.random() - 0.5) * (cfg.circleRadius || 100); + } + targetCtx.fillStyle = cfg.colorParticle || '#ffffff'; + targetCtx.globalAlpha = Math.min(1.0, p.alpha * (0.8 + rawBassEnergy * 0.5)); + targetCtx.beginPath(); + targetCtx.arc(p.x, p.y, p.radius * 1.2, 0, Math.PI * 2); targetCtx.fill(); } } targetCtx.restore(); } - // --- Layer 4: Spectrum Rendering --- - const baseRadius = cfg.circleRadius * state.currentPulse; + // Prepare Shape Geometry Values + const shape = cfg.shapeType || 'circle'; + const baseRadius = (cfg.circleRadius || 150) * state.currentPulse; const barCount = cfg.barCount || 96; const maxHeight = cfg.spectrumMaxHeight || 140; const barSpacing = cfg.barSpacing || 10; const sensitivity = cfg.freqSensitivity || 1.3; - // Build Gradient - const spectrumGrad = createVisualizerGradient( - targetCtx, - cfg.gradientStyle, - cfg.colorGrad1, - cfg.colorGrad2, - baseRadius, - state.currentRotation, - currentTime - ); - - targetCtx.save(); - targetCtx.fillStyle = spectrumGrad; - targetCtx.strokeStyle = spectrumGrad; - - // Glow Effect - if (cfg.glowIntensity > 0) { - targetCtx.shadowColor = cfg.colorGrad1; - targetCtx.shadowBlur = cfg.glowBlur * (0.8 + rawBassEnergy * cfg.glowIntensity); - } - - // Filtered frequency array based on focus + // Filter Frequency Array for Bars const processedBars = []; for (let i = 0; i < barCount; i++) { let freqIndex; @@ -496,88 +631,239 @@ processedBars.push(Math.max(2, heightVal)); } - // Draw Spectrum by Mode - if (cfg.spectrumMode === 'radial_bars') { - const angleStep = (Math.PI * 2) / barCount; - const barW = cfg.barWidth || 4; + // Build Visualizer Color Gradient + const spectrumGrad = createVisualizerGradient( + targetCtx, + cfg, + baseRadius, + state.currentRotation, + currentTime + ); - for (let i = 0; i < barCount; i++) { - const angle = i * angleStep; - const barH = processedBars[i]; - const rStart = baseRadius + barSpacing; - - targetCtx.save(); - targetCtx.rotate(angle); - targetCtx.fillRect(-barW / 2, -rStart - barH, barW, barH); - targetCtx.restore(); - } - } else if (cfg.spectrumMode === 'smooth_wave') { - const angleStep = (Math.PI * 2) / barCount; - targetCtx.beginPath(); - for (let i = 0; i <= barCount; i++) { - const idx = i % barCount; - const angle = i * angleStep; - const r = baseRadius + barSpacing + processedBars[idx]; - const x = Math.cos(angle - Math.PI / 2) * r; - const y = Math.sin(angle - Math.PI / 2) * r; - - if (i === 0) targetCtx.moveTo(x, y); - else targetCtx.lineTo(x, y); - } - targetCtx.closePath(); - targetCtx.lineWidth = cfg.barWidth * 1.5; - targetCtx.stroke(); - } else if (cfg.spectrumMode === 'floating_dots') { - const angleStep = (Math.PI * 2) / barCount; - for (let i = 0; i < barCount; i++) { - const angle = i * angleStep; - const r = baseRadius + barSpacing + processedBars[i]; - const x = Math.cos(angle - Math.PI / 2) * r; - const y = Math.sin(angle - Math.PI / 2) * r; - - targetCtx.beginPath(); - targetCtx.arc(x, y, cfg.barWidth * 1.2, 0, Math.PI * 2); - targetCtx.fill(); - } - } else if (cfg.spectrumMode === 'dual_ring') { - const angleStep = (Math.PI * 2) / barCount; - const barW = cfg.barWidth || 3; - - // Inner Ring (Bass) - for (let i = 0; i < barCount; i++) { - const angle = i * angleStep; - const barH = processedBars[i] * 0.7; - const rStart = baseRadius + barSpacing; - - targetCtx.save(); - targetCtx.rotate(angle); - targetCtx.fillRect(-barW / 2, -rStart - barH, barW, barH); - targetCtx.restore(); - } - - // Outer Ring (Treble Dots) - targetCtx.fillStyle = cfg.colorGrad2; - for (let i = 0; i < barCount; i += 2) { - const angle = i * angleStep + (currentTime * 0.2); - const r = baseRadius + barSpacing + maxHeight * 0.8 + (processedBars[i] * 0.4); - const x = Math.cos(angle) * r; - const y = Math.sin(angle) * r; - - targetCtx.beginPath(); - targetCtx.arc(x, y, barW, 0, Math.PI * 2); - targetCtx.fill(); - } - } - targetCtx.restore(); - - // --- Layer 5: Central Circle Artwork --- - targetCtx.save(); - targetCtx.beginPath(); - targetCtx.arc(0, 0, baseRadius, 0, Math.PI * 2); - targetCtx.clip(); - - if (state.coverImage) { + // --- LAYER 5: 3D Tunnel / Wormhole (If Shape === 'tunnel') --- + if (shape === 'tunnel') { targetCtx.save(); + const numRings = 10; + for (let r = numRings; r >= 1; r--) { + const depth = r + (currentTime * 1.5) % 1; + const ringRadius = baseRadius * (depth / numRings) * 2.2; + const ringAlpha = (1 - depth / (numRings + 1)) * 0.8; + const ringPulse = 1.0 + (rawBassEnergy * (1.2 - r / numRings) * 0.5); + + targetCtx.strokeStyle = r % 2 === 0 ? cfg.colorGrad1 : cfg.colorGrad2; + targetCtx.lineWidth = Math.max(1, 4 * (1 - depth / numRings)); + targetCtx.globalAlpha = ringAlpha; + + if (cfg.enableGlow) { + targetCtx.shadowColor = cfg.colorGrad1; + targetCtx.shadowBlur = cfg.glowBlur * 0.7; + } + + targetCtx.beginPath(); + targetCtx.arc(0, 0, ringRadius * ringPulse, 0, Math.PI * 2); + targetCtx.stroke(); + } + targetCtx.restore(); + } + + // --- LAYER 6: Audio Spectrum Rendering (Radial, Linear, Polygon, Oscilloscope) --- + if (cfg.showSpectrum) { + targetCtx.save(); + targetCtx.fillStyle = spectrumGrad; + targetCtx.strokeStyle = spectrumGrad; + + if (cfg.enableGlow && cfg.glowIntensity > 0) { + targetCtx.shadowColor = cfg.colorGrad1 || '#00f0ff'; + targetCtx.shadowBlur = cfg.glowBlur * (0.8 + rawBassEnergy * cfg.glowIntensity); + } + + // 6.1 LINEAR & HORIZON MODE + if (shape === 'linear') { + const align = cfg.linearAlignment || 'center_mirrored'; + const totalW = width * 0.85; + const barW = cfg.barWidth || 5; + const step = totalW / barCount; + const startX = -totalW / 2; + + if (cfg.spectrumMode === 'smooth_wave') { + targetCtx.beginPath(); + for (let i = 0; i < barCount; i++) { + const x = startX + i * step; + const h = processedBars[i]; + const y = align === 'bottom_up' ? height * 0.35 - h : (align === 'top_down' ? -height * 0.35 + h : -h); + if (i === 0) targetCtx.moveTo(x, y); + else targetCtx.lineTo(x, y); + } + targetCtx.lineWidth = cfg.barWidth * 1.5; + targetCtx.stroke(); + + if (align === 'center_mirrored') { + targetCtx.beginPath(); + for (let i = 0; i < barCount; i++) { + const x = startX + i * step; + const h = processedBars[i]; + if (i === 0) targetCtx.moveTo(x, h); + else targetCtx.lineTo(x, h); + } + targetCtx.stroke(); + } + } else if (cfg.spectrumMode === 'block_matrix') { + const blockSize = 6; + const blockGap = 3; + for (let i = 0; i < barCount; i++) { + const x = startX + i * step; + const h = processedBars[i]; + const blocks = Math.floor(h / (blockSize + blockGap)); + for (let b = 0; b < blocks; b++) { + const by = align === 'bottom_up' ? height * 0.38 - (b * (blockSize + blockGap)) : - (b * (blockSize + blockGap)); + targetCtx.fillRect(x - barW / 2, by, barW, blockSize); + if (align === 'center_mirrored') { + targetCtx.fillRect(x - barW / 2, b * (blockSize + blockGap), barW, blockSize); + } + } + } + } else { + // Standard Linear Bars + for (let i = 0; i < barCount; i++) { + const x = startX + i * step; + const h = processedBars[i]; + const r = cfg.barCornerRadius || 0; + + if (align === 'center_mirrored') { + targetCtx.fillRect(x - barW / 2, -h, barW, h * 2); + } else if (align === 'bottom_up') { + targetCtx.fillRect(x - barW / 2, height * 0.38 - h, barW, h); + } else if (align === 'top_down') { + targetCtx.fillRect(x - barW / 2, -height * 0.38, barW, h); + } else { + targetCtx.fillRect(x - barW / 2, -h / 2, barW, h); + } + } + } + + // 6.2 OSCILLOSCOPE / LASER WAVE MODE + } else if (shape === 'oscilloscope') { + const totalW = width * 0.85; + const step = totalW / barCount; + const startX = -totalW / 2; + + targetCtx.beginPath(); + for (let i = 0; i < barCount; i++) { + const x = startX + i * step; + const val = (freqData[i % binCount] || 0) / 255; + const oscY = Math.sin(i * 0.15 + currentTime * 8) * (val * maxHeight * 1.5) + (Math.cos(i * 0.3) * val * 20); + if (i === 0) targetCtx.moveTo(x, oscY); + else targetCtx.lineTo(x, oscY); + } + targetCtx.lineWidth = cfg.barWidth * 1.8; + targetCtx.lineCap = 'round'; + targetCtx.lineJoin = 'round'; + targetCtx.stroke(); + + // Laser Core Line (Bright white center line) + if (cfg.laserCore) { + targetCtx.save(); + targetCtx.strokeStyle = '#ffffff'; + targetCtx.lineWidth = Math.max(1.5, cfg.barWidth * 0.6); + targetCtx.stroke(); + targetCtx.restore(); + } + + // 6.3 POLYGON & RADIAL SPECTRUMS (Circle, Hexagon, Diamond, Triangle, Octagon, Tunnel) + } else { + const angleStep = (Math.PI * 2) / barCount; + const barW = cfg.barWidth || 4; + + if (cfg.spectrumMode === 'radial_bars') { + for (let i = 0; i < barCount; i++) { + const angle = i * angleStep; + const barH = processedBars[i]; + const rStart = baseRadius + barSpacing; + + targetCtx.save(); + targetCtx.rotate(angle); + targetCtx.fillRect(-barW / 2, -rStart - barH, barW, barH); + targetCtx.restore(); + } + } else if (cfg.spectrumMode === 'smooth_wave') { + targetCtx.beginPath(); + for (let i = 0; i <= barCount; i++) { + const idx = i % barCount; + const angle = i * angleStep; + const r = baseRadius + barSpacing + processedBars[idx]; + const x = Math.cos(angle - Math.PI / 2) * r; + const y = Math.sin(angle - Math.PI / 2) * r; + + if (i === 0) targetCtx.moveTo(x, y); + else targetCtx.lineTo(x, y); + } + targetCtx.closePath(); + targetCtx.lineWidth = cfg.barWidth * 1.5; + targetCtx.stroke(); + + if (cfg.laserCore) { + targetCtx.save(); + targetCtx.strokeStyle = '#ffffff'; + targetCtx.lineWidth = Math.max(1.5, cfg.barWidth * 0.5); + targetCtx.stroke(); + targetCtx.restore(); + } + } else if (cfg.spectrumMode === 'floating_dots') { + for (let i = 0; i < barCount; i++) { + const angle = i * angleStep; + const r = baseRadius + barSpacing + processedBars[i]; + const x = Math.cos(angle - Math.PI / 2) * r; + const y = Math.sin(angle - Math.PI / 2) * r; + + targetCtx.beginPath(); + targetCtx.arc(x, y, cfg.barWidth * 1.2, 0, Math.PI * 2); + targetCtx.fill(); + } + } else if (cfg.spectrumMode === 'dual_ring') { + // Inner Ring + for (let i = 0; i < barCount; i++) { + const angle = i * angleStep; + const barH = processedBars[i] * 0.7; + const rStart = baseRadius + barSpacing; + targetCtx.save(); + targetCtx.rotate(angle); + targetCtx.fillRect(-barW / 2, -rStart - barH, barW, barH); + targetCtx.restore(); + } + // Outer Ring (Treble Dots) + targetCtx.fillStyle = cfg.colorGrad2 || '#ff007f'; + for (let i = 0; i < barCount; i += 2) { + const angle = i * angleStep + (currentTime * 0.2); + const r = baseRadius + barSpacing + maxHeight * 0.8 + (processedBars[i] * 0.4); + const x = Math.cos(angle) * r; + const y = Math.sin(angle) * r; + targetCtx.beginPath(); + targetCtx.arc(x, y, barW, 0, Math.PI * 2); + targetCtx.fill(); + } + } + } + targetCtx.restore(); + } + + // --- LAYER 7: Central Core Artwork & Shape Mask (If showCover === true) --- + const sidesMap = { + circle: 0, + hexagon: 6, + diamond: 4, + triangle: 3, + octagon: 8, + linear: 0, + oscilloscope: 0, + tunnel: 0 + }; + const polySides = sidesMap[shape] || 0; + + if (cfg.showCover && state.coverImage && shape !== 'linear' && shape !== 'oscilloscope') { + targetCtx.save(); + drawPolygonPath(targetCtx, polySides, baseRadius); + targetCtx.clip(); + targetCtx.rotate(state.currentRotation); targetCtx.drawImage( state.coverImage, @@ -588,34 +874,41 @@ ); targetCtx.restore(); } - targetCtx.restore(); - // --- Layer 6: Circle Border & Progress Ring --- - if (cfg.borderWidth > 0) { + // --- LAYER 8: Shape Border & Progress Ring --- + if (cfg.showBorder && (cfg.borderWidth || 6) > 0 && shape !== 'linear' && shape !== 'oscilloscope') { targetCtx.save(); - targetCtx.beginPath(); - targetCtx.arc(0, 0, baseRadius, 0, Math.PI * 2); - targetCtx.lineWidth = cfg.borderWidth; + drawPolygonPath(targetCtx, polySides, baseRadius); + targetCtx.lineWidth = cfg.borderWidth || 6; targetCtx.strokeStyle = cfg.borderColor || '#ffffff'; + if (cfg.enableGlow) { + targetCtx.shadowColor = cfg.borderColor || '#ffffff'; + targetCtx.shadowBlur = 15; + } targetCtx.stroke(); targetCtx.restore(); } - if (cfg.showProgressRing && duration > 0) { + if (cfg.showProgressRing && duration > 0 && shape !== 'linear' && shape !== 'oscilloscope') { const progressAngle = (currentTime / duration) * (Math.PI * 2); + const ringW = cfg.progressRingWidth || 6; targetCtx.save(); targetCtx.beginPath(); - targetCtx.arc(0, 0, baseRadius + cfg.borderWidth / 2 + 1, -Math.PI / 2, -Math.PI / 2 + progressAngle); - targetCtx.lineWidth = Math.max(3, cfg.borderWidth * 0.8); + targetCtx.arc(0, 0, baseRadius + (cfg.borderWidth || 6) / 2 + 2, -Math.PI / 2, -Math.PI / 2 + progressAngle); + targetCtx.lineWidth = ringW; targetCtx.strokeStyle = cfg.progressRingColor || '#00f0ff'; targetCtx.lineCap = 'round'; + if (cfg.enableGlow) { + targetCtx.shadowColor = cfg.progressRingColor || '#00f0ff'; + targetCtx.shadowBlur = 20; + } targetCtx.stroke(); targetCtx.restore(); } targetCtx.restore(); // Restore Centered Coordinates - // --- Layer 7: Typography & Song Info --- + // --- LAYER 9: Typography & Song Info --- if (cfg.showTextOverlay) { const title = document.getElementById('trackTitle').value || 'Song Title'; const artist = document.getElementById('trackArtist').value || 'Artist'; @@ -624,36 +917,54 @@ targetCtx.textAlign = 'center'; targetCtx.textBaseline = 'middle'; - let textY = height / 2 + baseRadius + maxHeight + 70; + let textY = height / 2 + baseRadius + maxHeight + 75; if (cfg.textPosition === 'bottom_center') { - textY = height - 100; + textY = height - 90; } else if (cfg.textPosition === 'top_center') { - textY = 100; + textY = 90; } - // Audio-reactive text scale const textPulse = cfg.textPulseWithBeat ? (1.0 + rawBassEnergy * 0.05) : 1.0; targetCtx.translate(width / 2, textY); targetCtx.scale(textPulse, textPulse); // Title - targetCtx.font = `700 ${cfg.titleFontSize}px ${cfg.fontFamily}`; + targetCtx.font = `700 ${cfg.titleFontSize || 38}px ${cfg.fontFamily || "'Outfit', sans-serif"}`; targetCtx.fillStyle = '#ffffff'; - targetCtx.shadowColor = cfg.colorGrad1; - targetCtx.shadowBlur = 12; + if (cfg.enableGlow) { + targetCtx.shadowColor = cfg.colorGrad1 || '#00f0ff'; + targetCtx.shadowBlur = 14; + } targetCtx.fillText(title, 0, 0); // Artist - targetCtx.font = `500 ${cfg.artistFontSize}px ${cfg.fontFamily}`; + targetCtx.font = `500 ${cfg.artistFontSize || 22}px ${cfg.fontFamily || "'Outfit', sans-serif"}`; targetCtx.fillStyle = 'rgba(255, 255, 255, 0.75)'; targetCtx.shadowBlur = 0; - targetCtx.fillText(artist, 0, cfg.titleFontSize * 0.8 + 8); + targetCtx.fillText(artist, 0, (cfg.titleFontSize || 38) * 0.8 + 8); // Timecode if (cfg.showTimecode && duration > 0) { targetCtx.font = `600 16px 'Outfit', sans-serif`; - targetCtx.fillStyle = cfg.colorGrad1; - targetCtx.fillText(`${formatTime(currentTime)} / ${formatTime(duration)}`, 0, cfg.titleFontSize * 0.8 + cfg.artistFontSize + 18); + targetCtx.fillStyle = cfg.colorGrad1 || '#00f0ff'; + targetCtx.fillText( + `${formatTime(currentTime)} / ${formatTime(duration)}`, + 0, + (cfg.titleFontSize || 38) * 0.8 + (cfg.artistFontSize || 22) + 18 + ); + } + + // Linear Progress Bar + if (cfg.showProgressBar && duration > 0) { + const barW = Math.min(width * 0.4, 400); + const barH = 4; + const barY = (cfg.titleFontSize || 38) * 0.8 + (cfg.artistFontSize || 22) + 36; + targetCtx.fillStyle = 'rgba(255, 255, 255, 0.15)'; + targetCtx.fillRect(-barW / 2, barY, barW, barH); + + const fillW = (currentTime / duration) * barW; + targetCtx.fillStyle = cfg.colorGrad1 || '#00f0ff'; + targetCtx.fillRect(-barW / 2, barY, fillW, barH); } targetCtx.restore(); @@ -670,10 +981,10 @@ if (state.analyser && state.isPlaying) { state.analyser.getByteFrequencyData(freqArray); } else if (!state.isPlaying) { - // Subtle idle pulse + // Idle animation waves const t = performance.now() * 0.003; for (let i = 0; i < freqArray.length; i++) { - freqArray[i] = Math.max(0, Math.sin(t + i * 0.1) * 35); + freqArray[i] = Math.max(0, Math.sin(t + i * 0.08) * 32); } } @@ -693,42 +1004,64 @@ // --- UI Synchronizer --- function updateConfigFromUI() { + const checkedShape = document.querySelector('input[name="shapeType"]:checked'); + if (checkedShape) state.config.shapeType = checkedShape.value; + + state.config.showCover = document.getElementById('showCover').checked; state.config.circleRadius = parseInt(document.getElementById('circleRadius').value, 10); state.config.pulseIntensity = parseFloat(document.getElementById('pulseIntensity').value); state.config.pulseDecay = parseFloat(document.getElementById('pulseDecay').value); + state.config.showBorder = document.getElementById('showBorder').checked; state.config.borderWidth = parseInt(document.getElementById('borderWidth').value, 10); state.config.borderColor = document.getElementById('borderColor').value; state.config.showProgressRing = document.getElementById('showProgressRing').checked; state.config.progressRingColor = document.getElementById('progressRingColor').value; + state.config.progressRingWidth = parseInt(document.getElementById('progressRingWidth').value, 10); state.config.rotationSpeed = parseFloat(document.getElementById('rotationSpeed').value); state.config.beatSpinImpulse = document.getElementById('beatSpinImpulse').checked; + state.config.showSpectrum = document.getElementById('showSpectrum').checked; const checkedMode = document.querySelector('input[name="spectrumMode"]:checked'); if (checkedMode) state.config.spectrumMode = checkedMode.value; + state.config.linearAlignment = document.getElementById('linearAlignment').value; state.config.barCount = parseInt(document.getElementById('barCount').value, 10); state.config.spectrumMaxHeight = parseInt(document.getElementById('spectrumMaxHeight').value, 10); state.config.barWidth = parseFloat(document.getElementById('barWidth').value); state.config.barSpacing = parseInt(document.getElementById('barSpacing').value, 10); + state.config.barCornerRadius = parseFloat(document.getElementById('barCornerRadius').value); state.config.freqSensitivity = parseFloat(document.getElementById('freqSensitivity').value); state.config.freqFocus = document.getElementById('freqFocus').value; state.config.mirrorSpectrum = document.getElementById('mirrorSpectrum').checked; state.config.enableParticles = document.getElementById('enableParticles').checked; + state.config.particleType = document.getElementById('particleType').value; state.config.particleCount = parseInt(document.getElementById('particleCount').value, 10); state.config.particleSpeed = parseFloat(document.getElementById('particleSpeed').value); state.config.particleWarp = parseFloat(document.getElementById('particleWarp').value); + state.config.enableGrid = document.getElementById('enableGrid').checked; + state.config.colorGrid = document.getElementById('colorGrid').value; + state.config.gridPerspective = parseFloat(document.getElementById('gridPerspective').value); + state.config.gridAudioReactive = parseFloat(document.getElementById('gridAudioReactive').value); + + state.config.enableShake = document.getElementById('enableShake').checked; state.config.camShakeIntensity = parseFloat(document.getElementById('camShakeIntensity').value); + state.config.enableGlitch = document.getElementById('enableGlitch').checked; state.config.rgbSplitIntensity = parseFloat(document.getElementById('rgbSplitIntensity').value); state.config.beatFlash = document.getElementById('beatFlash').checked; + state.config.enableBassZoom = document.getElementById('enableBassZoom').checked; + state.config.colorMode = document.getElementById('colorMode').value; state.config.colorGrad1 = document.getElementById('colorGrad1').value; state.config.colorGrad2 = document.getElementById('colorGrad2').value; + state.config.colorGrad3 = document.getElementById('colorGrad3').value; state.config.colorParticle = document.getElementById('colorParticle').value; state.config.gradientStyle = document.getElementById('gradientStyle').value; + state.config.enableGlow = document.getElementById('enableGlow').checked; state.config.glowIntensity = parseFloat(document.getElementById('glowIntensity').value); state.config.glowBlur = parseInt(document.getElementById('glowBlur').value, 10); + state.config.laserCore = document.getElementById('laserCore').checked; state.config.backgroundMode = document.getElementById('backgroundMode').value; state.config.showTextOverlay = document.getElementById('showTextOverlay').checked; @@ -738,21 +1071,30 @@ state.config.artistFontSize = parseInt(document.getElementById('artistFontSize').value, 10); state.config.textPulseWithBeat = document.getElementById('textPulseWithBeat').checked; state.config.showTimecode = document.getElementById('showTimecode').checked; + state.config.showProgressBar = document.getElementById('showProgressBar').checked; + + // Toggle container visibility based on shape & toggles + document.getElementById('linearAlignmentSection').style.display = state.config.shapeType === 'linear' ? 'block' : 'none'; + coverUploadContainer.style.opacity = state.config.showCover ? '1' : '0.4'; // Update value badge labels document.getElementById('valCircleRadius').textContent = `${state.config.circleRadius}px`; document.getElementById('valPulseIntensity').textContent = `${Math.round(state.config.pulseIntensity * 100)}%`; document.getElementById('valPulseDecay').textContent = `${Math.round(state.config.pulseDecay * 100)}%`; document.getElementById('valBorderWidth').textContent = `${state.config.borderWidth}px`; + document.getElementById('valProgressRingWidth').textContent = `${state.config.progressRingWidth}px`; document.getElementById('valRotationSpeed').textContent = `${state.config.rotationSpeed} RPM`; document.getElementById('valBarCount').textContent = state.config.barCount; document.getElementById('valSpectrumMaxHeight').textContent = `${state.config.spectrumMaxHeight}px`; document.getElementById('valBarWidth').textContent = `${state.config.barWidth}px`; document.getElementById('valBarSpacing').textContent = `${state.config.barSpacing}px`; + document.getElementById('valBarCornerRadius').textContent = `${state.config.barCornerRadius}px`; document.getElementById('valFreqSensitivity').textContent = `${state.config.freqSensitivity}x`; document.getElementById('valParticleCount').textContent = state.config.particleCount; document.getElementById('valParticleSpeed').textContent = `${state.config.particleSpeed}x`; document.getElementById('valParticleWarp').textContent = `${state.config.particleWarp}x`; + document.getElementById('valGridPerspective').textContent = `${Math.round(state.config.gridPerspective * 100)}%`; + document.getElementById('valGridAudioReactive').textContent = `${Math.round(state.config.gridAudioReactive * 100)}%`; document.getElementById('valCamShakeIntensity').textContent = `${Math.round(state.config.camShakeIntensity * 100)}%`; document.getElementById('valRgbSplitIntensity').textContent = `${Math.round(state.config.rgbSplitIntensity * 100)}%`; document.getElementById('valGlowIntensity').textContent = `${Math.round(state.config.glowIntensity * 100)}%`; @@ -769,51 +1111,75 @@ Object.assign(state.config, p); - // Sync form elements with preset values - document.getElementById('circleRadius').value = state.config.circleRadius; - document.getElementById('pulseIntensity').value = state.config.pulseIntensity; - document.getElementById('pulseDecay').value = state.config.pulseDecay; - document.getElementById('borderWidth').value = state.config.borderWidth; - document.getElementById('borderColor').value = state.config.borderColor; - document.getElementById('showProgressRing').checked = state.config.showProgressRing; - document.getElementById('progressRingColor').value = state.config.progressRingColor; - document.getElementById('rotationSpeed').value = state.config.rotationSpeed; - document.getElementById('beatSpinImpulse').checked = state.config.beatSpinImpulse; + // Sync form inputs with preset + const shapeRadio = document.querySelector(`input[name="shapeType"][value="${state.config.shapeType || 'circle'}"]`); + if (shapeRadio) shapeRadio.checked = true; - const modeRadio = document.querySelector(`input[name="spectrumMode"][value="${state.config.spectrumMode}"]`); + document.getElementById('showCover').checked = state.config.showCover !== false; + document.getElementById('circleRadius').value = state.config.circleRadius || 150; + document.getElementById('pulseIntensity').value = state.config.pulseIntensity ?? 0.45; + document.getElementById('pulseDecay').value = state.config.pulseDecay ?? 0.8; + document.getElementById('showBorder').checked = state.config.showBorder !== false; + document.getElementById('borderWidth').value = state.config.borderWidth || 6; + document.getElementById('borderColor').value = state.config.borderColor || '#ffffff'; + document.getElementById('showProgressRing').checked = state.config.showProgressRing !== false; + document.getElementById('progressRingColor').value = state.config.progressRingColor || '#00f0ff'; + document.getElementById('progressRingWidth').value = state.config.progressRingWidth || 6; + document.getElementById('rotationSpeed').value = state.config.rotationSpeed ?? 3; + document.getElementById('beatSpinImpulse').checked = state.config.beatSpinImpulse !== false; + + document.getElementById('showSpectrum').checked = state.config.showSpectrum !== false; + const modeRadio = document.querySelector(`input[name="spectrumMode"][value="${state.config.spectrumMode || 'radial_bars'}"]`); if (modeRadio) modeRadio.checked = true; - document.getElementById('barCount').value = state.config.barCount; - document.getElementById('spectrumMaxHeight').value = state.config.spectrumMaxHeight; - document.getElementById('barWidth').value = state.config.barWidth; - document.getElementById('barSpacing').value = state.config.barSpacing; - document.getElementById('freqSensitivity').value = state.config.freqSensitivity; - document.getElementById('freqFocus').value = state.config.freqFocus; - document.getElementById('mirrorSpectrum').checked = state.config.mirrorSpectrum; + document.getElementById('linearAlignment').value = state.config.linearAlignment || 'center_mirrored'; + document.getElementById('barCount').value = state.config.barCount || 96; + document.getElementById('spectrumMaxHeight').value = state.config.spectrumMaxHeight || 130; + document.getElementById('barWidth').value = state.config.barWidth || 4; + document.getElementById('barSpacing').value = state.config.barSpacing || 12; + document.getElementById('barCornerRadius').value = state.config.barCornerRadius || 2; + document.getElementById('freqSensitivity').value = state.config.freqSensitivity || 1.3; + document.getElementById('freqFocus').value = state.config.freqFocus || 'balanced'; + document.getElementById('mirrorSpectrum').checked = state.config.mirrorSpectrum !== false; - document.getElementById('enableParticles').checked = state.config.enableParticles; - document.getElementById('particleCount').value = state.config.particleCount; - document.getElementById('particleSpeed').value = state.config.particleSpeed; - document.getElementById('particleWarp').value = state.config.particleWarp; + document.getElementById('enableParticles').checked = state.config.enableParticles !== false; + document.getElementById('particleType').value = state.config.particleType || 'starfield'; + document.getElementById('particleCount').value = state.config.particleCount || 100; + document.getElementById('particleSpeed').value = state.config.particleSpeed || 1.0; + document.getElementById('particleWarp').value = state.config.particleWarp || 2.5; - document.getElementById('camShakeIntensity').value = state.config.camShakeIntensity; - document.getElementById('rgbSplitIntensity').value = state.config.rgbSplitIntensity; - document.getElementById('beatFlash').checked = state.config.beatFlash; + document.getElementById('enableGrid').checked = !!state.config.enableGrid; + document.getElementById('colorGrid').value = state.config.colorGrid || '#ff007f'; + document.getElementById('gridPerspective').value = state.config.gridPerspective || 0.65; + document.getElementById('gridAudioReactive').value = state.config.gridAudioReactive || 0.5; - document.getElementById('colorGrad1').value = state.config.colorGrad1; - document.getElementById('colorGrad2').value = state.config.colorGrad2; - document.getElementById('colorParticle').value = state.config.colorParticle; - document.getElementById('gradientStyle').value = state.config.gradientStyle; - document.getElementById('glowIntensity').value = state.config.glowIntensity; - document.getElementById('glowBlur').value = state.config.glowBlur; - document.getElementById('backgroundMode').value = state.config.backgroundMode; + document.getElementById('enableShake').checked = state.config.enableShake !== false; + document.getElementById('camShakeIntensity').value = state.config.camShakeIntensity || 0.35; + document.getElementById('enableGlitch').checked = state.config.enableGlitch !== false; + document.getElementById('rgbSplitIntensity').value = state.config.rgbSplitIntensity || 0.25; + document.getElementById('beatFlash').checked = !!state.config.beatFlash; + document.getElementById('enableBassZoom').checked = state.config.enableBassZoom !== false; - document.getElementById('fontFamily').value = state.config.fontFamily; - document.getElementById('textPosition').value = state.config.textPosition; - document.getElementById('titleFontSize').value = state.config.titleFontSize; - document.getElementById('artistFontSize').value = state.config.artistFontSize; - document.getElementById('textPulseWithBeat').checked = state.config.textPulseWithBeat; - document.getElementById('showTimecode').checked = state.config.showTimecode; + document.getElementById('colorMode').value = state.config.colorMode || 'gradient_2'; + document.getElementById('colorGrad1').value = state.config.colorGrad1 || '#00f0ff'; + document.getElementById('colorGrad2').value = state.config.colorGrad2 || '#ff007f'; + document.getElementById('colorGrad3').value = state.config.colorGrad3 || '#7000ff'; + document.getElementById('colorParticle').value = state.config.colorParticle || '#ffffff'; + document.getElementById('gradientStyle').value = state.config.gradientStyle || 'angular'; + document.getElementById('enableGlow').checked = state.config.enableGlow !== false; + document.getElementById('glowIntensity').value = state.config.glowIntensity || 0.75; + document.getElementById('glowBlur').value = state.config.glowBlur || 25; + document.getElementById('laserCore').checked = !!state.config.laserCore; + document.getElementById('backgroundMode').value = state.config.backgroundMode || 'transparent'; + + document.getElementById('showTextOverlay').checked = state.config.showTextOverlay !== false; + document.getElementById('fontFamily').value = state.config.fontFamily || "'Outfit', sans-serif"; + document.getElementById('textPosition').value = state.config.textPosition || 'below_circle'; + document.getElementById('titleFontSize').value = state.config.titleFontSize || 38; + document.getElementById('artistFontSize').value = state.config.artistFontSize || 22; + document.getElementById('textPulseWithBeat').checked = state.config.textPulseWithBeat !== false; + document.getElementById('showTimecode').checked = state.config.showTimecode !== false; + document.getElementById('showProgressBar').checked = !!state.config.showProgressBar; updateConfigFromUI(); } @@ -826,7 +1192,7 @@ }); canvasWrapper.className = 'canvas-wrapper'; - if (toggleAlphaGrid.checked) canvasWrapper.classList.add('has-grid'); + if (toggleAlphaGrid.checked) canvasWrapper.classList.add('alpha-bg'); if (aspect === '16:9') { canvasWrapper.classList.add('aspect-16-9'); @@ -847,15 +1213,15 @@ initParticles(state.config.particleCount); } - // --- Export Pipeline (ProRes 4444 MOV with Alpha / WebM Alpha / MP4) --- + // --- Export Pipeline --- async function startExport() { if (!state.audioLoaded || !state.audioFile) { alert('Bitte lade zuerst eine Audio-Datei (.wav oder .mp3) hoch!'); return; } - const format = document.getElementById('exportFormat').value; // prores, webm, mp4 - const res = document.getElementById('exportResolution').value; // 1080p, 4k, 720p + const format = document.getElementById('exportFormat').value; + const res = document.getElementById('exportResolution').value; const fps = parseInt(document.getElementById('exportFps').value, 10); let renderWidth = state.width; @@ -878,10 +1244,9 @@ pausePlayback(); - // Prepare FormData for server-side FFmpeg processing const formData = new FormData(); formData.append('audio', state.audioFile); - if (state.imageFile) { + if (state.imageFile && state.config.showCover) { formData.append('cover', state.imageFile); } formData.append('config', JSON.stringify(state.config)); @@ -909,11 +1274,9 @@ throw new Error(errText || 'Render-Fehler auf dem Server'); } - // Track download progress if streamed or grab blob const blob = await response.blob(); const filename = `viz_${(document.getElementById('trackTitle').value || 'video').replace(/[^a-z0-9]/gi, '_')}_${format === 'prores' ? 'ProRes4444_Alpha.mov' : (format === 'webm' ? 'Alpha.webm' : 'video.mp4')}`; - // Trigger instant browser download const downloadUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = downloadUrl; @@ -956,7 +1319,6 @@ // --- Event Listeners & Binding --- function setupEvents() { - // Dropzones & File Inputs audioDropzone.addEventListener('click', () => audioFileInput.click()); imageDropzone.addEventListener('click', () => imageFileInput.click()); @@ -981,7 +1343,6 @@ }); }); - // Player controls btnPlayPause.addEventListener('click', () => { if (state.isPlaying) pausePlayback(); else startPlayback(state.pauseOffset); @@ -1015,7 +1376,6 @@ if (state.gainNode) state.gainNode.gain.value = v; }); - // Aspect Switchers aspectBtns.forEach(btn => { btn.addEventListener('click', () => setAspectRatio(btn.dataset.aspect)); }); @@ -1025,13 +1385,25 @@ }); toggleAlphaGrid.addEventListener('change', (e) => { - canvasWrapper.classList.toggle('has-grid', e.target.checked); + canvasWrapper.classList.toggle('alpha-bg', e.target.checked); }); quickPresetSelect.addEventListener('change', (e) => { applyPreset(e.target.value); }); + // 1-Click Palette Chips + document.querySelectorAll('.palette-chip').forEach(chip => { + chip.addEventListener('click', () => { + document.getElementById('colorGrad1').value = chip.dataset.p1; + document.getElementById('colorGrad2').value = chip.dataset.p2; + document.getElementById('colorGrad3').value = chip.dataset.p3; + document.getElementById('colorParticle').value = chip.dataset.p1; + document.getElementById('colorGrid').value = chip.dataset.p2; + updateConfigFromUI(); + }); + }); + // Tabs tabBtns.forEach(btn => { btn.addEventListener('click', () => { @@ -1042,7 +1414,7 @@ }); }); - // Dynamic UI Inputs (Two-way binding) + // Two-way UI sync document.querySelectorAll('.sidebar-content input, .sidebar-content select').forEach(el => { el.addEventListener('input', updateConfigFromUI); el.addEventListener('change', updateConfigFromUI); @@ -1084,15 +1456,12 @@ } }); - // Export Buttons btnStartExport.addEventListener('click', startExport); headerExportBtn.addEventListener('click', () => { - // Switch to export tab and start document.querySelector('[data-tab="tab-export"]').click(); }); btnCancelExport.addEventListener('click', cancelExport); - // Keyboard Shortcuts (Spacebar Play/Pause) window.addEventListener('keydown', (e) => { if (e.code === 'Space' && e.target.tagName !== 'INPUT') { e.preventDefault(); diff --git a/public/index.html b/public/index.html index a60b91c..6cb59bf 100644 --- a/public/index.html +++ b/public/index.html @@ -3,10 +3,10 @@ - Viz — Audio Visualizer Studio für DaVinci Resolve + Viz 2.0 — Audio Visualizer Studio für DaVinci Resolve - + @@ -16,16 +16,13 @@
-

VIZStudio

+

VIZStudio 2.0

DaVinci Alpha Edition
@@ -33,14 +30,16 @@
- +
@@ -58,13 +57,13 @@
- - -
@@ -72,9 +71,9 @@
-