Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | |
| <title>Advanced Image-to-Video Morphing Studio</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-bg: #0a0a0a; | |
| --secondary-bg: #1a1a1a; | |
| --accent-color: #00ff88; | |
| --spot-color-1: #ff006e; | |
| --spot-color-2: #00f5ff; | |
| --text-primary: #ffffff; | |
| --text-secondary: #888888; | |
| --glass-bg: rgba(30, 30, 40, 0.4); | |
| --glass-border: rgba(255, 255, 255, 0.1); | |
| --control-height: 60px; | |
| --panel-width: 320px; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: var(--primary-bg); | |
| color: var(--text-primary); | |
| overflow: hidden; | |
| height: 100vh; | |
| display: grid; | |
| grid-template-rows: auto 1fr auto; | |
| grid-template-columns: 1fr var(--panel-width); | |
| grid-template-areas: | |
| "header header" | |
| "main controls" | |
| "footer footer"; | |
| } | |
| header { | |
| grid-area: header; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border-bottom: 1px solid var(--glass-border); | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| z-index: 100; | |
| } | |
| header h1 { | |
| font-size: clamp(1.2rem, 2vw, 1.8rem); | |
| font-weight: 300; | |
| letter-spacing: 2px; | |
| background: linear-gradient(45deg, var(--accent-color), var(--spot-color-1)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| header a { | |
| color: var(--accent-color); | |
| text-decoration: none; | |
| font-weight: 500; | |
| transition: all 0.3s ease; | |
| } | |
| header a:hover { | |
| text-shadow: 0 0 10px var(--accent-color); | |
| } | |
| main { | |
| grid-area: main; | |
| perspective: 1200px; | |
| position: relative; | |
| overflow: hidden; | |
| background: radial-gradient(circle at center, #111 0%, #000 100%); | |
| } | |
| .showroom-container { | |
| position: absolute; | |
| inset: 0; | |
| display: grid; | |
| place-items: center; | |
| transform-style: preserve-3d; | |
| transition: transform 0.1s ease-out; | |
| } | |
| #mainCanvas { | |
| width: min(80vw, 80vh); | |
| height: min(80vw, 80vh); | |
| max-width: 600px; | |
| max-height: 600px; | |
| border: 1px solid var(--glass-border); | |
| background: #000; | |
| transform: rotateX(15deg) rotateY(-15deg); | |
| box-shadow: | |
| 0 0 50px rgba(0, 255, 136, 0.2), | |
| 0 0 100px rgba(255, 0, 110, 0.1); | |
| transition: transform 0.3s ease; | |
| } | |
| .canvas-locked-x { | |
| transform: rotateX(15deg) ; | |
| } | |
| .canvas-locked-y { | |
| transform: rotateY(-15deg) ; | |
| } | |
| .overlay-effects { | |
| position: absolute; | |
| inset: 0; | |
| pointer-events: none; | |
| mix-blend-mode: screen; | |
| } | |
| .pulse-effect { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 0; | |
| height: 0; | |
| border-radius: 50%; | |
| background: radial-gradient(circle, var(--accent-color) 0%, transparent 70%); | |
| transform: translate(-50%, -50%); | |
| animation: pulse 0.5s ease-out; | |
| } | |
| @keyframes pulse { | |
| to { | |
| width: 200%; | |
| height: 200%; | |
| opacity: 0; | |
| } | |
| } | |
| .controls-panel { | |
| grid-area: controls; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border-left: 1px solid var(--glass-border); | |
| padding: 1rem; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .control-section { | |
| background: rgba(20, 20, 30, 0.6); | |
| border-radius: 12px; | |
| padding: 1rem; | |
| border: 1px solid var(--glass-border); | |
| } | |
| .control-section h3 { | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: var(--accent-color); | |
| margin-bottom: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .file-input-wrapper { | |
| position: relative; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| input[type="file"] { | |
| position: absolute; | |
| opacity: 0; | |
| width: 100%; | |
| height: 100%; | |
| cursor: pointer; | |
| } | |
| .file-button { | |
| background: linear-gradient(45deg, var(--accent-color), var(--spot-color-2)); | |
| border: none; | |
| padding: 0.75rem 1rem; | |
| border-radius: 8px; | |
| color: var(--primary-bg); | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| } | |
| .file-button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 20px rgba(0, 255, 136, 0.4); | |
| } | |
| .slider-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| margin-bottom: 0.75rem; | |
| } | |
| .slider-label { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| } | |
| input[type="range"] { | |
| width: 100%; | |
| height: 4px; | |
| background: var(--secondary-bg); | |
| border-radius: 2px; | |
| outline: none; | |
| -webkit-appearance: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| background: var(--accent-color); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { | |
| transform: scale(1.2); | |
| box-shadow: 0 0 10px var(--accent-color); | |
| } | |
| .toggle-group { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 0.5rem; | |
| } | |
| .toggle-button { | |
| background: var(--secondary-bg); | |
| border: 1px solid var(--glass-border); | |
| padding: 0.5rem; | |
| border-radius: 6px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 0.8rem; | |
| } | |
| .toggle-button.active { | |
| background: var(--accent-color); | |
| color: var(--primary-bg); | |
| border-color: var(--accent-color); | |
| } | |
| .action-button { | |
| background: linear-gradient(45deg, var(--spot-color-1), var(--spot-color-2)); | |
| border: none; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| color: white; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| width: 100%; | |
| } | |
| .action-button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 25px rgba(255, 0, 110, 0.5); | |
| } | |
| .status-bar { | |
| grid-area: footer; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(20px); | |
| border-top: 1px solid var(--glass-border); | |
| padding: 0.5rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| } | |
| .image-thumbnails { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); | |
| gap: 0.5rem; | |
| max-height: 150px; | |
| overflow-y: auto; | |
| } | |
| .thumbnail { | |
| aspect-ratio: 1; | |
| background: var(--secondary-bg); | |
| border-radius: 4px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| border: 2px solid transparent; | |
| } | |
| .thumbnail:hover { | |
| border-color: var(--accent-color); | |
| transform: scale(1.05); | |
| } | |
| .thumbnail.active { | |
| border-color: var(--spot-color-1); | |
| } | |
| @media (max-width: 768px) { | |
| body { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: auto 1fr auto auto; | |
| grid-template-areas: | |
| "header" | |
| "main" | |
| "controls" | |
| "footer"; | |
| } | |
| .controls-panel { | |
| max-height: 40vh; | |
| } | |
| #mainCanvas { | |
| width: 90vw; | |
| height: 90vw; | |
| } | |
| } | |
| .loading-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.9); | |
| display: none; | |
| place-items: center; | |
| z-index: 1000; | |
| } | |
| .loading-spinner { | |
| width: 60px; | |
| height: 60px; | |
| border: 3px solid var(--glass-border); | |
| border-top-color: var(--accent-color); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .audio-visualizer { | |
| height: 40px; | |
| background: var(--secondary-bg); | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 2px; | |
| padding: 4px; | |
| overflow: hidden; | |
| } | |
| .audio-bar { | |
| flex: 1; | |
| background: linear-gradient(to top, var(--accent-color), var(--spot-color-1)); | |
| border-radius: 2px; | |
| transition: height 0.1s ease; | |
| min-height: 2px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1><i class="fas fa-magic"></i> Advanced Morphing Studio</h1> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">Built with anycoder</a> | |
| </header> | |
| <main> | |
| <div class="showroom-container" id="showroom"> | |
| <canvas id="mainCanvas"></canvas> | |
| <div class="overlay-effects" id="overlayEffects"></div> | |
| </div> | |
| </main> | |
| <aside class="controls-panel"> | |
| <div class="control-section"> | |
| <h3><i class="fas fa-images"></i> Bilder</h3> | |
| <div class="file-input-wrapper"> | |
| <button class="file-button" onclick="document.getElementById('folderInput').click()"> | |
| <i class="fas fa-folder-open"></i> Ordner laden | |
| </button> | |
| <input type="file" id="folderInput" webkitdirectory directory multiple accept="image/*"> | |
| <button class="file-button" onclick="document.getElementById('fileInput').click()"> | |
| <i class="fas fa-file-image"></i> Einzelbilder | |
| </button> | |
| <input type="file" id="fileInput" multiple accept="image/*"> | |
| </div> | |
| <div class="image-thumbnails" id="thumbnails"></div> | |
| </div> | |
| <div class="control-section"> | |
| <h3><i class="fas fa-sliders-h"></i> Farbanalyse</h3> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Kontrast Winkel</span> | |
| <span id="contrastValue">50</span> | |
| </div> | |
| <input type="range" id="contrastAngle" min="0" max="100" value="50"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Lichtverschiebung</span> | |
| <span id="lightShiftValue">0</span> | |
| </div> | |
| <input type="range" id="lightShift" min="-100" max="100" value="0"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Spot Farbe 1</span> | |
| <span id="spotColor1Value">#ff006e</span> | |
| </div> | |
| <input type="color" id="spotColor1" value="#ff006e" style="width: 100%; height: 30px; border: none; border-radius: 6px;"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Spot Farbe 2</span> | |
| <span id="spotColor2Value">#00f5ff</span> | |
| </div> | |
| <input type="color" id="spotColor2" value="#00f5ff" style="width: 100%; height: 30px; border: none; border-radius: 6px;"> | |
| </div> | |
| </div> | |
| <div class="control-section"> | |
| <h3><i class="fas fa-cube"></i> 3D Effekte</h3> | |
| <div class="toggle-group"> | |
| <button class="toggle-button active" id="toggle3D">3D Raum</button> | |
| <button class="toggle-button" id="toggleDoubleExp">Double Exposure</button> | |
| <button class="toggle-button" id="toggleMorph">Morphing</button> | |
| <button class="toggle-button" id="togglePulse">Pulse</button> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>3D Tiefe</span> | |
| <span id="depth3DValue">50</span> | |
| </div> | |
| <input type="range" id="depth3D" min="0" max="100" value="50"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Morph Stärke</span> | |
| <span id="morphStrengthValue">30</span> | |
| </div> | |
| <input type="range" id="morphStrength" min="0" max="100" value="30"> | |
| </div> | |
| </div> | |
| <div class="control-section"> | |
| <h3><i class="fas fa-music"></i> Audio Engine</h3> | |
| <div class="file-input-wrapper"> | |
| <button class="file-button" onclick="document.getElementById('audioInput').click()"> | |
| <i class="fas fa-file-audio"></i> Audio importieren | |
| </button> | |
| <input type="file" id="audioInput" accept="audio/*"> | |
| </div> | |
| <div class="audio-visualizer" id="audioVisualizer"></div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Takt (BPM)</span> | |
| <span id="bpmValue">120</span> | |
| </div> | |
| <input type="range" id="bpm" min="60" max="180" value="120"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Fazer Tiefe</span> | |
| <span id="fazerValue">20</span> | |
| </div> | |
| <input type="range" id="fazerDepth" min="0" max="100" value="20"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Flanger Rate</span> | |
| <span id="flangerValue">0.5</span> | |
| </div> | |
| <input type="range" id="flangerRate" min="0" max="5" step="0.1" value="0.5"> | |
| </div> | |
| <button class="action-button" id="generateAudioBtn"> | |
| <i class="fas fa-play"></i> Generiere Space Audio | |
| </button> | |
| </div> | |
| <div class="control-section"> | |
| <h3><i class="fas fa-video"></i> Video Output</h3> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Frame Rate</span> | |
| <span id="fpsValue">30</span> | |
| </div> | |
| <input type="range" id="fps" min="15" max="60" value="30"> | |
| </div> | |
| <div class="slider-group"> | |
| <div class="slider-label"> | |
| <span>Übergangszeit (s)</span> | |
| <span id="transitionValue">2</span> | |
| </div> | |
| <input type="range" id="transitionTime" min="1" max="10" value="2"> | |
| </div> | |
| <button class="action-button" id="renderBtn"> | |
| <i class="fas fa-film"></i> Rendere Video | |
| </button> | |
| <video id="outputVideo" style="width: 100%; margin-top: 1rem; border-radius: 8px; display: none;"></video> | |
| </div> | |
| </aside> | |
| <footer class="status-bar"> | |
| <div id="statusText">Bereit - Lade Bilder um zu starten</div> | |
| <div id="sensorStatus">Sensor: Inaktiv</div> | |
| </footer> | |
| <div class="loading-overlay" id="loadingOverlay"> | |
| <div class="loading-spinner"></div> | |
| </div> | |
| <script> | |
| // Global State | |
| const state = { | |
| images: [], | |
| currentImageIndex: 0, | |
| canvas: null, | |
| ctx: null, | |
| audioContext: null, | |
| audioNodes: {}, | |
| isRendering: false, | |
| lockedAxes: { x: false, y: false }, | |
| effects: { | |
| contrastAngle: 50, | |
| lightShift: 0, | |
| spotColor1: '#ff006e', | |
| spotColor2: '#00f5ff', | |
| depth3D: 50, | |
| morphStrength: 30, | |
| doubleExposure: false, | |
| pulse: false, | |
| bpm: 120, | |
| fazerDepth: 20, | |
| flangerRate: 0.5, | |
| fps: 30, | |
| transitionTime: 2 | |
| } | |
| }; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initializeCanvas(); | |
| initializeControls(); | |
| initializeAudio(); | |
| initializeSensors(); | |
| createAudioVisualizer(); | |
| }); | |
| function initializeCanvas() { | |
| state.canvas = document.getElementById('mainCanvas'); | |
| state.ctx = state.canvas.getContext('2d'); | |
| state.canvas.width = 800; | |
| state.canvas.height = 800; | |
| } | |
| function initializeControls() { | |
| // File inputs | |
| document.getElementById('folderInput').addEventListener('change', handleFolderSelect); | |
| document.getElementById('fileInput').addEventListener('change', handleFileSelect); | |
| document.getElementById('audioInput').addEventListener('change', handleAudioImport); | |
| // Sliders | |
| const sliders = [ | |
| 'contrastAngle', 'lightShift', 'depth3D', 'morphStrength', | |
| 'bpm', 'fazerDepth', 'flangerRate', 'fps', 'transitionTime' | |
| ]; | |
| sliders.forEach(id => { | |
| const slider = document.getElementById(id); | |
| const valueSpan = document.getElementById(id + 'Value'); | |
| slider.addEventListener('input', (e) => { | |
| const value = e.target.value; | |
| valueSpan.textContent = value; | |
| state.effects[id] = parseFloat(value); | |
| updateProcessing(); | |
| }); | |
| }); | |
| // Color pickers | |
| document.getElementById('spotColor1').addEventListener('input', (e) => { | |
| state.effects.spotColor1 = e.target.value; | |
| document.getElementById('spotColor1Value').textContent = e.target.value; | |
| }); | |
| document.getElementById('spotColor2').addEventListener('input', (e) => { | |
| state.effects.spotColor2 = e.target.value; | |
| document.getElementById('spotColor2Value').textContent = e.target.value; | |
| }); | |
| // Toggle buttons | |
| document.getElementById('toggle3D').addEventListener('click', toggleEffect); | |
| document.getElementById('toggleDoubleExp').addEventListener('click', toggleEffect); | |
| document.getElementById('toggleMorph').addEventListener('click', toggleEffect); | |
| document.getElementById('togglePulse').addEventListener('click', toggleEffect); | |
| // Action buttons | |
| document.getElementById('generateAudioBtn').addEventListener('click', generateSpaceAudio); | |
| document.getElementById('renderBtn').addEventListener('click', renderVideo); | |
| } | |
| function toggleEffect(e) { | |
| e.target.classList.toggle('active'); | |
| const effectMap = { | |
| 'toggle3D': 'depth3D', | |
| 'toggleDoubleExp': 'doubleExposure', | |
| 'toggleMorph': 'morphing', | |
| 'togglePulse': 'pulse' | |
| }; | |
| const effect = effectMap[e.target.id]; | |
| if (effect) state.effects[effect] = e.target.classList.contains('active'); | |
| updateProcessing(); | |
| } | |
| async function handleFolderSelect(e) { | |
| const files = Array.from(e.target.files).filter(f => f.type.startsWith('image/')); | |
| await loadImages(files); | |
| } | |
| async function handleFileSelect(e) { | |
| await loadImages(Array.from(e.target.files)); | |
| } | |
| async function loadImages(files) { | |
| showLoading(true); | |
| state.images = []; | |
| const thumbnails = document.getElementById('thumbnails'); | |
| thumbnails.innerHTML = ''; | |
| for (const file of files) { | |
| const img = await createImageBitmap(file); | |
| state.images.push(img); | |
| const thumb = document.createElement('div'); | |
| thumb.className = 'thumbnail'; | |
| thumb.style.backgroundImage = `url(${URL.createObjectURL(file)})`; | |
| thumb.style.backgroundSize = 'cover'; | |
| thumb.onclick = () => setActiveImage(state.images.length - 1); | |
| thumbnails.appendChild(thumb); | |
| } | |
| if (state.images.length > 0) { | |
| setActiveImage(0); | |
| updateStatus(`${state.images.length} Bilder geladen`); | |
| } | |
| showLoading(false); | |
| } | |
| function setActiveImage(index) { | |
| state.currentImageIndex = index; | |
| document.querySelectorAll('.thumbnail').forEach((thumb, i) => { | |
| thumb.classList.toggle('active', i === index); | |
| }); | |
| updateProcessing(); | |
| } | |
| function updateProcessing() { | |
| if (state.images.length === 0) return; | |
| const img = state.images[state.currentImageIndex]; | |
| const canvas = state.canvas; | |
| const ctx = state.ctx; | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Apply long exposure effect with previous image | |
| if (state.currentImageIndex > 0 && state.effects.morphing) { | |
| const prevImg = state.images[state.currentImageIndex - 1]; | |
| applyLongExposureMorph(ctx, prevImg, img, state.effects.morphStrength / 100); | |
| } else { | |
| ctx.drawImage(img, 0, 0, canvas.width, canvas.height); | |
| } | |
| // Analyze and apply color grading | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| applyColorAnalysis(imageData); | |
| ctx.putImageData(imageData, 0, 0); | |
| // Apply double exposure if enabled | |
| if (state.effects.doubleExposure && state.currentImageIndex < state.images.length - 1) { | |
| const nextImg = state.images[state.currentImageIndex + 1]; | |
| applyDoubleExposure(ctx, nextImg, state.effects.depth3D / 100); | |
| } | |
| // Apply 3D displacement | |
| if (state.effects.depth3D) { | |
| apply3DDisplacement(ctx, state.effects.depth3D); | |
| } | |
| } | |
| function applyLongExposureMorph(ctx, img1, img2, strength) { | |
| const tempCanvas = document.createElement('canvas'); | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| tempCanvas.width = ctx.canvas.width; | |
| tempCanvas.height = ctx.canvas.height; | |
| // Draw first image with fade | |
| tempCtx.globalAlpha = 1 - strength * 0.5; | |
| tempCtx.drawImage(img1, 0, 0); | |
| // Draw second image with morph | |
| tempCtx.globalAlpha = strength; | |
| tempCtx.filter = `blur(${strength * 2}px)`; | |
| tempCtx.drawImage(img2, 0, 0); | |
| ctx.globalAlpha = 1; | |
| ctx.filter = 'none'; | |
| ctx.drawImage(tempCanvas, 0, 0); | |
| } | |
| function applyColorAnalysis(imageData) { | |
| const data = imageData.data; | |
| const contrast = state.effects.contrastAngle / 50; | |
| const lightShift = state.effects.lightShift; | |
| // Extract spot colors | |
| const spot1 = hexToRgb(state.effects.spotColor1); | |
| const spot2 = hexToRgb(state.effects.spotColor2); | |
| for (let i = 0; i < data.length; i += 4) { | |
| // Convert to grayscale with custom weights | |
| let gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114; | |
| // Apply contrast curve | |
| gray = 128 + (gray - 128) * contrast; | |
| gray = Math.max(0, Math.min(255, gray + lightShift)); | |
| // Determine which spot color to apply based on pixel position | |
| const pixelIndex = i / 4; | |
| const x = (pixelIndex % imageData.width) / imageData.width; | |
| const y = Math.floor(pixelIndex / imageData.width) / imageData.height; | |
| const spotMix = (x + y) / 2; | |
| const spotColor = spotMix > 0.5 ? spot1 : spot2; | |
| const mixFactor = Math.abs(spotMix - 0.5) * 2; | |
| // Mix grayscale with spot color | |
| data[i] = gray * (1 - mixFactor * 0.3) + spotColor.r * mixFactor * 0.3; | |
| data[i + 1] = gray * (1 - mixFactor * 0.3) + spotColor.g * mixFactor * 0.3; | |
| data[i + 2] = gray * (1 - mixFactor * 0.3) + spotColor.b * mixFactor * 0.3; | |
| } | |
| } | |
| function applyDoubleExposure(ctx, overlayImg, opacity) { | |
| ctx.globalAlpha = opacity * 0.5; | |
| ctx.globalCompositeOperation = 'screen'; | |
| ctx.drawImage(overlayImg, 0, 0, ctx.canvas.width, ctx.canvas.height); | |
| ctx.globalAlpha = 1; | |
| ctx.globalCompositeOperation = 'source-over'; | |
| } | |
| function apply3DDisplacement(ctx, depth) { | |
| const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); | |
| const newData = ctx.createImageData(ctx.canvas.width, ctx.canvas.height); | |
| const displacement = depth / 10; | |
| for (let y = 0; y < ctx.canvas.height; y++) { | |
| for (let x = 0; x < ctx.canvas.width; x++) { | |
| const offsetX = Math.sin(y * 0.01) * displacement; | |
| const offsetY = Math.cos(x * 0.01) * displacement; | |
| const srcX = Math.max(0, Math.min(ctx.canvas.width - 1, x + offsetX)); | |
| const srcY = Math.max(0, Math.min(ctx.canvas.height - 1, y + offsetY)); | |
| const srcIndex = (Math.floor(srcY) * ctx.canvas.width + Math.floor(srcX)) * 4; | |
| const destIndex = (y * ctx.canvas.width + x) * 4; | |
| newData.data[destIndex] = imageData.data[srcIndex]; | |
| newData.data[destIndex + 1] = imageData.data[srcIndex + 1]; | |
| newData.data[destIndex + 2] = imageData.data[srcIndex + 2]; | |
| newData.data[destIndex + 3] = imageData.data[srcIndex + 3]; | |
| } | |
| } | |
| ctx.putImageData(newData, 0, 0); | |
| } | |
| function hexToRgb(hex) { | |
| const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); | |
| return result ? { | |
| r: parseInt(result[1], 16), | |
| g: parseInt(result[2], 16), | |
| b: parseInt(result[3], 16) | |
| } : null; | |
| } | |
| function initializeAudio() { | |
| state.audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| function createAudioVisualizer() { | |
| const visualizer = document.getElementById('audioVisualizer'); | |
| for (let i = 0; i < 32; i++) { | |
| const bar = document.createElement('div'); | |
| bar.className = 'audio-bar'; | |
| bar.style.height = '2px'; | |
| visualizer.appendChild(bar); | |
| } | |
| } | |
| function generateSpaceAudio() { | |
| if (!state.audioContext) return; | |
| updateStatus('Generiere Space Audio...'); | |
| // Create drone | |
| const oscillator = state.audioContext.createOscillator(); | |
| const gainNode = state.audioContext.createGain(); | |
| const filter = state.audioContext.createBiquadFilter(); | |
| oscillator.type = 'sawtooth'; | |
| oscillator.frequency.setValueAtTime(55, state.audioContext.currentTime); // A1 | |
| filter.type = 'lowpass'; | |
| filter.frequency.setValueAtTime(200, state.audioContext.currentTime); | |
| filter.Q.setValueAtTime(10, state.audioContext.currentTime); | |
| gainNode.gain.setValueAtTime(0.1, state.audioContext.currentTime); | |
| // Create LFO for pulsating | |
| const lfo = state.audioContext.createOscillator(); | |
| const lfoGain = state.audioContext.createGain(); | |
| lfo.frequency.setValueAtTime(state.effects.bpm / 60, state.audioContext.currentTime); | |
| lfoGain.gain.setValueAtTime(0.05, state.audioContext.currentTime); | |
| lfo.connect(lfoGain); | |
| lfoGain.connect(gainNode.gain); | |
| // Create stereo panner for binaural effect | |
| const panner = state.audioContext.createStereoPanner(); | |
| panner.pan.setValueAtTime(-0.5, state.audioContext.currentTime); | |
| // Create delay for space echo | |
| const delay = state.audioContext.createDelay(1); | |
| delay.delayTime.setValueAtTime(0.5, state.audioContext.currentTime); | |
| const delayGain = state.audioContext.createGain(); | |
| delayGain.gain.setValueAtTime(0.3, state.audioContext.currentTime); | |
| // Create flanger | |
| const flanger = state.audioContext.createDelay(0.05); | |
| flanger.delayTime.setValueAtTime(0.01, state.audioContext.currentTime); | |
| const flangerLfo = state.audioContext.createOscillator(); | |
| flangerLfo.frequency.setValueAtTime(state.effects.flangerRate, state.audioContext.currentTime); | |
| const flangerGain = state.audioContext.createGain(); | |
| flangerGain.gain.setValueAtTime(0.01, state.audioContext.currentTime); | |
| flangerLfo.connect(flangerGain); | |
| flangerGain.connect(flanger.delayTime); | |
| // Connect nodes | |
| oscillator.connect(filter); | |
| filter.connect(gainNode); | |
| gainNode.connect(delay); | |
| gainNode.connect(panner); | |
| delay.connect(delayGain); | |
| delayGain.connect(panner); | |
| panner.connect(state.audioContext.destination); | |
| flangerLfo.start(); | |
| lfo.start(); | |
| oscillator.start(); | |
| // Store nodes for later manipulation | |
| state.audioNodes = { oscillator, gainNode, filter, lfo, panner, flangerLfo }; | |
| // Generate random thunder/explosions | |
| setInterval(() => { | |
| if (Math.random() > 0.7) { | |
| createThunderEffect(); | |
| } | |
| }, 3000); | |
| updateStatus('Space Audio aktiv'); | |
| } | |
| function createThunderEffect() { | |
| if (!state.audioContext) return; | |
| const noise = state.audioContext.createBufferSource(); | |
| const buffer = state.audioContext.createBuffer(1, state.audioContext.sampleRate * 2, state.audioContext.sampleRate); | |
| const data = buffer.getChannelData(0); | |
| for (let i = 0; i < buffer.length; i++) { | |
| data[i] = Math.random() * 2 - 1; | |
| } | |
| noise.buffer = buffer; | |
| const filter = state.audioContext.createBiquadFilter(); | |
| filter.type = 'lowpass'; | |
| filter.frequency.setValueAtTime(100, state.audioContext.currentTime); | |
| filter.frequency.exponentialRampToValueAtTime(20, state.audioContext.currentTime + 2); | |
| const gainNode = state.audioContext.createGain(); | |
| gainNode.gain.setValueAtTime(0.5, state.audioContext.currentTime); | |
| gainNode.gain.exponentialRampToValueAtTime(0.001, state.audioContext.currentTime + 2); | |
| noise.connect(filter); | |
| filter.connect(gainNode); | |
| gainNode.connect(state.audioContext.destination); | |
| noise.start(); | |
| noise.stop(state.audioContext.currentTime + 2); | |
| // Trigger visual pulse | |
| if (state.effects.pulse) { | |
| createPulseEffect(); | |
| } | |
| } | |
| function createPulseEffect() { | |
| const overlay = document.getElementById('overlayEffects'); | |
| const pulse = document.createElement('div'); | |
| pulse.className = 'pulse-effect'; | |
| overlay.appendChild(pulse); | |
| setTimeout(() => pulse.remove(), 500); | |
| } | |
| function handleAudioImport(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = async (event) => { | |
| const audioBuffer = await state.audioContext.decodeAudioData(event.target.result); | |
| analyzeAudioBeat(audioBuffer); | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| } | |
| function analyzeAudioBeat(buffer) { | |
| // Simple beat detection | |
| const channelData = buffer.getChannelData(0); | |
| const sampleRate = buffer.sampleRate; | |
| const bpm = detectBPM(channelData, sampleRate); | |
| state.effects.bpm = bpm; | |
| document.getElementById('bpm').value = bpm; | |
| document.getElementById('bpmValue').textContent = bpm; | |
| updateStatus(`BPM erkannt: ${bpm}`); | |
| } | |
| function detectBPM(data, sampleRate) { | |
| // Simplified beat detection - returns average energy peaks | |
| const frameSize = Math.floor(sampleRate * 0.1); | |
| let sum = 0; | |
| let peaks = 0; | |
| for (let i = 0; i < data.length; i += frameSize) { | |
| let energy = 0; | |
| for (let j = i; j < Math.min(i + frameSize, data.length); j++) { | |
| energy += Math.abs(data[j]); | |
| } | |
| if (energy > 0.1) peaks++; | |
| sum += energy; | |
| } | |
| return Math.max(60, Math.min(180, Math.floor(60 / (peaks / (data.length / sampleRate))))); | |
| } | |
| function initializeSensors() { | |
| if (window.DeviceOrientationEvent) { | |
| window.addEventListener('deviceorientation', handleOrientation); | |
| document.getElementById('sensorStatus').textContent = 'Sensor: Aktiv'; | |
| } else { | |
| // Fallback to mouse interaction | |
| let mouseX = 0, mouseY = 0; | |
| document.addEventListener('mousemove', (e) => { | |
| mouseX = (e.clientX / window.innerWidth - 0.5) * 2; | |
| mouseY = (e.clientY / window.innerHeight - 0.5) * 2; | |
| update3DView(mouseY * 30, mouseX * 30); | |
| }); | |
| document.getElementById('sensorStatus').textContent = 'Sensor: Maus-Modus'; | |
| } | |
| // Click to lock axes | |
| document.getElementById('showroom').addEventListener('click', (e) => { | |
| if (e.shiftKey) { | |
| state.lockedAxes.x = !state.lockedAxes.x; | |
| state.canvas.classList.toggle('canvas-locked-x', state.lockedAxes.x); | |
| } else if (e.ctrlKey) { | |
| state.lockedAxes.y = !state.lockedAxes.y; | |
| state.canvas.classList.toggle('canvas-locked-y', state.lockedAxes.y); | |
| } | |
| }); | |
| } | |
| function handleOrientation(event) { | |
| const alpha = event.alpha || 0; | |
| const beta = event.beta || 0; | |
| const gamma = event.gamma || 0; | |
| const rotX = state.lockedAxes.x ? 15 : beta * 0.5; | |
| const rotY = state.lockedAxes.y ? -15 : gamma * 0.5; | |
| update3DView(rotX, rotY); | |
| // Trigger effects based on movement | |
| if (Math.abs(beta) > 30 || Math.abs(gamma) > 30) { | |
| createThunderEffect(); | |
| } | |
| } | |
| function update3DView(rotX, rotY) { | |
| const showroom = document.getElementById('showroom'); | |
| if (!state.lockedAxes.x && !state.lockedAxes.y) { | |
| showroom.style.transform = `rotateX(${rotX}deg) rotateY(${rotY}deg)`; | |
| } else if (state.lockedAxes.x) { | |
| showroom.style.transform = `rotateY(${rotY}deg)`; | |
| } else if (state.lockedAxes.y) { | |
| showroom.style.transform = `rotateX(${rotX}deg)`; | |
| } | |
| } | |
| async function renderVideo() { | |
| if (state.images.length < 2) { | |
| updateStatus('Mindestens 2 Bilder für Video benötigt'); | |
| return; | |
| } | |
| showLoading(true); | |
| updateStatus('Rendere Video...'); | |
| const fps = state.effects.fps; | |
| const duration = state.effects.transitionTime; | |
| const totalFrames = fps * duration * (state.images.length - 1); | |
| const stream = state.canvas.captureStream(fps); | |
| const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' }); | |
| const chunks = []; | |
| recorder.ondataavailable = (e) => chunks.push(e.data); | |
| recorder.onstop = () => { | |
| const blob = new Blob(chunks, { type: 'video/webm' }); | |
| const video = document.getElementById('outputVideo'); | |
| video.src = URL.createObjectURL(blob); | |
| video.style.display = 'block'; | |
| showLoading(false); | |
| updateStatus('Video fertig!'); | |
| }; | |
| recorder.start(); | |
| // Render frames | |
| for (let i = 0; i < state.images.length - 1; i++) { | |
| for (let frame = 0; frame < fps * duration; frame++) { | |
| const progress = frame / (fps * duration); | |
| // Interpolate between images | |
| await interpolateFrames(state.images[i], state.images[i + 1], progress); | |
| // Update audio visualizer | |
| updateVisualizer(); | |
| await new Promise(r => setTimeout(r, 1000 / fps)); | |
| } | |
| } | |
| recorder.stop(); | |
| } | |
| async function interpolateFrames(img1, img2, progress) { | |
| const ctx = state.ctx; | |
| const canvas = state.canvas; | |
| // Clear with fade | |
| ctx.globalAlpha = 0.1; | |
| ctx.fillStyle = '#000'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.globalAlpha = 1; | |
| // Draw morphed image |