Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>VisionFX Studio Pro - WebGL & Math Engine</title> | |
| <!-- Externe Bibliotheken (CDN) --> | |
| <!-- FontAwesome für Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Math.js für Formelverarbeitung --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/11.8.0/math.min.js"></script> | |
| <!-- glfx.js für WebGL Bildverarbeitung (Filter, Verzerrung) --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/glfx/0.0.4/glfx.min.js"></script> | |
| <style> | |
| :root { | |
| --bg-dark: #0f0f13; | |
| --bg-panel: #1a1a20; | |
| --bg-input: #25252e; | |
| --primary: #bb86fc; | |
| --secondary: #03dac6; | |
| --accent: #cf6679; | |
| --text-main: #e0e0e0; | |
| --text-muted: #a0a0a0; | |
| --border: #333; | |
| --font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| --header-height: 60px; | |
| --footer-height: 50px; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| outline: none; | |
| } | |
| body { | |
| font-family: var(--font-family); | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| background-color: var(--bg-panel); | |
| border-bottom: 1px solid var(--border); | |
| padding: 0 1.5rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| height: var(--header-height); | |
| flex-shrink: 0; | |
| z-index: 20; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.3); | |
| } | |
| .logo { | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| color: var(--primary); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| letter-spacing: 0.5px; | |
| } | |
| .logo i { color: var(--secondary); } | |
| .header-actions { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .btn { | |
| background-color: var(--bg-input); | |
| border: 1px solid var(--border); | |
| color: var(--text-main); | |
| padding: 0.4rem 0.8rem; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 0.85rem; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| text-decoration: none; | |
| } | |
| .btn:hover { | |
| border-color: var(--primary); | |
| color: var(--primary); | |
| background-color: #2f2f3a; | |
| } | |
| .btn-primary { | |
| background-color: var(--primary); | |
| color: #000; | |
| border: none; | |
| font-weight: 600; | |
| } | |
| .btn-primary:hover { | |
| background-color: #a370db; | |
| color: #000; | |
| } | |
| /* --- Main Layout --- */ | |
| main { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* --- Canvas Area --- */ | |
| .canvas-container { | |
| flex: 1; | |
| background-color: #000; | |
| background-image: | |
| radial-gradient(circle at center, #1a1a2e 0%, #000 100%); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| /* Canvas Wrapper to handle scaling */ | |
| .canvas-wrapper { | |
| box-shadow: 0 0 30px rgba(0, 0, 0, 0.6); | |
| max-width: 95%; | |
| max-height: 95%; | |
| position: relative; | |
| } | |
| canvas { | |
| display: block; | |
| max-width: 100%; | |
| max-height: 100%; | |
| } | |
| /* --- Sidebar --- */ | |
| .sidebar { | |
| width: 400px; | |
| background-color: var(--bg-panel); | |
| border-left: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| flex-shrink: 0; | |
| transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); | |
| z-index: 10; | |
| } | |
| .sidebar-header { | |
| padding: 1rem; | |
| border-bottom: 1px solid var(--border); | |
| font-weight: 600; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: rgba(26, 26, 32, 0.95); | |
| } | |
| .filter-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 1rem; | |
| } | |
| /* Scrollbar Styling */ | |
| .filter-list::-webkit-scrollbar { width: 6px; } | |
| .filter-list::-webkit-scrollbar-track { background: var(--bg-dark); } | |
| .filter-list::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; } | |
| /* --- Filter Item UI --- */ | |
| .filter-item { | |
| background-color: var(--bg-input); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| margin-bottom: 12px; | |
| padding: 12px; | |
| transition: all 0.2s; | |
| } | |
| .filter-item.active { | |
| border-color: var(--secondary); | |
| box-shadow: 0 0 10px rgba(3, 218, 198, 0.1); | |
| } | |
| .filter-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 10px; | |
| } | |
| .filter-title { | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| color: var(--text-main); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .filter-type-badge { | |
| font-size: 0.6rem; | |
| padding: 2px 4px; | |
| border-radius: 2px; | |
| background: #333; | |
| color: #888; | |
| text-transform: uppercase; | |
| } | |
| /* Toggle Switch */ | |
| .toggle-switch { | |
| position: relative; | |
| display: inline-block; | |
| width: 32px; | |
| height: 18px; | |
| } | |
| .toggle-switch input { opacity: 0; width: 0; height: 0; } | |
| .slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; left: 0; right: 0; bottom: 0; | |
| background-color: #444; | |
| transition: .4s; | |
| border-radius: 18px; | |
| } | |
| .slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 12px; width: 12px; | |
| left: 3px; bottom: 3px; | |
| background-color: white; | |
| transition: .4s; | |
| border-radius: 50%; | |
| } | |
| input:checked + .slider { background-color: var(--primary); } | |
| input:checked + .slider:before { transform: translateX(14px); } | |
| /* Inputs */ | |
| .control-group { margin-bottom: 8px; } | |
| .control-group label { | |
| display: block; | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| margin-bottom: 4px; | |
| } | |
| .range-slider { | |
| width: 100%; | |
| -webkit-appearance: none; | |
| background: transparent; | |
| } | |
| .range-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 14px; width: 14px; | |
| border-radius: 50%; | |
| background: var(--secondary); | |
| cursor: pointer; | |
| margin-top: -5px; | |
| } | |
| .range-slider::-webkit-slider-runnable-track { | |
| width: 100%; height: 4px; | |
| cursor: pointer; | |
| background: #444; | |
| border-radius: 2px; | |
| } | |
| .math-input { | |
| width: 100%; | |
| background-color: #15151a; | |
| border: 1px solid var(--border); | |
| color: var(--primary); | |
| padding: 6px; | |
| border-radius: 4px; | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.8rem; | |
| margin-top: 4px; | |
| } | |
| .math-input:focus { border-color: var(--secondary); } | |
| .math-hint { | |
| font-size: 0.7rem; color: var(--text-muted); margin-top: 2px; | |
| display: flex; justify-content: space-between; | |
| } | |
| /* --- Footer / Timeline --- */ | |
| .timeline { | |
| height: var(--footer-height); | |
| background-color: var(--bg-panel); | |
| border-top: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| padding: 0 1rem; | |
| justify-content: space-between; | |
| } | |
| .playback-controls { display: flex; gap: 10px; align-items: center; } | |
| .time-display { font-family: monospace; color: var(--secondary); font-size: 0.9rem; } | |
| /* --- Components --- */ | |
| .hidden { display: none ; } | |
| #loading { | |
| position: absolute; top: 0; left: 0; right: 0; bottom: 0; | |
| background: rgba(15, 15, 19, 0.9); | |
| color: white; | |
| display: flex; justify-content: center; align-items: center; | |
| z-index: 50; flex-direction: column; gap: 15px; | |
| } | |
| .spinner { | |
| width: 40px; height: 40px; | |
| border: 4px solid rgba(255, 255, 255, 0.1); | |
| border-top: 4px solid var(--primary); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| .drop-zone { | |
| position: absolute; top: 20px; left: 20px; right: 20px; bottom: 20px; | |
| border: 2px dashed var(--text-muted); | |
| display: flex; justify-content: center; align-items: center; | |
| flex-direction: column; color: var(--text-muted); | |
| pointer-events: none; opacity: 0; transition: opacity 0.3s; | |
| } | |
| .canvas-container.drag-over .drop-zone { opacity: 1; background: rgba(0, 0, 0, 0.5); pointer-events: all; } | |
| /* --- Double Exposure Badge --- */ | |
| .badge-double-exp { | |
| position: absolute; | |
| top: 10px; right: 10px; | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(4px); | |
| color: var(--primary); | |
| padding: 4px 8px; | |
| font-size: 0.7rem; | |
| font-weight: bold; | |
| border: 1px solid var(--primary); | |
| border-radius: 2px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .badge-double-exp.visible { opacity: 1; } | |
| /* --- Responsive --- */ | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| position: absolute; height: 100%; transform: translateX(100%); | |
| box-shadow: -5px 0 15px rgba(0,0,0,0.5); | |
| } | |
| .sidebar.open { transform: translateX(0); } | |
| .header-actions span { display: none; } /* Hide button text on mobile */ | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header> | |
| <div class="logo"> | |
| <i class="fa-solid fa-layer-group"></i> VisionFX Studio | |
| </div> | |
| <div class="header-actions"> | |
| <button class="btn" id="toggleSidebarBtn" title="Effekte"> | |
| <i class="fa-solid fa-sliders"></i> <span>Effekte</span> | |
| </button> | |
| <button class="btn" onclick="document.getElementById('fileInput').click()"> | |
| <i class="fa-solid fa-upload"></i> <span>Import</span> | |
| </button> | |
| <input type="file" id="fileInput" class="hidden" accept="image/*,video/*"> | |
| <button class="btn btn-primary" id="exportBtn"> | |
| <i class="fa-solid fa-download"></i> <span>Export</span> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main> | |
| <!-- Canvas Area --> | |
| <div class="canvas-container" id="canvasWrapper"> | |
| <div id="loading" class="hidden"> | |
| <div class="spinner"></div> | |
| <span id="loadingText">Verarbeite...</span> | |
| </div> | |
| <div class="drop-zone" id="dropZone"> | |
| <i class="fa-solid fa-cloud-arrow-down fa-3x"></i> | |
| <p style="margin-top: 10px;">Datei hier ablegen (Bild/Video)</p> | |
| </div> | |
| <div class="canvas-wrapper"> | |
| <div id="doubleExpBadge" class="badge-double-exp">Double Exposure Mode</div> | |
| <!-- Canvas wird hier dynamisch erstellt --> | |
| <canvas id="mainCanvas"></canvas> | |
| </div> | |
| </div> | |
| <!-- Sidebar --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <span><i class="fa-solid fa-wave-square"></i> FX Rack</span> | |
| <button class="btn" style="padding: 2px 8px; font-size: 0.75rem;" id="resetAllBtn">Reset</button> | |
| </div> | |
| <div class="filter-list" id="filterList"> | |
| <!-- Filter items werden hier per JS generiert --> | |
| </div> | |
| <div style="padding: 1rem; border-top: 1px solid var(--border); font-size: 0.75rem; color: var(--text-muted); background: var(--bg-panel);"> | |
| <p><strong>Math.js Variablen:</strong></p> | |
| <ul style="margin-left: 20px; margin-top: 5px; list-style: none;"> | |
| <li><code style="color:var(--secondary)">t</code>: Zeit (ms)</li> | |
| <li><code style="color:var(--secondary)">v</code>: Slider-Wert</li> | |
| <li><code style="color:var(--secondary)">x,y</code>: Zufall (0-1)</li> | |
| </ul> | |
| <p style="margin-top: 5px;">Beispiel: <code>sin(t/500) * v</code></p> | |
| </div> | |
| </aside> | |
| </main> | |
| <!-- Timeline / Playback --> | |
| <div class="timeline"> | |
| <div class="playback-controls"> | |
| <button class="btn" id="playPauseBtn"><i class="fa-solid fa-play"></i></button> | |
| <button class="btn" id="stopBtn"><i class="fa-solid fa-stop"></i></button> | |
| <span class="time-display" id="timeDisplay">00:00:00</span> | |
| </div> | |
| <div> | |
| <span style="font-size: 0.75rem; color: var(--text-muted);"> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: var(--primary); text-decoration: none; font-weight: bold;">anycoder</a> | |
| </span> | |
| </div> | |
| </div> | |
| <script> | |
| /** | |
| * VisionFX Studio Pro - Core Logic | |
| * Hybrid Rendering: WebGL (glfx.js) für Filter, Canvas 2D für Overlays/UI | |
| */ | |
| // --- Konfiguration & State --- | |
| const config = { | |
| width: 1280, | |
| height: 720, | |
| bgColor: '#000000' | |
| }; | |
| const state = { | |
| isPlaying: false, | |
| startTime: 0, | |
| elapsedTime: 0, | |
| mediaType: 'none', // 'image' | 'video' | |
| mediaSource: null, // Image Objekt oder Video Element | |
| animationId: null, | |
| filters: {}, // Speichert aktuelle Filterzustände | |
| canvas: null, // Der Haupt-Canvas | |
| texture: null, // WebGL Texture | |
| fxCanvas: null, // WebGL Context (glfx) | |
| ctx: null // 2D Fallback (für Overlays) | |
| }; | |
| // --- DOM Elemente --- | |
| const canvasWrapper = document.getElementById('canvasWrapper'); | |
| const filterListEl = document.getElementById('filterList'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const playPauseBtn = document.getElementById('playPauseBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const timeDisplay = document.getElementById('timeDisplay'); | |
| const loadingEl = document.getElementById('loading'); | |
| const dropZone = document.getElementById('dropZone'); | |
| const sidebar = document.getElementById('sidebar'); | |
| const doubleExpBadge = document.getElementById('doubleExpBadge'); | |
| // --- Filter Registry (Auto-Discovery Simulation) --- | |
| // Wir definieren hier, welche Effekte verfügbar sind. | |
| // In einer echten "Discovery" Umgebung würde man die Objekte von glfx.js scannen. | |
| const filterRegistry = [ | |
| // --- WebGL Filter (glfx.js) --- | |
| { | |
| id: 'brightnessContrast', | |
| name: 'Helligkeit & Kontrast', | |
| type: 'webgl', | |
| params: [ | |
| { name: 'brightness', min: -1, max: 1, def: 0, label: 'Helligkeit' }, | |
| { name: 'contrast', min: -1, max: 1, def: 0, label: 'Kontrast' } | |
| ] | |
| }, | |
| { | |
| id: 'hueSaturation', | |
| name: 'Farbe & Sättigung', | |
| type: 'webgl', | |
| params: [ | |
| { name: 'hue', min: -1, max: 1, def: 0, label: 'Farbrotation' }, | |
| { name: 'saturation', min: -1, max: 1, def: 0, label: 'Sättigung' } | |
| ] | |
| }, | |
| { | |
| id: 'vignette', | |
| name: 'Vignette', | |
| type: 'webgl', | |
| params: [ | |
| { name: 'size', min: 0, max: 1, def: 0.5, label: 'Größe' }, | |
| { name: 'amount', min: 0, max: 1, def: 0.5, label: 'Intensität' } | |
| ] | |
| }, | |
| { | |
| id: 'blur', | |
| name: 'Weichzeichner (Gaussian)', | |
| type: 'webgl', | |
| params: [ | |
| { name: 'strength', min: 0, max: 20, def: 0, label: 'Stärke' } | |
| ] | |
| }, | |
| { | |
| id: 'noise', | |
| name: 'Rauschen (Film Grain)', | |
| type: 'webgl', | |
| params: [ | |
| { name: 'amount', min: 0, max: 1, def: 0, label: 'Menge' } | |
| ] | |
| }, | |
| { | |
| id: 'denoise', | |
| name: 'Rauschentfernung', | |
| type: 'webgl', | |
| params: [ | |
| { name: 'strength', min: 0, max: 1, def: 0, label: 'Stärke' } | |
| ] | |
| }, | |
| { | |
| id: 'sepia', | |
| name: 'Sepia', | |
| type: 'webgl', | |
| params: [ | |
| { name: 'amount', min: 0, max: 1, def: 0, label: 'Menge' } | |
| ] | |
| }, | |
| { | |
| id: 'hexagonalPixelate', | |
| name: 'Hexagon Pixelate', | |
| type: 'webgl', | |
| params: [ | |
| { name: 'scale', min: 2, max: 50, def: 10, label: 'Blockgröße' } | |
| ] | |
| }, | |
| { | |
| id: 'ink', | |
| name: 'Tinte / Kanten', | |
| type: 'webgl', | |
| params: [ | |
| { name: 'strength', min: 0, max: 1, def: 0.25, label: 'Stärke' } | |
| ] | |
| }, | |
| { | |
| id: 'swirl', | |
| name: 'Wirbel Verzerrung', | |
| type: 'webgl', | |
| params: [ | |
| { name: 'strength', min: -10, max: 10, def: 0, label: 'Stärke' }, | |
| { name: 'radius', min: 0, max: 500, def: 200, label: 'Radius' } | |
| ] | |
| }, | |
| { | |
| id: 'bulgePinch', | |
| name: 'Wölbung', | |
| type: 'webgl', | |
| params: [ | |
| { name: 'strength', min: -1, max: 1, def: 0, label: 'Stärke' }, | |
| { name: 'radius', min: 0, max: 500, def: 200, label: 'Radius' } | |
| ] | |
| }, | |
| // --- Custom / Hybrid Effekte --- | |
| { | |
| id: 'glitch', | |
| name: 'Digital Glitch', | |
| type: 'custom', | |
| params: [ | |
| { name: 'amount', min: 0, max: 100, def: 0, label: 'Intensität' } | |
| ] | |
| }, | |
| { | |
| id: 'rgbSplit', | |
| name: 'RGB Shift (Chroma)', | |
| type: 'custom', | |
| params: [ | |
| { name: 'offset', min: 0, max: 50, def: 0, label: 'Versatz' } | |
| ] | |
| }, | |
| { | |
| id: 'scanlines', | |
| name: 'TV Scanlines', | |
| type: 'custom', | |
| params: [ | |
| { name: 'opacity', min: 0, max: 1, def: 0.3, label: 'Deckkraft' } | |
| ] | |
| }, | |
| { | |
| id: 'doubleExposure', | |
| name: 'Double Exposure', | |
| type: 'custom', | |
| params: [ | |
| { name: 'mix', min: 0, max: 1, def: 0, label: 'Mischung' } | |
| ] | |
| } | |
| ]; | |
| // --- Initialisierung --- | |
| function init() { | |
| // Canvas Setup | |
| state.canvas = document.getElementById('mainCanvas'); | |
| state.ctx = state.canvas.getContext('2d'); | |
| // Initial WebGL Canvas vorbereiten (wird beim ersten Media Load aktiviert) | |
| if (typeof fx !== 'undefined') { | |
| state.fxCanvas = fx.canvas(); | |
| } | |
| // Events | |
| fileInput.addEventListener('change', handleFileSelect); | |
| playPauseBtn.addEventListener('click', togglePlay); | |
| stopBtn.addEventListener('click', stopMedia); | |
| document.getElementById('toggleSidebarBtn').addEventListener('click', () => sidebar.classList.toggle('open')); | |
| document.getElementById('resetAllBtn').addEventListener('click', resetFilters); | |
| document.getElementById('exportBtn').addEventListener('click', exportMedia); | |
| // Drag & Drop | |
| canvasWrapper.addEventListener('dragover', (e) => { e.preventDefault(); canvasWrapper.classList.add('drag-over'); }); | |
| canvasWrapper.addEventListener('dragleave', () => canvasWrapper.classList.remove('drag-over')); | |
| canvasWrapper.addEventListener('drop', handleDrop); | |
| // UI Generierung | |
| generateFilterUI(); | |
| // Canvas Startzustand | |
| resizeCanvas(800, 600); | |
| state.ctx.fillStyle = config.bgColor; | |
| state.ctx.fillRect(0, 0, state.canvas.width, state.canvas.height); | |
| // Start Render Loop | |
| requestAnimationFrame(renderLoop); | |
| console.log("VisionFX initialized. Libraries: Math.js, glfx.js"); | |
| } | |
| // --- UI Generierung (Dynamic) --- | |
| function generateFilterUI() { | |
| filterListEl.innerHTML = ''; | |
| filterRegistry.forEach(f => { | |
| const item = document.createElement('div'); | |
| item.className = 'filter-item'; | |
| item.id = `filter-${f.id}`; | |
| // Typ Badge | |
| const typeLabel = f.type === 'webgl' ? 'GPU' : 'CPU'; | |
| let paramsHtml = ''; | |
| f.params.forEach(p => { | |
| paramsHtml += ` | |
| <div class="control-group"> | |
| <label>${p.label}</label> | |
| <input type="range" class="range-slider param-slider" | |
| data-filter="${f.id}" data-param="${p.name}" | |
| min="${p.min}" max="${p.max}" value="${p.def}" step="0.01"> | |
| <div class="math-hint"> | |
| <span>Val: <span class="val-display" id="val-${f.id}-${p.name}">${p.def}</span></span> | |
| </div> | |
| <input type="text" class="math-input param-formula" | |
| data-filter="${f.id}" data-param="${p.name}" | |
| placeholder="Formel (z.B. sin(t)*v)" value=""> | |
| </div> | |
| `; | |
| }); | |
| item.innerHTML = ` | |
| <div class="filter-header"> | |
| <div class="filter-title"> | |
| ${f.name} <span class="filter-type-badge">${typeLabel}</span> | |
| </div> | |
| <label class="toggle-switch"> | |
| <input type="checkbox" class="filter-toggle" data-id="${f.id}"> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| ${paramsHtml} | |
| `; | |
| filterListEl.appendChild(item); | |
| // State Initialisierung | |
| state.filters[f.id] = { | |
| active: false, | |
| type: f.type, | |
| params: {} | |
| }; | |
| f.params.forEach(p => { | |
| state.filters[f.id].params[p.name] = { | |
| value: p.def, | |
| formula: '', | |
| computed: p.def | |
| }; | |
| }); | |
| // Event Listeners | |
| const toggle = item.querySelector('.filter-toggle'); | |
| toggle.addEventListener('change', (e) => { | |
| state.filters[f.id].active = e.target.checked; | |
| item.classList.toggle('active', e.target.checked); | |
| // Spezielles Handling für Double Exposure Badge | |
| if(f.id === 'doubleExposure') { | |
| doubleExpBadge.classList.toggle('visible', e.target.checked); | |
| } | |
| }); | |
| // Sliders | |
| item.querySelectorAll('.param-slider').forEach(slider => { | |
| slider.addEventListener('input', (e) => { | |
| const pName = e.target.dataset.param; | |
| state.filters[f.id].params[pName].value = parseFloat(e.target.value); | |
| document.getElementById(`val-${f.id}-${pName}`).innerText = parseFloat(e.target.value).toFixed(2); | |
| }); | |
| }); | |
| // Formulas | |
| item.querySelectorAll('.param-formula').forEach(input => { | |
| input.addEventListener('input', (e) => { | |
| const pName = e.target.dataset.param; | |
| state.filters[f.id].params[pName].formula = e.target.value; | |
| }); | |
| }); | |
| }); | |
| } | |
| function resetFilters() { | |
| document.querySelectorAll('.filter-toggle').forEach(el => el.checked = false); | |
| document.querySelectorAll('.filter-item').forEach(el => el.classList.remove('active')); | |
| doubleExpBadge.classList.remove('visible'); | |
| Object.keys(state.filters).forEach(key => { | |
| state.filters[key].active = false; | |
| const defParams = filterRegistry.find(f => f.id === key).params; | |
| defParams.forEach(p => { | |
| state.filters[key].params[p.name].value = p.def; | |
| state.filters[key].params[p.name].formula = ''; | |
| // Update UI | |
| const slider = document.querySelector(`.param-slider[data-filter="${key}"][data-param="${p.name}"]`); | |
| const display = document.getElementById(`val-${key}-${p.name}`); | |
| const formula = document.querySelector(`.param-formula[data-filter="${key}"][data-param="${p.name}"]`); | |
| if(slider) slider.value = p.def; | |
| if(display) display.innerText = p.def; | |
| if(formula) formula.value = ''; | |
| }); | |
| }); | |
| } | |
| // --- Math Engine --- | |
| function evaluateFormula(formula, time, sliderVal) { | |
| if (!formula || formula.trim() === '') return sliderVal; | |
| try { | |
| const scope = { | |
| t: time, | |
| v: sliderVal, | |
| x: Math.random(), | |
| y: Math.random(), | |
| pi: Math.PI | |
| }; | |
| return math.evaluate(formula, scope); | |
| } catch (err) { | |
| return sliderVal; // Fallback bei Fehler | |
| } | |
| } | |
| // --- File Handling --- | |
| function handleFileSelect(e) { | |
| const file = e.target.files[0]; | |
| if (file) loadMedia(file); | |
| } | |
| function handleDrop(e) { | |
| e.preventDefault(); | |
| canvasWrapper.classList.remove('drag-over'); | |
| if (e.dataTransfer.files.length) loadMedia(e.dataTransfer.files[0]); | |
| } | |
| function loadMedia(file) { | |
| loadingEl.classList.remove('hidden'); | |
| const url = URL.createObjectURL(file); | |
| if (file.type.startsWith('image/')) { | |
| const img = new Image(); | |
| img.onload = () => { | |
| state.mediaType = 'image'; | |
| state.mediaSource = img; | |
| setupCanvasForMedia(img.width, img.height); | |
| state.texture = state.fxCanvas.texture(img); | |
| loadingEl.classList.add('hidden'); | |
| renderFrame(0); // Init render | |
| }; |