anycoder-df60dd48 / index.html
HI7RAI's picture
Upload folder using huggingface_hub
134bf98 verified
<!DOCTYPE html>
<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,