Spaces:
Running
Running
| <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>HYPER-FX STUDIO | Ultimate Morph & Shader Engine</title> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Icons --> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <!-- GIF.js --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js"></script> | |
| <!-- Import Map für sauberes Three.js Module Loading --> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/three@0.160.0/build/three.module.js", | |
| "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| colors: { | |
| cyber: { | |
| bg: '#050505', | |
| panel: '#111111', | |
| border: '#333333', | |
| accent: '#00ff9d', // Neon Green | |
| secondary: '#bd00ff', // Neon Purple | |
| alert: '#ff0055', // Neon Red | |
| text: '#eeeeee' | |
| } | |
| }, | |
| fontFamily: { mono: ['monospace'] } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| /* UI Tweaks */ | |
| body { | |
| overflow: hidden; | |
| background: #000; | |
| } | |
| ::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #333; | |
| border-radius: 2px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #00ff9d; | |
| } | |
| .shader-card { | |
| border-left: 2px solid transparent; | |
| transition: all 0.2s; | |
| } | |
| .shader-card:hover { | |
| border-left-color: #00ff9d; | |
| background: #1a1a1a; | |
| } | |
| canvas { | |
| display: block; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| #loader { | |
| position: fixed; | |
| inset: 0; | |
| background: #000; | |
| z-index: 50; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: #00ff9d; | |
| font-family: monospace; | |
| } | |
| /* Range Slider Styling */ | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| background: transparent; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 14px; | |
| width: 14px; | |
| border-radius: 50%; | |
| background: #00ff9d; | |
| cursor: pointer; | |
| margin-top: -5px; | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 4px; | |
| cursor: pointer; | |
| background: #333; | |
| border-radius: 2px; | |
| } | |
| /* Custom Progress Bar for Render */ | |
| .progress-container { | |
| width: 100%; | |
| background-color: #1a1a1a; | |
| border-radius: 0.25rem; | |
| overflow: hidden; | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| width: 0%; | |
| background-color: #ff0055; | |
| transition: width 0.2s ease; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-cyber-bg text-cyber-text h-screen flex flex-col font-sans"> | |
| <!-- Loading Screen --> | |
| <div id="loader"> | |
| <div class="text-center"> | |
| <div class="text-2xl font-bold mb-2 text-cyber-accent">INITIALIZING HYPER CORE</div> | |
| <div class="text-xs text-gray-500">Loading WebGL & Canvas Modules...</div> | |
| <div class="mt-4 w-64 h-1 bg-gray-800 rounded overflow-hidden mx-auto"> | |
| <div id="loader-bar" class="h-full bg-cyber-accent animate-pulse" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Header --> | |
| <header class="h-14 bg-cyber-panel border-b border-cyber-border flex items-center justify-between px-4 z-20 shrink-0 shadow-lg shadow-black"> | |
| <div class="flex items-center gap-3"> | |
| <i data-lucide="cpu" class="text-cyber-accent w-6 h-6"></i> | |
| <div> | |
| <h1 class="font-bold text-sm tracking-widest uppercase">Hyper-FX | |
| <span class="text-cyber-secondary">Studio</span> | |
| </h1> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <!-- Mode Switcher --> | |
| <div class="flex bg-black rounded p-1 border border-cyber-border shadow-inner"> | |
| <button id="modeVideo" class="px-3 py-1 text-xs rounded bg-cyber-accent text-black font-bold transition shadow-[0_0_10px_rgba(0,255,157,0.3)]">VIDEO MODE</button> | |
| <button id="modeImage" class="px-3 py-1 text-xs rounded text-gray-400 hover:text-white transition">GIF MODE</button> | |
| </div> | |
| <div class="h-6 w-px bg-cyber-border"></div> | |
| <button id="btnRandom" class="px-3 py-1 bg-cyber-secondary/20 border border-cyber-secondary text-cyber-secondary text-xs rounded hover:bg-cyber-secondary hover:text-white transition uppercase font-bold hidden" title="Randomize Shaders"> | |
| <i data-lucide="shuffle" class="inline w-3 h-3 mr-1"></i> Random FX | |
| </button> | |
| <button id="btnRenderGif" class="px-3 py-1 bg-cyber-alert/20 border border-cyber-alert text-cyber-alert text-xs rounded hover:bg-cyber-alert hover:text-white transition uppercase font-bold hidden" title="Generate GIF"> | |
| <i data-lucide="film" class="inline w-3 h-3 mr-1"></i> Render GIF | |
| </button> | |
| </div> | |
| </header> | |
| <div class="flex flex-1 overflow-hidden"> | |
| <!-- LEFT: Resource Library --> | |
| <aside class="w-72 bg-cyber-panel border-r border-cyber-border flex flex-col z-10 shrink-0 shadow-xl"> | |
| <!-- Tabs --> | |
| <div class="flex border-b border-cyber-border"> | |
| <button id="tabShaders" class="flex-1 py-2 text-xs font-bold text-cyber-accent border-b-2 border-cyber-accent bg-cyber-border/30 transition-colors">SHADERS</button> | |
| <button id="tabImages" class="flex-1 py-2 text-xs font-bold text-gray-500 hover:text-white transition-colors">IMAGES</button> | |
| </div> | |
| <!-- Content: Shader Library (Video Mode) --> | |
| <div id="panelShaders" class="flex-1 overflow-y-auto flex flex-col"> | |
| <div class="p-3 border-b border-cyber-border bg-black/20"> | |
| <div class="flex items-center gap-2 mb-2"> | |
| <i data-lucide="search" class="text-gray-500 w-3 h-3"></i> | |
| <input type="text" id="searchFx" placeholder="Search Shaders..." class="w-full bg-black border border-cyber-border rounded px-3 py-2 text-xs text-white focus:border-cyber-accent outline-none transition-colors"> | |
| </div> | |
| <div class="flex justify-between items-center text-[10px] text-gray-500 uppercase font-bold px-1"> | |
| <span>Library</span> | |
| <span id="shaderCount">11 Effects</span> | |
| </div> | |
| </div> | |
| <div id="libraryList" class="flex-1 overflow-y-auto p-2 space-y-1 custom-scrollbar"> | |
| <!-- Injected via JS --> | |
| </div> | |
| <!-- Media Input (Video) --> | |
| <div class="p-3 border-t border-cyber-border bg-black/40"> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <label class="cursor-pointer bg-cyber-border hover:bg-cyber-accent hover:text-black text-center py-2 rounded text-xs transition font-bold flex items-center justify-center gap-2"> | |
| <i data-lucide="video" class="w-3 h-3"></i> LOAD VIDEO | |
| <input type="file" id="inpVideo" accept="video/*" class="hidden"> | |
| </label> | |
| <label class="cursor-pointer bg-cyber-border hover:bg-cyber-secondary hover:text-white text-center py-2 rounded text-xs transition font-bold flex items-center justify-center gap-2"> | |
| <i data-lucide="music" class="w-3 h-3"></i> LOAD AUDIO | |
| <input type="file" id="inpAudio" accept="audio/*" class="hidden"> | |
| </label> | |
| </div> | |
| <div class="mt-3 text-[10px] text-gray-600 text-center font-mono"> | |
| Video State: <span id="videoStatus" class="text-gray-400">Idle</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Content: Image Sequence (GIF Mode) --> | |
| <div id="panelImages" class="flex-1 overflow-y-auto flex flex-col hidden"> | |
| <div class="p-3 border-b border-cyber-border"> | |
| <label class="text-[10px] text-gray-500 uppercase font-bold mb-1 block flex items-center gap-2"> | |
| <i data-lucide="layers" class="w-3 h-3"></i> Source URLs (Comma separated) | |
| </label> | |
| <textarea id="imageSource" rows="3" class="w-full bg-black border border-cyber-border rounded px-2 py-1 text-xs text-white focus:border-cyber-accent outline-none mb-2 font-mono resize-none">https://picsum.photos/id/237/400/400,https://picsum.photos/id/238/400/400,https://picsum.photos/id/239/400/400,https://picsum.photos/id/240/400/400</textarea> | |
| <button id="loadImagesBtn" class="w-full bg-cyber-secondary/20 border border-cyber-secondary text-cyber-secondary text-xs rounded py-2 hover:bg-cyber-secondary hover:text-white transition font-bold flex items-center justify-center gap-2"> | |
| <i data-lucide="upload" class="w-3 h-3"></i> LOAD & ANALYZE | |
| </button> | |
| </div> | |
| <div id="mediaList" class="flex-1 overflow-y-auto p-2 grid grid-cols-3 gap-2 content-start"> | |
| <div class="col-span-3 text-center text-xs text-gray-600 mt-4 flex flex-col items-center"> | |
| <i data-lucide="image-off" class="w-8 h-8 mb-2 opacity-50"></i> | |
| <p>No images loaded.</p> | |
| <p class="text-[10px]">Use URLs above to generate a sequence.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- CENTER: Viewport --> | |
| <main class="flex-1 bg-black relative flex items-center justify-center overflow-hidden"> | |
| <!-- Video Container (Three.js) --> | |
| <div id="video-container" class="absolute inset-0 w-full h-full z-10 bg-black"> | |
| <div id="canvas-wrapper" class="w-full h-full relative"></div> | |
| <!-- Video Overlay Controls --> | |
| <div class="absolute bottom-6 left-1/2 -translate-x-1/2 bg-cyber-panel/90 backdrop-blur-md border border-cyber-border rounded-full px-6 py-3 flex items-center gap-6 shadow-2xl shadow-black"> | |
| <button id="btnPlay" class="w-10 h-10 rounded-full bg-white text-black flex items-center justify-center hover:bg-cyber-accent hover:text-black transition-all transform hover:scale-105 shadow-lg"> | |
| <i data-lucide="play" class="w-5 h-5 ml-0.5"></i> | |
| </button> | |
| <div class="flex flex-col"> | |
| <div class="text-xs font-mono text-cyber-accent flex items-center gap-2"> | |
| <i data-lucide="clock" class="w-3 h-3"></i> | |
| <span id="timeDisplay">00:00</span> | |
| </div> | |
| <div class="w-32 h-1 bg-gray-800 rounded-full mt-1 overflow-hidden"> | |
| <div id="videoProgress" class="h-full bg-cyber-accent w-0 transition-all duration-100"></div> | |
| </div> | |
| </div> | |
| <div class="flex flex-col items-end"> | |
| <span class="text-[10px] text-gray-500 uppercase">Active Effects</span> | |
| <span id="activeEffectCount" class="text-cyber-secondary font-bold text-lg">0</span> | |
| </div> | |
| </div> | |
| <!-- Overlay Status Message --> | |
| <div id="viewportMessage" class="absolute inset-0 flex items-center justify-center pointer-events-none opacity-0 transition-opacity duration-500"> | |
| <div class="bg-black/80 px-6 py-3 rounded border border-cyber-accent text-cyber-accent font-mono text-sm backdrop-blur-sm"> | |
| Waiting for video source... | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Image Container (2D Canvas) --> | |
| <div id="image-container" class="absolute inset-0 flex items-center justify-center bg-gray-900 z-0 hidden"> | |
| <div class="relative shadow-2xl border border-cyber-border rounded overflow-hidden max-w-full max-h-full"> | |
| <canvas id="artCanvas" width="600" height="600" class="bg-black block max-h-[80vh] max-w-[90vw]"></canvas> | |
| <div class="absolute bottom-2 right-2 bg-black/60 text-[10px] text-white px-2 py-1 rounded font-mono pointer-events-none"> | |
| Canvas Active | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- RIGHT: Stack & Settings --> | |
| <aside class="w-80 bg-cyber-panel border-l border-cyber-border flex flex-col z-10 shrink-0 shadow-2xl"> | |
| <!-- Video Mode Stack --> | |
| <div id="rightStack" class="flex flex-col h-full"> | |
| <div class="p-3 border-b border-cyber-border flex justify-between items-center bg-black/20"> | |
| <span class="text-xs font-bold text-gray-400 uppercase tracking-wider">ACTIVE SHADER STACK</span> | |
| <span id="stackCount" class="text-[10px] bg-cyber-border px-2 py-0.5 rounded text-white">0/8</span> | |
| </div> | |
| <div id="stackList" class="flex-1 overflow-y-auto p-2 space-y-2 custom-scrollbar bg-black/10"> | |
| <div class="flex flex-col items-center justify-center h-full text-center text-gray-600 text-xs pb-10"> | |
| <i data-lucide="layers" class="w-10 h-10 mb-3 opacity-20"></i> | |
| <p>Stack is empty.</p> | |
| <p class="text-[10px]">Add effects from the left panel.</p> | |
| </div> | |
| </div> | |
| <!-- Snippet Info --> | |
| <div class="p-3 border-t border-cyber-border bg-black/40 text-[10px] text-gray-500 font-mono"> | |
| <div class="flex justify-between mb-1"> | |
| <span class="flex items-center gap-1"><i data-lucide="cpu" class="w-3 h-3"></i> WebGL Renderer</span> | |
| <span class="text-cyber-accent">Active</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="flex items-center gap-1"><i data-lucide="activity" class="w-3 h-3"></i> Audio Sync</span> | |
| <span class="text-cyber-secondary" id="audioStatus">Waiting...</span> | |
| </div> | |
| <div class="flex justify-between mt-1"> | |
| <span class="flex items-center gap-1"><i data-lucide="zap" class="w-3 h-3"></i> FPS</span> | |
| <span id="fpsDisplay" class="text-cyber-accent">60</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- GIF Mode Settings --> | |
| <div id="rightSettings" class="flex flex-col h-full hidden overflow-y-auto bg-black/20"> | |
| <div class="p-3 border-b border-cyber-border flex justify-between items-center bg-black/40"> | |
| <span class="text-xs font-bold text-cyber-alert uppercase tracking-wider">GIF CONFIGURATION</span> | |
| <i data-lucide="settings" class="w-4 h-4 text-cyber-alert"></i> | |
| </div> | |
| <div class="p-4 space-y-6"> | |
| <!-- Animation --> | |
| <div> | |
| <label class="text-[10px] uppercase text-gray-500 font-bold flex items-center gap-2 mb-2"> | |
| <i data-lucide="zap" class="w-3 h-3"></i> Transition Speed | |
| </label> | |
| <input type="range" id="transitionSpeed" min="50" max="2000" value="300" class="w-full mt-1 accent-cyber-accent"> | |
| <div class="flex justify-between text-[10px] text-gray-600 mt-1"> | |
| <span class="font-mono">Fast</span><span id="speedVal" class="font-mono text-cyber-accent">300ms</span><span class="font-mono">Slow</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="text-[10px] uppercase text-gray-500 font-bold flex items-center gap-2 mb-2"> | |
| <i data-lucide="clock" class="w-3 h-3"></i> Hold Duration | |
| </label> | |
| <input type="range" id="holdDuration" min="0" max="2000" value="100" class="w-full mt-1 accent-cyber-accent"> | |
| <div class="flex justify-between text-[10px] text-gray-600 mt-1"> | |
| <span class="font-mono">Instant</span><span id="holdVal" class="font-mono text-cyber-accent">100ms</span><span class="font-mono">Stare</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="text-[10px] uppercase text-gray-500 font-bold flex items-center gap-2 mb-2"> | |
| <i data-lucide="repeat" class="w-3 h-3"></i> Loops | |
| </label> | |
| <input type="number" id="loopCount" value="5" min="1" max="50" class="w-full bg-black border border-cyber-border rounded px-2 py-1 text-xs text-white mt-1 focus:border-cyber-accent outline-none"> | |
| </div> | |
| <div class="border-t border-cyber-border my-2"></div> | |
| <!-- Filters --> | |
| <div> | |
| <label class="text-[10px] uppercase text-gray-500 font-bold flex items-center gap-2 mb-2"> | |
| <i data-lucide="sliders-vertical" class="w-3 h-3"></i> Image Processing | |
| </label> | |
| <div class="space-y-3"> | |
| <div> | |
| <label class="text-[10px] text-gray-600">Contrast</label> | |
| <input type="range" id="contrast" min="0" max="300" value="100" class="w-full mt-1 accent-cyber-secondary"> | |
| </div> | |
| <div> | |
| <label class="text-[10px] text-gray-600">Brightness</label> | |
| <input type="range" id="brightness" min="0" max="200" value="100" class="w-full mt-1 accent-cyber-secondary"> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="text-[10px] uppercase text-gray-500 font-bold block mb-2 flex items-center gap-2"> | |
| <i data-lucide="palette" class="w-3 h-3"></i> Spot Color Overlay | |
| </label> | |
| <div class="flex gap-2 flex-wrap" id="spotColorPreview"> | |
| <!-- Swatches injected via JS --> | |
| </div> | |
| </div> | |
| <div class="border-t border-cyber-border my-2"></div> | |
| <div class="space-y-3"> | |
| <label class="flex items-center gap-3 cursor-pointer group"> | |
| <div class="relative"> | |
| <input type="checkbox" id="grayscaleToggle" class="sr-only peer"> | |
| <div class="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyber-accent"></div> | |
| </div> | |
| <span class="text-xs text-gray-400 group-hover:text-white transition-colors">Grayscale</span> | |
| </label> | |
| <label class="flex items-center gap-3 cursor-pointer group"> | |
| <div class="relative"> | |
| <input type="checkbox" id="glitchToggle" class="sr-only peer"> | |
| <div class="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyber-alert"></div> | |
| </div> | |
| <span class="text-xs text-gray-400 group-hover:text-white transition-colors">Glitch Noise</span> | |
| </label> | |
| <label class="flex items-center gap-3 cursor-pointer group"> | |
| <div class="relative"> | |
| <input type="checkbox" id="rotateToggle" class="sr-only peer"> | |
| <div class="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyber-secondary"></div> | |
| </div> | |
| <span class="text-xs text-gray-400 group-hover:text-white transition-colors">Film Jitter</span> | |
| </label> | |
| </div> | |
| <div class="border-t border-cyber-border my-4"></div> | |
| <!-- Status & Download --> | |
| <div class="bg-black p-3 rounded border border-cyber-border shadow-inner"> | |
| <div class="flex justify-between text-[10px] mb-2 font-mono"> | |
| <span id="renderStatus">Ready to Render</span> | |
| <span id="frameCount" class="text-gray-500">0/0</span> | |
| </div> | |
| <div class="w-full bg-gray-800 h-2 rounded overflow-hidden mb-2 border border-gray-700"> | |
| <div id="renderProgress" class="h-full bg-gradient-to-r from-cyber-alert to-cyber-secondary w-0 transition-all duration-300"></div> | |
| </div> | |
| <a id="downloadLink" href="#" | |
| class="hidden block text-center text-xs text-cyber-accent underline hover:text-white py-2 border border-cyber-accent/50 rounded transition-all hover:bg-cyber-accent/10"> | |
| Download GIF | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| <!-- Hidden Sources --> | |
| <video id="videoSrc" playsinline loop crossorigin="anonymous" style="display:none;"></video> | |
| <audio id="audioSrc" crossorigin="anonymous" style="display:none;"></audio> | |
| <!-- MODULE LOGIC --> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; | |
| import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; | |
| import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; | |
| // --- 1. IMAGE PROCESSOR MODULE (Simulating Python Logic) --- | |
| const ImageProcessor = { | |
| source: null, | |
| processed: false, | |
| analyze: function(sourceUrl) { | |
| console.log(`Analysing image source: ${sourceUrl}...`); | |
| // Simulation of image analysis | |
| return { | |
| status: "success", | |
| confidence: 0.98, | |
| dimensions: "600x600", | |
| format: "JPEG" | |
| }; | |
| }, | |
| processImage: function(imgElement, filters) { | |
| // This function applies the logic defined in the GIF Engine settings | |
| // to a standard Image object context if needed for pre-processing. | |
| return imgElement; | |
| } | |
| }; | |
| // --- GLOBAL STATE --- | |
| const State = { | |
| mode: 'VIDEO', // 'VIDEO' or 'IMAGE' | |
| fps: 0, | |
| lastTime: 0 | |
| }; | |
| // --- 2. SHADER LIBRARY (GLSL SNIPPETS) --- | |
| const SHADER_LIB = [ | |
| { id: 'bw', name: 'Grayscale', cat: 'Color', glsl: `uniform float amount; void main() { vec4 color = texture2D(tDiffuse, vUv); float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); gl_FragColor = vec4(mix(color.rgb, vec3(gray), amount), color.a); }` }, | |
| { id: 'sepia', name: 'Sepia Tone', cat: 'Color', glsl: `uniform float amount; void main() { vec4 color = texture2D(tDiffuse, vUv); vec3 sepia = vec3(dot(color.rgb, vec3(0.393, 0.769, 0.189)), dot(color.rgb, vec3(0.349, 0.686, 0.168)), dot(color.rgb, vec3(0.272, 0.534, 0.131))); gl_FragColor = vec4(mix(color.rgb, sepia, amount), color.a); }` }, | |
| { id: 'invert', name: 'Invert Color', cat: 'Color', glsl: `uniform float amount; void main() { vec4 color = texture2D(tDiffuse, vUv); gl_FragColor = vec4(mix(color.rgb, 1.0 - color.rgb, amount), color.a); }` }, | |
| { id: 'rgb', name: 'RGB Shift', cat: 'Glitch', glsl: `uniform float amount; uniform float uAudio; void main() { float offset = amount * 0.05 * (1.0 + uAudio); vec2 rUv = vUv + vec2(offset, 0.0); vec2 gUv = vUv; vec2 bUv = vUv - vec2(offset, 0.0); vec4 r = texture2D(tDiffuse, rUv); vec4 g = texture2D(tDiffuse, gUv); vec4 b = texture2D(tDiffuse, bUv); gl_FragColor = vec4(r.r, g.g, b.b, 1.0); }` }, | |
| { id: 'pixel', name: 'Pixelate', cat: 'Glitch', glsl: `uniform float amount; uniform vec2 resolution; void main() { float d = 1.0 / (amount * 100.0 + 10.0); vec2 coord = d * floor(vUv / d); gl_FragColor = texture2D(tDiffuse, coord); }` }, | |
| { id: 'noise', name: 'Static Noise', cat: 'Glitch', glsl: `uniform float amount; uniform float time; float rand(vec2 co) { return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); } void main() { vec4 color = texture2D(tDiffuse, vUv); float diff = (rand(vUv + time) - 0.5) * amount; gl_FragColor = vec4(color.rgb + diff, color.a); }` }, | |
| { id: 'scanline', name: 'CRT Scanlines', cat: 'Glitch', glsl: `uniform float amount; uniform vec2 resolution; void main() { vec4 color = texture2D(tDiffuse, vUv); float scanline = sin(vUv.y * resolution.y * 0.5) * 0.1 * amount; gl_FragColor = vec4(color.rgb - scanline, color.a); }` }, | |
| { id: 'vignette', name: 'Dark Vignette', cat: 'Art', glsl: `uniform float amount; void main() { vec4 color = texture2D(tDiffuse, vUv); float dist = distance(vUv, vec2(0.5)); color.rgb *= smoothstep(0.8, 0.8 - amount, dist); gl_FragColor = color; }` }, | |
| { id: 'kaleido', name: 'Kaleidoscope', cat: 'Art', glsl: `uniform float amount; void main() { vec2 uv = vUv - 0.5; float angle = atan(uv.y, uv.x); float radius = length(uv); float segments = 6.0 + floor(amount * 10.0); angle = mod(angle, 6.28318 / segments); angle = abs(angle - 3.14159 / segments); vec2 newUv = vec2(cos(angle), sin(angle)) * radius + 0.5; gl_FragColor = texture2D(tDiffuse, newUv); }` }, | |
| { id: 'contrast', name: 'High Contrast', cat: 'Color', glsl: `uniform float amount; void main() { vec4 c = texture2D(tDiffuse, vUv); gl_FragColor = vec4((c.rgb - 0.5) * (1.0 + amount) + 0.5, c.a); }` }, | |
| { id: 'shake', name: 'Bass Shake', cat: 'Motion', glsl: `uniform float amount; uniform float uAudio; uniform float time; void main() { vec2 uv = vUv; uv.x += sin(time * 50.0) * amount * 0.1 * uAudio; gl_FragColor = texture2D(tDiffuse, uv); }` } | |
| ]; | |
| // --- 3. VIDEO ENGINE (Three.js) --- | |
| const VideoEngine = { | |
| scene: null, | |
| camera: null, | |
| renderer: null, | |
| composer: null, | |
| video: null, | |
| videoTex: null, | |
| audioCtx: null, | |
| analyser: null, | |
| dataArray: null, | |
| stack: [], | |
| maxEffects: 8, | |
| isPlaying: false, | |
| lastFrameTime: 0, | |
| frameCount: 0, | |
| init: () => { | |
| const container = document.getElementById('canvas-wrapper'); | |
| const width = container.clientWidth; | |
| const height = container.clientHeight; | |
| VideoEngine.scene = new THREE.Scene(); | |
| VideoEngine.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); | |
| VideoEngine.renderer = new THREE.WebGLRenderer({ antialias: false, preserveDrawingBuffer: true }); | |
| VideoEngine.renderer.setSize(width, height); | |
| container.appendChild(VideoEngine.renderer.domElement); | |
| VideoEngine.video = document.getElementById('videoSrc'); | |
| VideoEngine.videoTex = new THREE.VideoTexture(VideoEngine.video); | |
| VideoEngine.videoTex.minFilter = THREE.LinearFilter; | |
| VideoEngine.videoTex.magFilter = THREE.LinearFilter; | |
| VideoEngine.composer = new EffectComposer(VideoEngine.renderer); | |
| const renderPass = new RenderPass(VideoEngine.scene, VideoEngine.camera); | |
| const geometry = new THREE.PlaneGeometry(2, 2); | |
| const material = new THREE.MeshBasicMaterial({ map: VideoEngine.videoTex }); | |
| const quad = new THREE.Mesh(geometry, material); | |
| VideoEngine.scene.add(quad); | |
| VideoEngine.composer.addPass(renderPass); | |
| // Setup Audio Context | |
| VideoEngine.setupAudio(); | |
| VideoEngine.buildLibrary(); | |
| VideoEngine.animate(); | |
| }, | |
| setupAudio: () => { | |
| try { | |
| VideoEngine.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| VideoEngine.analyser = VideoEngine.audioCtx.createAnalyser(); | |
| VideoEngine.analyser.fftSize = 256; | |
| VideoEngine.dataArray = new Uint8Array(VideoEngine.analyser.frequencyBinCount); | |
| const audioEl = document.getElementById('audioSrc'); | |
| const source = VideoEngine.audioCtx.createMediaElementSource(audioEl); | |
| source.connect(VideoEngine.analyser); | |
| VideoEngine.analyser.connect(VideoEngine.audioCtx.destination); | |
| } catch (e) { | |
| console.warn("Audio initialization failed", e); | |
| } | |
| }, | |
| buildLibrary: () => { | |
| const list = document.getElementById('libraryList'); | |
| list.innerHTML = ''; | |
| const categories = [...new Set(SHADER_LIB.map(s => s.cat))]; | |
| categories.forEach(cat => { | |
| const header = document.createElement('div'); | |
| header.className = "px-2 py-1 bg-cyber-border/30 text-[10px] font-bold text-cyber-accent uppercase mt-2 mb-1 sticky top-0 backdrop-blur-sm flex items-center gap-2"; | |
| header.innerHTML = `<i data-lucide="layers" class="w-3 h-3"></i> ${cat}`; | |
| list.appendChild(header); | |
| SHADER_LIB.filter(s => s.cat === cat).forEach(shader => { | |
| const el = document.createElement('div'); | |
| el.className = "shader-card p-2 text-xs text-gray-300 cursor-pointer flex justify-between items-center rounded border border-transparent hover:border-cyber-accent/50 transition-all"; | |
| el.innerHTML = `<div class="flex items-center gap-2"><span class="font-bold">${shader.name}</span> <i data-lucide="sliders-vertical" class="w-2 h-2 opacity-50"></i></div> <i data-lucide="plus" class="w-3 h-3 opacity-50 group-hover:text-cyber-accent"></i>`; | |
| el.onclick = () => VideoEngine.addEffect(shader.id); | |
| list.appendChild(el); | |
| }); | |
| }); | |
| lucide.createIcons(); | |
| }, | |
| addEffect: (id) => { | |
| if (VideoEngine.stack.length >= VideoEngine.maxEffects) { | |
| alert("Stack Full! Remove an effect to add more."); | |
| return; | |
| } | |
| const def = SHADER_LIB.find(s => s.id === id); | |
| if (!def) return; | |
| const myUniforms = { | |
| "tDiffuse": { value: null }, | |
| "amount": { value: 0.5 }, | |
| "time": { value: 0.0 }, | |
| "resolution": { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, | |
| "uAudio": { value: 0.0 } | |
| }; | |
| const myShader = { | |
| uniforms: myUniforms, | |
| vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); }`, | |
| fragmentShader: def.glsl | |
| }; | |
| const pass = new ShaderPass(myShader); | |
| pass.uid = Date.now() + Math.random(); | |
| pass.def = def; | |
| VideoEngine.composer.addPass(pass); | |
| VideoEngine.stack.push(pass); | |
| VideoEngine.renderStackUI(); | |
| }, | |
| removeEffect: (uid) => { | |
| const idx = VideoEngine.stack.findIndex(p => p.uid === uid); | |
| if (idx > -1) { | |
| VideoEngine.composer.removePass(VideoEngine.stack[idx]); | |
| VideoEngine.stack.splice(idx, 1); | |
| VideoEngine.renderStackUI(); | |
| } | |
| }, | |
| updateEffectAmount: (uid, val) => { | |
| const pass = VideoEngine.stack.find(p => p.uid === uid); | |
| if (pass) { | |
| pass.uniforms.amount.value = parseFloat(val); | |
| } | |
| }, | |
| renderStackUI: () => { | |
| const container = document.getElementById('stackList'); | |
| document.getElementById('stackCount').innerText = `${VideoEngine.stack.length}/${VideoEngine.maxEffects}`; | |
| document.getElementById('activeEffectCount').innerText = VideoEngine.stack.length; | |
| if (VideoEngine.stack.length === 0) { | |
| container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-center text-gray-600 text-xs pb-10"> | |
| <i data-lucide="layers" class="w-10 h-10 mb-3 opacity-20"></i> | |
| <p>Stack is empty.</p> | |
| <p class="text-[10px]">Add effects from the left panel.</p> | |
| </div>`; | |
| return; | |
| } | |
| container.innerHTML = ''; | |
| VideoEngine.stack.forEach((pass, i) => { | |
| const el = document.createElement('div'); | |
| el.className = "bg-black border border-cyber-border rounded p-3 mb-2 shadow-lg relative group"; | |
| el.innerHTML = ` | |
| <div class="flex justify-between items-center mb-2"> | |
| <div class="flex items-center gap-2"> | |
| <span class="font-bold text-cyber-secondary text-xs">${i+1}.</span> | |
| <span class="text-xs font-medium text-gray-200">${pass.def.name}</span> | |
| </div> | |
| <button class="text-red-500 hover:text-white hover:bg-red-500/20 p-1 rounded transition" onclick="window.removeVideoFx(${pass.uid})"><i data-lucide="x" class="w-3 h-3"></i></button> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <i data-lucide="sliders-vertical" class="w-3 h-3 text-gray-600"></i> | |
| <input type="range" class="w-full h-1 bg-gray-800 rounded appearance-none cursor-pointer accent-cyber-secondary" | |
| min="0" max="1" step="0.01" value="${pass.uniforms.amount.value}" | |
| oninput="window.updateVideoFx(${pass.uid}, this.value)"> | |
| </div> | |
| <div class="flex justify-between text-[9px] text-gray-500 mt-1 font-mono"> | |
| <span>INTENSITY</span> | |
| <span id="val-${pass.uid}">${(pass.uniforms.amount.value * 100).toFixed(0)}%</span> | |
| </div> | |
| `; | |
| container.appendChild(el); | |
| }); | |
| lucide.createIcons(); | |
| }, | |
| animate: () => { | |
| if (State.mode !== 'VIDEO') return; | |
| requestAnimationFrame(VideoEngine.animate); | |
| // FPS Calculation | |
| const now = performance.now(); | |
| const delta = now - State.lastTime; | |
| if (delta >= 1000) { | |
| State.lastTime = now; | |
| State.fps = VideoEngine.frameCount; | |
| VideoEngine.frameCount = 0; | |
| document.getElementById('fpsDisplay').innerText = State.fps; | |
| } | |
| VideoEngine.frameCount++; | |
| let audioLevel = 0; | |
| if (VideoEngine.analyser) { | |
| try { | |
| VideoEngine.analyser.getByteFrequencyData(VideoEngine.dataArray); | |
| // Calculate average bass | |
| let sum = 0; | |
| for(let i=0; i<10; i++) sum += VideoEngine.dataArray[i]; | |
| audioLevel = sum / 10 / 255; | |
| } catch(e) { audioLevel = 0; } | |
| } | |
| const time = performance.now() * 0.001; | |
| VideoEngine.stack.forEach(pass => { | |
| if (pass.uniforms.time) pass.uniforms.time.value = time; | |
| if (pass.uniforms.uAudio) pass.uniforms.uAudio.value = audioLevel; | |
| }); | |
| // Sync Audio | |
| const audioEl = document.getElementById('audioSrc'); | |
| if (audioEl && !VideoEngine.video.paused) { | |
| if (Math.abs(VideoEngine.video.currentTime - audioEl.currentTime) > 0.5) { | |
| audioEl.currentTime = VideoEngine.video.currentTime; | |
| } | |
| } | |
| // Update Time Display | |
| const m = Math.floor(VideoEngine.video.currentTime / 60); | |
| const s = Math.floor(VideoEngine.video.currentTime % 60); | |
| document.getElementById('timeDisplay').innerText = `${m}:${s.toString().padStart(2, '0')}`; | |
| // Update Progress Bar | |
| if (VideoEngine.video.duration > 0) { | |
| const pct = (VideoEngine.video.currentTime / VideoEngine.video.duration) * 100; | |
| document.getElementById('videoProgress').style.width = `${pct}%`; | |
| } | |
| VideoEngine.composer.render(); | |
| } | |
| }; | |
| // --- 4. GIF ENGINE (Canvas 2D) --- | |
| const GifEngine = { | |
| canvas: null, | |
| ctx: null, | |
| images: [], | |
| currentIndex: 0, | |
| isRendering: false, | |
| animationId: null, | |
| filters: { | |
| contrast: 100, | |
| brightness: 100, | |
| grayscale: false, | |
| spotColor: { r: 255, g: 0, b: 85, active: true }, | |
| glitch: false, | |
| rotation: false | |
| }, | |
| animation: { | |
| speed: 300, | |
| hold: 100, | |
| loops: 5 | |
| }, | |
| init: () => { | |
| GifEngine.canvas = document.getElementById('artCanvas'); | |
| GifEngine.ctx = GifEngine.canvas.getContext('2d'); | |
| GifEngine.initSpotColors(); | |
| // Load default images | |
| document.getElementById('loadImagesBtn').click(); | |
| }, | |
| initSpotColors: () => { | |
| const colors = [ | |
| { c: '255,255,255', bg: '#ffffff', name: 'None' }, | |
| { c: '255,0,85', bg: '#ff0055', name: 'Red' }, | |
| { c: '0,204,255', bg: '#00ccff', name: 'Blue' }, | |
| { c: '255,204,0', bg: '#ffcc00', name: 'Gold' }, | |
| { c: '51,255, |