HI7RAI commited on
Commit
134bf98
·
verified ·
1 Parent(s): 7e8f596

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +475 -611
index.html CHANGED
@@ -118,6 +118,20 @@
118
  background: #333;
119
  border-radius: 2px;
120
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  </style>
122
  </head>
123
 
@@ -126,15 +140,18 @@
126
  <!-- Loading Screen -->
127
  <div id="loader">
128
  <div class="text-center">
129
- <div class="text-2xl font-bold mb-2">INITIALIZING HYPER CORE</div>
130
  <div class="text-xs text-gray-500">Loading WebGL & Canvas Modules...</div>
 
 
 
131
  </div>
132
  </div>
133
 
134
  <!-- Header -->
135
- <header class="h-14 bg-cyber-panel border-b border-cyber-border flex items-center justify-between px-4 z-20 shrink-0">
136
  <div class="flex items-center gap-3">
137
- <i data-lucide="cpu" class="text-cyber-accent"></i>
138
  <div>
139
  <h1 class="font-bold text-sm tracking-widest uppercase">Hyper-FX
140
  <span class="text-cyber-secondary">Studio</span>
@@ -144,8 +161,8 @@
144
 
145
  <div class="flex items-center gap-4">
146
  <!-- Mode Switcher -->
147
- <div class="flex bg-black rounded p-1 border border-cyber-border">
148
- <button id="modeVideo" class="px-3 py-1 text-xs rounded bg-cyber-accent text-black font-bold transition">VIDEO MODE</button>
149
  <button id="modeImage" class="px-3 py-1 text-xs rounded text-gray-400 hover:text-white transition">GIF MODE</button>
150
  </div>
151
 
@@ -163,178 +180,258 @@
163
  <div class="flex flex-1 overflow-hidden">
164
 
165
  <!-- LEFT: Resource Library -->
166
- <aside class="w-72 bg-cyber-panel border-r border-cyber-border flex flex-col z-10 shrink-0">
167
  <!-- Tabs -->
168
  <div class="flex border-b border-cyber-border">
169
- <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">SHADERS</button>
170
- <button id="tabImages" class="flex-1 py-2 text-xs font-bold text-gray-500 hover:text-white">IMAGES</button>
171
  </div>
172
 
173
  <!-- Content: Shader Library (Video Mode) -->
174
  <div id="panelShaders" class="flex-1 overflow-y-auto flex flex-col">
175
- <div class="p-3 border-b border-cyber-border">
176
- <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">
 
 
 
 
 
 
 
177
  </div>
178
- <div id="libraryList" class="flex-1 overflow-y-auto p-2 space-y-1">
179
  <!-- Injected via JS -->
180
  </div>
181
 
182
  <!-- Media Input (Video) -->
183
  <div class="p-3 border-t border-cyber-border bg-black/40">
184
  <div class="grid grid-cols-2 gap-2">
185
- <label class="cursor-pointer bg-cyber-border hover:bg-cyber-accent hover:text-black text-center py-2 rounded text-xs transition font-bold">
186
- LOAD VIDEO
187
  <input type="file" id="inpVideo" accept="video/*" class="hidden">
188
  </label>
189
- <label class="cursor-pointer bg-cyber-border hover:bg-cyber-secondary hover:text-white text-center py-2 rounded text-xs transition font-bold">
190
- LOAD AUDIO
191
  <input type="file" id="inpAudio" accept="audio/*" class="hidden">
192
  </label>
193
  </div>
 
 
 
194
  </div>
195
  </div>
196
 
197
  <!-- Content: Image Sequence (GIF Mode) -->
198
  <div id="panelImages" class="flex-1 overflow-y-auto flex flex-col hidden">
199
  <div class="p-3 border-b border-cyber-border">
200
- <label class="text-[10px] text-gray-500 uppercase font-bold mb-1 block">Source URLs (Comma separated)</label>
201
- <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">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>
202
- <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">LOAD & ANALYZE</button>
 
 
 
 
203
  </div>
204
  <div id="mediaList" class="flex-1 overflow-y-auto p-2 grid grid-cols-3 gap-2 content-start">
205
- <div class="col-span-3 text-center text-xs text-gray-600 mt-4">No images loaded.</div>
 
 
 
 
206
  </div>
207
  </div>
208
  </aside>
209
 
210
  <!-- CENTER: Viewport -->
211
  <main class="flex-1 bg-black relative flex items-center justify-center overflow-hidden">
212
-
213
  <!-- Video Container (Three.js) -->
214
- <div id="video-container" class="absolute inset-0 w-full h-full z-10">
215
- <div id="canvas-wrapper" class="w-full h-full"></div>
216
-
217
  <!-- Video Overlay Controls -->
218
- <div
219
- class="absolute bottom-6 left-1/2 -translate-x-1/2 bg-cyber-panel/90 backdrop-blur border border-cyber-border rounded-full px-6 py-2 flex items-center gap-4 shadow-2xl">
220
- <button id="btnPlay" class="text-white hover:text-cyber-accent"><i data-lucide="play"></i></button>
221
- <div class="text-xs font-mono text-cyber-accent" id="timeDisplay">00:00</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  </div>
223
  </div>
224
 
225
  <!-- Image Container (2D Canvas) -->
226
  <div id="image-container" class="absolute inset-0 flex items-center justify-center bg-gray-900 z-0 hidden">
227
- <canvas id="artCanvas" width="600" height="600"
228
- class="max-w-full max-h-full shadow-2xl border border-cyber-border"></canvas>
 
 
 
 
229
  </div>
230
 
231
  </main>
232
 
233
  <!-- RIGHT: Stack & Settings -->
234
- <aside class="w-80 bg-cyber-panel border-l border-cyber-border flex flex-col z-10 shrink-0">
235
-
236
  <!-- Video Mode Stack -->
237
  <div id="rightStack" class="flex flex-col h-full">
238
- <div class="p-3 border-b border-cyber-border flex justify-between items-center">
239
- <span class="text-xs font-bold text-gray-400">ACTIVE SHADERS</span>
240
- <span id="stackCount" class="text-[10px] bg-cyber-border px-1 rounded">0/8</span>
241
  </div>
242
- <div id="stackList" class="flex-1 overflow-y-auto p-2 space-y-2">
243
- <div class="text-center mt-10 text-gray-600 text-xs">Stack is empty.</div>
 
 
 
 
244
  </div>
245
 
246
  <!-- Snippet Info -->
247
  <div class="p-3 border-t border-cyber-border bg-black/40 text-[10px] text-gray-500 font-mono">
248
  <div class="flex justify-between mb-1">
249
- <span>WebGL Renderer</span>
250
  <span class="text-cyber-accent">Active</span>
251
  </div>
252
  <div class="flex justify-between">
253
- <span>Audio Sync</span>
254
  <span class="text-cyber-secondary" id="audioStatus">Waiting...</span>
255
  </div>
 
 
 
 
256
  </div>
257
  </div>
258
 
259
  <!-- GIF Mode Settings -->
260
- <div id="rightSettings" class="flex flex-col h-full hidden overflow-y-auto">
261
- <div class="p-3 border-b border-cyber-border flex justify-between items-center">
262
- <span class="text-xs font-bold text-cyber-alert">GIF SETTINGS</span>
 
263
  </div>
264
 
265
- <div class="p-4 space-y-4">
266
  <!-- Animation -->
267
  <div>
268
- <label class="text-[10px] uppercase text-gray-500 font-bold">Transition Speed</label>
269
- <input type="range" id="transitionSpeed" min="50" max="2000" value="300" class="w-full mt-1">
270
- <div class="flex justify-between text-[10px] text-gray-600">
271
- <span>Fast</span><span id="speedVal">300ms</span><span>Slow</span>
 
 
272
  </div>
273
  </div>
274
 
275
  <div>
276
- <label class="text-[10px] uppercase text-gray-500 font-bold">Hold Duration</label>
277
- <input type="range" id="holdDuration" min="0" max="2000" value="100" class="w-full mt-1">
278
- <div class="flex justify-between text-[10px] text-gray-600">
279
- <span>Instant</span><span id="holdVal">100ms</span><span>Stare</span>
 
 
280
  </div>
281
  </div>
282
 
283
  <div>
284
- <label class="text-[10px] uppercase text-gray-500 font-bold">Loops</label>
285
- <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">
 
 
286
  </div>
287
 
288
  <div class="border-t border-cyber-border my-2"></div>
289
 
290
  <!-- Filters -->
291
  <div>
292
- <label class="text-[10px] uppercase text-gray-500 font-bold">Contrast</label>
293
- <input type="range" id="contrast" min="0" max="300" value="100" class="w-full mt-1">
294
- </div>
295
-
296
- <div>
297
- <label class="text-[10px] uppercase text-gray-500 font-bold">Brightness</label>
298
- <input type="range" id="brightness" min="0" max="200" value="100" class="w-full mt-1">
 
 
 
 
 
 
 
299
  </div>
300
 
301
  <div>
302
- <label class="text-[10px] uppercase text-gray-500 font-bold block mb-2">Spot Color</label>
 
 
303
  <div class="flex gap-2 flex-wrap" id="spotColorPreview">
304
  <!-- Swatches injected via JS -->
305
  </div>
306
  </div>
307
 
308
- <div class="space-y-2 pt-2">
309
- <label class="flex items-center gap-2 cursor-pointer">
310
- <input type="checkbox" id="grayscaleToggle" class="accent-cyber-accent">
311
- <span class="text-xs">Grayscale</span>
312
- </label>
313
- <label class="flex items-center gap-2 cursor-pointer">
314
- <input type="checkbox" id="glitchToggle" class="accent-cyber-alert">
315
- <span class="text-xs">Glitch Noise</span>
316
- </label>
317
- <label class="flex items-center gap-2 cursor-pointer">
318
- <input type="checkbox" id="rotateToggle" class="accent-cyber-secondary">
319
- <span class="text-xs">Film Jitter</span>
320
- </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  </div>
322
 
323
  <div class="border-t border-cyber-border my-4"></div>
324
 
325
  <!-- Status & Download -->
326
- <div class="bg-black p-2 rounded border border-cyber-border">
327
- <div class="flex justify-between text-[10px] mb-1">
328
- <span id="renderStatus">Ready</span>
329
- <span id="frameCount">0/0</span>
330
  </div>
331
- <div class="w-full bg-gray-800 h-1 rounded overflow-hidden mb-2">
332
- <div id="renderProgress" class="h-full bg-cyber-alert w-0"></div>
333
  </div>
 
 
 
 
334
  </div>
335
-
336
- <a id="downloadLink" href="#"
337
- class="hidden block text-center text-xs text-cyber-accent underline py-2">Download GIF</a>
338
  </div>
339
  </div>
340
 
@@ -348,556 +445,323 @@
348
  <!-- MODULE LOGIC -->
349
  <script type="module">
350
  import * as THREE from 'three';
351
- import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
352
- import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
353
- import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
354
-
355
- // --- GLOBAL STATE ---
356
- const State = {
357
- mode: 'VIDEO', // 'VIDEO' or 'IMAGE'
358
- };
359
-
360
- // --- 1. SHADER LIBRARY (GLSL SNIPPETS) ---
361
- const SHADER_LIB = [
362
- { 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); }` },
363
- { 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); }` },
364
- { 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); }` },
365
- { 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); }` },
366
- { 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); }` },
367
- { 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); }` },
368
- { 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); }` },
369
- { 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; }` },
370
- { 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); }` },
371
- { 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); }` },
372
- { 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); }` }
373
- ];
374
-
375
- // --- 2. VIDEO ENGINE (Three.js) ---
376
- const VideoEngine = {
377
- scene: null,
378
- camera: null,
379
- renderer: null,
380
- composer: null,
381
- video: null,
382
- videoTex: null,
383
- audioCtx: null,
384
- analyser: null,
385
- dataArray: null,
386
- stack: [],
387
- maxEffects: 8,
388
- isPlaying: false,
389
-
390
- init: () => {
391
- const container = document.getElementById('canvas-wrapper');
392
- const width = container.clientWidth;
393
- const height = container.clientHeight;
394
-
395
- VideoEngine.scene = new THREE.Scene();
396
- VideoEngine.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
397
-
398
- VideoEngine.renderer = new THREE.WebGLRenderer({ antialias: false, preserveDrawingBuffer: true });
399
- VideoEngine.renderer.setSize(width, height);
400
- container.appendChild(VideoEngine.renderer.domElement);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
- VideoEngine.video = document.getElementById('videoSrc');
403
- VideoEngine.videoTex = new THREE.VideoTexture(VideoEngine.video);
404
- VideoEngine.videoTex.minFilter = THREE.LinearFilter;
405
- VideoEngine.videoTex.magFilter = THREE.LinearFilter;
406
 
407
- VideoEngine.composer = new EffectComposer(VideoEngine.renderer);
408
- const renderPass = new RenderPass(VideoEngine.scene, VideoEngine.camera);
409
-
410
- const geometry = new THREE.PlaneGeometry(2, 2);
411
- const material = new THREE.MeshBasicMaterial({ map: VideoEngine.videoTex });
412
- const quad = new THREE.Mesh(geometry, material);
413
- VideoEngine.scene.add(quad);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
- VideoEngine.composer.addPass(renderPass);
416
- VideoEngine.buildLibrary();
417
- VideoEngine.animate();
418
- },
419
-
420
- buildLibrary: () => {
421
- const list = document.getElementById('libraryList');
422
- list.innerHTML = '';
423
- const categories = [...new Set(SHADER_LIB.map(s => s.cat))];
 
 
 
 
 
 
 
 
 
 
424
 
425
- categories.forEach(cat => {
426
- const header = document.createElement('div');
427
- 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";
428
- header.innerText = cat;
429
- list.appendChild(header);
430
-
431
- SHADER_LIB.filter(s => s.cat === cat).forEach(shader => {
432
- const el = document.createElement('div');
433
- el.className = "shader-card p-2 text-xs text-gray-300 cursor-pointer flex justify-between items-center rounded";
434
- el.innerHTML = `<span>${shader.name}</span> <i data-lucide="plus" class="w-3 h-3 opacity-50"></i>`;
435
- el.onclick = () => VideoEngine.addEffect(shader.id);
436
- list.appendChild(el);
437
- });
438
  });
439
- lucide.createIcons();
440
- },
441
-
442
- addEffect: (id) => {
443
- if (VideoEngine.stack.length >= VideoEngine.maxEffects) return;
444
- const def = SHADER_LIB.find(s => s.id === id);
445
- if (!def) return;
446
-
447
- const myUniforms = {
448
- "tDiffuse": { value: null },
449
- "amount": { value: 0.5 },
450
- "time": { value: 0.0 },
451
- "resolution": { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
452
- "uAudio": { value: 0.0 }
453
- };
454
-
455
- const myShader = {
456
- uniforms: myUniforms,
457
- vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); }`,
458
- fragmentShader: def.glsl
459
- };
460
-
461
- const pass = new ShaderPass(myShader);
462
- pass.uid = Date.now() + Math.random();
463
- pass.def = def;
464
-
465
- VideoEngine.composer.addPass(pass);
466
- VideoEngine.stack.push(pass);
 
 
 
 
 
 
 
 
 
 
 
 
467
  VideoEngine.renderStackUI();
468
- },
469
-
470
- removeEffect: (uid) => {
471
- const idx = VideoEngine.stack.findIndex(p => p.uid === uid);
472
- if (idx > -1) {
473
- VideoEngine.composer.removePass(VideoEngine.stack[idx]);
474
- VideoEngine.stack.splice(idx, 1);
475
- VideoEngine.renderStackUI();
476
- }
477
- },
478
 
479
- renderStackUI: () => {
480
- const container = document.getElementById('stackList');
481
- document.getElementById('stackCount').innerText = `${VideoEngine.stack.length}/${VideoEngine.maxEffects}`;
482
-
483
- if (VideoEngine.stack.length === 0) {
484
- container.innerHTML = '<div class="text-center mt-10 text-gray-600 text-xs">Stack is empty.</div>';
485
- return;
486
- }
487
 
488
- container.innerHTML = '';
489
- VideoEngine.stack.forEach((pass, i) => {
490
- const el = document.createElement('div');
491
- el.className = "bg-black border border-cyber-border rounded p-2 mb-2";
492
- el.innerHTML = `
493
- <div class="flex justify-between items-center mb-1">
494
- <span class="font-bold text-cyber-secondary text-xs">${i+1}. ${pass.def.name}</span>
495
- <button class="text-red-500 hover:text-white" onclick="window.removeVideoFx(${pass.uid})"><i data-lucide="x" class="w-3 h-3"></i></button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  </div>
497
- <input type="range" class="w-full h-1 bg-gray-800 rounded appearance-none cursor-pointer"
 
 
 
 
498
  min="0" max="1" step="0.01" value="${pass.uniforms.amount.value}"
499
  oninput="window.updateVideoFx(${pass.uid}, this.value)">
500
- `;
501
- container.appendChild(el);
502
- });
503
- lucide.createIcons();
504
- },
505
-
506
- animate: () => {
507
- if (State.mode !== 'VIDEO') return;
508
- requestAnimationFrame(VideoEngine.animate);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
 
510
- let audioLevel = 0;
511
- if (VideoEngine.analyser) {
 
512
  VideoEngine.analyser.getByteFrequencyData(VideoEngine.dataArray);
 
513
  let sum = 0;
514
  for(let i=0; i<10; i++) sum += VideoEngine.dataArray[i];
515
  audioLevel = sum / 10 / 255;
516
- }
517
-
518
- const time = performance.now() * 0.001;
519
- VideoEngine.stack.forEach(pass => {
520
- if (pass.uniforms.time) pass.uniforms.time.value = time;
521
- if (pass.uniforms.uAudio) pass.uniforms.uAudio.value = audioLevel;
522
- });
523
-
524
- const audioEl = document.getElementById('audioSrc');
525
- if (Math.abs(VideoEngine.video.currentTime - audioEl.currentTime) > 0.5 && !VideoEngine.video.paused) {
526
- audioEl.currentTime = VideoEngine.video.currentTime;
527
- }
528
-
529
- const m = Math.floor(VideoEngine.video.currentTime / 60);
530
- const s = Math.floor(VideoEngine.video.currentTime % 60);
531
- document.getElementById('timeDisplay').innerText = `${m}:${s.toString().padStart(2, '0')}`;
532
-
533
- VideoEngine.composer.render();
534
  }
535
- };
536
-
537
- // --- 3. GIF ENGINE (Canvas 2D) ---
538
- const GifEngine = {
539
- canvas: null,
540
- ctx: null,
541
- images: [],
542
- currentIndex: 0,
543
- isRendering: false,
544
- animationId: null,
545
-
546
- filters: {
547
- contrast: 100,
548
- brightness: 100,
549
- grayscale: true,
550
- spotColor: { r: 255, g: 0, b: 85, active: true },
551
- glitch: false,
552
- rotation: false
553
- },
554
- animation: {
555
- speed: 300,
556
- hold: 100,
557
- loops: 5
558
- },
559
-
560
- init: () => {
561
- GifEngine.canvas = document.getElementById('artCanvas');
562
- GifEngine.ctx = GifEngine.canvas.getContext('2d');
563
- GifEngine.initSpotColors();
564
-
565
- // Load default images
566
- document.getElementById('loadImagesBtn').click();
567
- },
568
-
569
- initSpotColors: () => {
570
- const colors = [
571
- { c: '255,255,255', bg: '#ffffff', name: 'None' },
572
- { c: '255,0,85', bg: '#ff0055', name: 'Red' },
573
- { c: '0,204,255', bg: '#00ccff', name: 'Blue' },
574
- { c: '255,204,0', bg: '#ffcc00', name: 'Gold' },
575
- { c: '51,255,51', bg: '#33ff33', name: 'Green' },
576
- { c: '170,0,255', bg: '#aa00ff', name: 'Purple' }
577
- ];
578
-
579
- const container = document.getElementById('spotColorPreview');
580
- container.innerHTML = '';
581
- colors.forEach((col, idx) => {
582
- const div = document.createElement('div');
583
- div.className = `w-6 h-6 rounded-full cursor-pointer border border-gray-600 hover:scale-110 transition ${idx===1?'ring-2 ring-cyber-alert':''}`;
584
- div.style.backgroundColor = col.bg;
585
- div.onclick = () => {
586
- GifEngine.filters.spotColor = {
587
- r: parseInt(col.c.split(',')[0]),
588
- g: parseInt(col.c.split(',')[1]),
589
- b: parseInt(col.c.split(',')[2]),
590
- active: true
591
- };
592
- // Visual update
593
- Array.from(container.children).forEach(c => c.classList.remove('ring-2', 'ring-cyber-alert'));
594
- div.classList.add('ring-2', 'ring-cyber-alert');
595
- if (!GifEngine.isRendering) GifEngine.drawFrame();
596
- };
597
- container.appendChild(div);
598
- });
599
- },
600
-
601
- getLuminance: (img) => {
602
- const tempC = document.createElement('canvas');
603
- tempC.width = 50; tempC.height = 50;
604
- const tCtx = tempC.getContext('2d');
605
- tCtx.drawImage(img, 0, 0, 50, 50);
606
- const data = tCtx.getImageData(0,0,50,50).data;
607
- let sum = 0;
608
- for(let i=0; i<data.length; i+=4) sum += (0.2126*data[i] + 0.7152*data[i+1] + 0.0722*data[i+2]);
609
- return sum / 2500;
610
- },
611
-
612
- sortImages: () => {
613
- GifEngine.images.sort((a, b) => GifEngine.getLuminance(a) - GifEngine.getLuminance(b));
614
- },
615
-
616
- drawFrame: () => {
617
- if (GifEngine.images.length === 0) return;
618
- const ctx = GifEngine.ctx;
619
- const canvas = GifEngine.canvas;
620
-
621
- ctx.fillStyle = '#000';
622
- ctx.fillRect(0, 0, canvas.width, canvas.height);
623
-
624
- const img = GifEngine.images[GifEngine.currentIndex];
625
-
626
- ctx.save();
627
-
628
- // CSS Filters string
629
- let f = `contrast(${GifEngine.filters.contrast}%) brightness(${GifEngine.filters.brightness}%)`;
630
- if (GifEngine.filters.grayscale) f += ' grayscale(100%)';
631
- ctx.filter = f;
632
-
633
- // Transforms
634
- if (GifEngine.filters.rotation) {
635
- const ang = (Math.random() - 0.5) * 0.05;
636
- ctx.translate(canvas.width/2, canvas.height/2);
637
- ctx.rotate(ang);
638
- ctx.translate(-canvas.width/2, -canvas.height/2);
639
- }
640
 
641
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
 
 
 
 
642
 
643
- // Spot Color
644
- if (GifEngine.filters.spotColor.active) {
645
- ctx.globalCompositeOperation = 'overlay';
646
- ctx.fillStyle = `rgba(${GifEngine.filters.spotColor.r},${GifEngine.filters.spotColor.g},${GifEngine.filters.spotColor.b}, 0.4)`;
647
- ctx.fillRect(0, 0, canvas.width, canvas.height);
648
- }
649
-
650
- // Glitch
651
- if (GifEngine.filters.glitch) {
652
- const h = Math.random() * 30;
653
- const y = Math.random() * canvas.height;
654
- const off = (Math.random() - 0.5) * 60;
655
- try {
656
- const d = ctx.getImageData(0, y, canvas.width, h);
657
- ctx.putImageData(d, off, y);
658
- } catch(e){}
659
  }
660
-
661
- ctx.restore();
662
- },
663
-
664
- loop: () => {
665
- if (State.mode !== 'IMAGE' || GifEngine.isRendering) return;
666
-
667
- GifEngine.drawFrame();
668
-
669
- setTimeout(() => {
670
- GifEngine.currentIndex = (GifEngine.currentIndex + 1) % GifEngine.images.length;
671
- GifEngine.animationId = requestAnimationFrame(GifEngine.loop);
672
- }, GifEngine.animation.speed + GifEngine.animation.hold);
673
- },
674
-
675
- loadImages: () => {
676
- const urls = document.getElementById('imageSource').value.split(',').map(s=>s.trim()).filter(s=>s);
677
- const list = document.getElementById('mediaList');
678
- list.innerHTML = '<div class="col-span-3 text-center text-xs animate-pulse">Analyzing...</div>';
679
-
680
- let loaded = 0;
681
- GifEngine.images = [];
682
-
683
- urls.forEach(url => {
684
- const img = new Image();
685
- img.crossOrigin = "Anonymous";
686
- img.onload = () => {
687
- GifEngine.images.push(img);
688
- loaded++;
689
- if (loaded === urls.length) {
690
- GifEngine.sortImages();
691
- list.innerHTML = '';
692
- GifEngine.images.forEach((im, idx) => {
693
- const div = document.createElement('div');
694
- div.className = "relative rounded overflow-hidden group";
695
- div.innerHTML = `<img src="${im.src}" class="w-full h-auto opacity-70 group-hover:opacity-100 transition">`;
696
- list.appendChild(div);
697
- });
698
- GifEngine.drawFrame();
699
- lucide.createIcons();
700
- }
701
- };
702
- img.onerror = () => {};
703
- img.src = url;
704
- });
705
  }
706
- };
707
-
708
- // --- 4. GIF RENDERING ENGINE (gif.js + Canvas) ---
709
- const RenderEngine = {
710
- gif: null,
711
- canvas: null,
712
- ctx: null,
713
- frames: [],
714
- progress: 0,
715
-
716
- init: () => {
717
- RenderEngine.canvas = document.getElementById('artCanvas');
718
- RenderEngine.ctx = RenderEngine.canvas.getContext('2d');
719
- },
720
-
721
- startRender: () => {
722
- if (GifEngine.images.length === 0) {
723
- alert("Please load images first.");
724
- return;
725
- }
726
-
727
- RenderEngine.progress = 0;
728
- document.getElementById('renderStatus').innerText = "Rendering Frames...";
729
- document.getElementById('renderStatus').className = "text-cyber-secondary";
730
- document.getElementById('renderProgress').style.width = "0%";
731
- document.getElementById('downloadLink').classList.add('hidden');
732
-
733
- const totalFrames = GifEngine.images.length * parseInt(document.getElementById('loopCount').value);
734
- document.getElementById('frameCount').innerText = `0/${totalFrames}`;
735
-
736
- RenderEngine.gif = new GIF({
737
- workers: 2,
738
- quality: 10,
739
- width: RenderEngine.canvas.width,
740
- height: RenderEngine.canvas.height,
741
- workerScript: 'https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.js'
742
- });
743
-
744
- RenderEngine.gif.on('finished', function(blob) {
745
- const url = URL.createObjectURL(blob);
746
- const link = document.getElementById('downloadLink');
747
- link.href = url;
748
- link.download = 'hyper-fx-masterpiece.gif';
749
- link.classList.remove('hidden');
750
- document.getElementById('renderStatus').innerText = "Render Complete!";
751
- document.getElementById('renderStatus').className = "text-cyber-accent";
752
- document.getElementById('renderProgress').style.width = "100%";
753
- });
754
-
755
- RenderEngine.renderLoop(0, totalFrames);
756
- },
757
-
758
- renderLoop: (currentFrame, totalFrames) => {
759
- if (currentFrame >= totalFrames) {
760
- RenderEngine.gif.render();
761
- return;
762
- }
763
 
764
- // Set GIF Engine state for this frame
765
- GifEngine.currentIndex = currentFrame % GifEngine.images.length;
766
- GifEngine.drawFrame();
767
-
768
- RenderEngine.gif.addFrame(RenderEngine.ctx, {delay: parseInt(document.getElementById('transitionSpeed').value)});
769
-
770
- RenderEngine.progress = ((currentFrame + 1) / totalFrames) * 100;
771
- document.getElementById('renderProgress').style.width = `${RenderEngine.progress}%`;
772
- document.getElementById('frameCount').innerText = `${currentFrame + 1}/${totalFrames}`;
773
- document.getElementById('renderStatus').innerText = `Rendering Frame ${currentFrame + 1}...`;
774
-
775
- requestAnimationFrame(() => {
776
- // Small timeout to prevent UI freeze
777
- setTimeout(() => RenderEngine.renderLoop(currentFrame + 1, totalFrames), 0);
778
- });
779
  }
780
- };
781
-
782
- // --- 5. UI CONTROLLERS ---
783
- const UI = {
784
- init: () => {
785
- // Mode Switching
786
- document.getElementById('modeVideo').addEventListener('click', () => UI.setMode('VIDEO'));
787
- document.getElementById('modeImage').addEventListener('click', () => UI.setMode('IMAGE'));
788
-
789
- // Tabs
790
- document.getElementById('tabShaders').addEventListener('click', () => UI.setTab('SHADERS'));
791
- document.getElementById('tabImages').addEventListener('click', () => UI.setTab('IMAGES'));
792
-
793
- // Video Controls
794
- document.getElementById('btnPlay').addEventListener('click', () => {
795
- const v = document.getElementById('videoSrc');
796
- if (v.paused) v.play();
797
- else v.pause();
798
- });
799
- document.getElementById('videoSrc').addEventListener('timeupdate', () => {
800
- if (State.mode === 'VIDEO') {
801
- const m = Math.floor(document.getElementById('videoSrc').currentTime / 60);
802
- const s = Math.floor(document.getElementById('videoSrc').currentTime % 60);
803
- document.getElementById('timeDisplay').innerText = `${m}:${s.toString().padStart(2, '0')}`;
804
- }
805
- });
806
 
807
- // File Inputs
808
- document.getElementById('inpVideo').addEventListener('change', (e) => {
809
- const file = e.target.files[0];
810
- if (file) {
811
- const url = URL.createObjectURL(file);
812
- document.getElementById('videoSrc').src = url;
813
- document.getElementById('videoSrc').load();
814
- if (State.mode === 'VIDEO') document.getElementById('videoSrc').play();
815
- }
816
- });
817
-
818
- document.getElementById('inpAudio').addEventListener('change', (e) => {
819
- const file = e.target.files[0];
820
- if (file) {
821
- const url = URL.createObjectURL(file);
822
- document.getElementById('audioSrc').src = url;
823
- document.getElementById('audioSrc').load();
824
- // Sync audio to video
825
- document.getElementById('audioSrc').play();
826
- document.getElementById('videoSrc').play();
827
- }
828
- });
829
-
830
- // GIF Settings Listeners
831
- document.getElementById('transitionSpeed').addEventListener('input', (e) => {
832
- document.getElementById('speedVal').innerText = `${e.target.value}ms`;
833
- GifEngine.animation.speed = parseInt(e.target.value);
834
- });
835
- document.getElementById('holdDuration').addEventListener('input', (e) => {
836
- document.getElementById('holdVal').innerText = `${e.target.value}ms`;
837
- GifEngine.animation.hold = parseInt(e.target.value);
838
- });
839
- document.getElementById('loopCount').addEventListener('change', (e) => {
840
- GifEngine.animation.loops = parseInt(e.target.value);
841
- });
842
-
843
- document.getElementById('contrast').addEventListener('input', (e) => GifEngine.filters.contrast = parseInt(e.target.value));
844
- document.getElementById('brightness').addEventListener('input', (e) => GifEngine.filters.brightness = parseInt(e.target.value));
845
- document.getElementById('grayscaleToggle').addEventListener('change', (e) => GifEngine.filters.grayscale = e.target.checked);
846
- document.getElementById('glitchToggle').addEventListener('change', (e) => GifEngine.filters.glitch = e.target.checked);
847
- document.getElementById('rotateToggle').addEventListener('change', (e) => GifEngine.filters.rotation = e.target.checked);
848
-
849
- // Actions
850
- document.getElementById('loadImagesBtn').addEventListener('click', GifEngine.loadImages);
851
- document.getElementById('btnRenderGif').addEventListener('click', RenderEngine.startRender);
852
- document.getElementById('btnRandom').addEventListener('click', VideoEngine.randomizeStack);
853
-
854
- // Global Window Functions for HTML Event Handlers
855
- window.removeVideoFx = (uid) => VideoEngine.removeEffect(uid);
856
- window.updateVideoFx = (uid, val) => {
857
- const pass = VideoEngine.stack.find(p => p.uid === uid);
858
- if (pass) {
859
- pass.uniforms.amount.value = parseFloat(val);
860
- }
861
- };
862
-
863
- // Resize Handler
864
- window.addEventListener('resize', () => {
865
- if (VideoEngine.renderer) {
866
- const w = document.getElementById('canvas-wrapper').clientWidth;
867
- const h = document.getElementById('canvas-wrapper').clientHeight;
868
- VideoEngine.renderer.setSize(w, h);
869
- VideoEngine.stack.forEach(pass => {
870
- if (pass.uniforms.resolution) pass.uniforms.resolution.value.set(w, h);
871
- });
872
- }
873
- });
874
-
875
- // Init Engines
876
- VideoEngine.init();
877
- GifEngine.init();
878
- RenderEngine.init();
879
-
880
- // Hide Loader
881
- setTimeout(() => {
882
- document.getElementById('loader').style.display = 'none';
883
- }, 1500);
884
- },
885
-
886
- setMode: (mode) => {
887
- State.mode = mode;
888
-
889
- // UI Updates
890
- if (mode === 'VIDEO') {
891
- document.getElementById('modeVideo').classList.add('bg-cyber-accent', 'text-black');
892
- document.getElementById('modeVideo').classList.remove('text-gray-400');
893
- document.getElementById('modeImage').classList.remove('bg-cyber-accent', 'text-black');
894
- document.getElementById('modeImage').classList.add('text-gray-400');
895
-
896
- document.getElementById('panelShaders').classList.remove('hidden');
897
- document.getElementById('panelImages').classList.add('hidden');
898
-
899
- document.getElementById('rightStack').classList.remove('hidden');
900
- document.getElementById('rightSettings').classList.add('hidden');
901
-
902
- document.getElementById('video-container').classList.remove('hidden');
903
- document.getElementById('image-container').classList.add('hidden');
 
118
  background: #333;
119
  border-radius: 2px;
120
  }
121
+
122
+ /* Custom Progress Bar for Render */
123
+ .progress-container {
124
+ width: 100%;
125
+ background-color: #1a1a1a;
126
+ border-radius: 0.25rem;
127
+ overflow: hidden;
128
+ }
129
+ .progress-bar {
130
+ height: 100%;
131
+ width: 0%;
132
+ background-color: #ff0055;
133
+ transition: width 0.2s ease;
134
+ }
135
  </style>
136
  </head>
137
 
 
140
  <!-- Loading Screen -->
141
  <div id="loader">
142
  <div class="text-center">
143
+ <div class="text-2xl font-bold mb-2 text-cyber-accent">INITIALIZING HYPER CORE</div>
144
  <div class="text-xs text-gray-500">Loading WebGL & Canvas Modules...</div>
145
+ <div class="mt-4 w-64 h-1 bg-gray-800 rounded overflow-hidden mx-auto">
146
+ <div id="loader-bar" class="h-full bg-cyber-accent animate-pulse" style="width: 0%"></div>
147
+ </div>
148
  </div>
149
  </div>
150
 
151
  <!-- Header -->
152
+ <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">
153
  <div class="flex items-center gap-3">
154
+ <i data-lucide="cpu" class="text-cyber-accent w-6 h-6"></i>
155
  <div>
156
  <h1 class="font-bold text-sm tracking-widest uppercase">Hyper-FX
157
  <span class="text-cyber-secondary">Studio</span>
 
161
 
162
  <div class="flex items-center gap-4">
163
  <!-- Mode Switcher -->
164
+ <div class="flex bg-black rounded p-1 border border-cyber-border shadow-inner">
165
+ <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>
166
  <button id="modeImage" class="px-3 py-1 text-xs rounded text-gray-400 hover:text-white transition">GIF MODE</button>
167
  </div>
168
 
 
180
  <div class="flex flex-1 overflow-hidden">
181
 
182
  <!-- LEFT: Resource Library -->
183
+ <aside class="w-72 bg-cyber-panel border-r border-cyber-border flex flex-col z-10 shrink-0 shadow-xl">
184
  <!-- Tabs -->
185
  <div class="flex border-b border-cyber-border">
186
+ <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>
187
+ <button id="tabImages" class="flex-1 py-2 text-xs font-bold text-gray-500 hover:text-white transition-colors">IMAGES</button>
188
  </div>
189
 
190
  <!-- Content: Shader Library (Video Mode) -->
191
  <div id="panelShaders" class="flex-1 overflow-y-auto flex flex-col">
192
+ <div class="p-3 border-b border-cyber-border bg-black/20">
193
+ <div class="flex items-center gap-2 mb-2">
194
+ <i data-lucide="search" class="text-gray-500 w-3 h-3"></i>
195
+ <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">
196
+ </div>
197
+ <div class="flex justify-between items-center text-[10px] text-gray-500 uppercase font-bold px-1">
198
+ <span>Library</span>
199
+ <span id="shaderCount">11 Effects</span>
200
+ </div>
201
  </div>
202
+ <div id="libraryList" class="flex-1 overflow-y-auto p-2 space-y-1 custom-scrollbar">
203
  <!-- Injected via JS -->
204
  </div>
205
 
206
  <!-- Media Input (Video) -->
207
  <div class="p-3 border-t border-cyber-border bg-black/40">
208
  <div class="grid grid-cols-2 gap-2">
209
+ <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">
210
+ <i data-lucide="video" class="w-3 h-3"></i> LOAD VIDEO
211
  <input type="file" id="inpVideo" accept="video/*" class="hidden">
212
  </label>
213
+ <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">
214
+ <i data-lucide="music" class="w-3 h-3"></i> LOAD AUDIO
215
  <input type="file" id="inpAudio" accept="audio/*" class="hidden">
216
  </label>
217
  </div>
218
+ <div class="mt-3 text-[10px] text-gray-600 text-center font-mono">
219
+ Video State: <span id="videoStatus" class="text-gray-400">Idle</span>
220
+ </div>
221
  </div>
222
  </div>
223
 
224
  <!-- Content: Image Sequence (GIF Mode) -->
225
  <div id="panelImages" class="flex-1 overflow-y-auto flex flex-col hidden">
226
  <div class="p-3 border-b border-cyber-border">
227
+ <label class="text-[10px] text-gray-500 uppercase font-bold mb-1 block flex items-center gap-2">
228
+ <i data-lucide="layers" class="w-3 h-3"></i> Source URLs (Comma separated)
229
+ </label>
230
+ <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>
231
+ <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">
232
+ <i data-lucide="upload" class="w-3 h-3"></i> LOAD & ANALYZE
233
+ </button>
234
  </div>
235
  <div id="mediaList" class="flex-1 overflow-y-auto p-2 grid grid-cols-3 gap-2 content-start">
236
+ <div class="col-span-3 text-center text-xs text-gray-600 mt-4 flex flex-col items-center">
237
+ <i data-lucide="image-off" class="w-8 h-8 mb-2 opacity-50"></i>
238
+ <p>No images loaded.</p>
239
+ <p class="text-[10px]">Use URLs above to generate a sequence.</p>
240
+ </div>
241
  </div>
242
  </div>
243
  </aside>
244
 
245
  <!-- CENTER: Viewport -->
246
  <main class="flex-1 bg-black relative flex items-center justify-center overflow-hidden">
247
+
248
  <!-- Video Container (Three.js) -->
249
+ <div id="video-container" class="absolute inset-0 w-full h-full z-10 bg-black">
250
+ <div id="canvas-wrapper" class="w-full h-full relative"></div>
251
+
252
  <!-- Video Overlay Controls -->
253
+ <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">
254
+ <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">
255
+ <i data-lucide="play" class="w-5 h-5 ml-0.5"></i>
256
+ </button>
257
+ <div class="flex flex-col">
258
+ <div class="text-xs font-mono text-cyber-accent flex items-center gap-2">
259
+ <i data-lucide="clock" class="w-3 h-3"></i>
260
+ <span id="timeDisplay">00:00</span>
261
+ </div>
262
+ <div class="w-32 h-1 bg-gray-800 rounded-full mt-1 overflow-hidden">
263
+ <div id="videoProgress" class="h-full bg-cyber-accent w-0 transition-all duration-100"></div>
264
+ </div>
265
+ </div>
266
+ <div class="flex flex-col items-end">
267
+ <span class="text-[10px] text-gray-500 uppercase">Active Effects</span>
268
+ <span id="activeEffectCount" class="text-cyber-secondary font-bold text-lg">0</span>
269
+ </div>
270
+ </div>
271
+
272
+ <!-- Overlay Status Message -->
273
+ <div id="viewportMessage" class="absolute inset-0 flex items-center justify-center pointer-events-none opacity-0 transition-opacity duration-500">
274
+ <div class="bg-black/80 px-6 py-3 rounded border border-cyber-accent text-cyber-accent font-mono text-sm backdrop-blur-sm">
275
+ Waiting for video source...
276
+ </div>
277
  </div>
278
  </div>
279
 
280
  <!-- Image Container (2D Canvas) -->
281
  <div id="image-container" class="absolute inset-0 flex items-center justify-center bg-gray-900 z-0 hidden">
282
+ <div class="relative shadow-2xl border border-cyber-border rounded overflow-hidden max-w-full max-h-full">
283
+ <canvas id="artCanvas" width="600" height="600" class="bg-black block max-h-[80vh] max-w-[90vw]"></canvas>
284
+ <div class="absolute bottom-2 right-2 bg-black/60 text-[10px] text-white px-2 py-1 rounded font-mono pointer-events-none">
285
+ Canvas Active
286
+ </div>
287
+ </div>
288
  </div>
289
 
290
  </main>
291
 
292
  <!-- RIGHT: Stack & Settings -->
293
+ <aside class="w-80 bg-cyber-panel border-l border-cyber-border flex flex-col z-10 shrink-0 shadow-2xl">
294
+
295
  <!-- Video Mode Stack -->
296
  <div id="rightStack" class="flex flex-col h-full">
297
+ <div class="p-3 border-b border-cyber-border flex justify-between items-center bg-black/20">
298
+ <span class="text-xs font-bold text-gray-400 uppercase tracking-wider">ACTIVE SHADER STACK</span>
299
+ <span id="stackCount" class="text-[10px] bg-cyber-border px-2 py-0.5 rounded text-white">0/8</span>
300
  </div>
301
+ <div id="stackList" class="flex-1 overflow-y-auto p-2 space-y-2 custom-scrollbar bg-black/10">
302
+ <div class="flex flex-col items-center justify-center h-full text-center text-gray-600 text-xs pb-10">
303
+ <i data-lucide="layers" class="w-10 h-10 mb-3 opacity-20"></i>
304
+ <p>Stack is empty.</p>
305
+ <p class="text-[10px]">Add effects from the left panel.</p>
306
+ </div>
307
  </div>
308
 
309
  <!-- Snippet Info -->
310
  <div class="p-3 border-t border-cyber-border bg-black/40 text-[10px] text-gray-500 font-mono">
311
  <div class="flex justify-between mb-1">
312
+ <span class="flex items-center gap-1"><i data-lucide="cpu" class="w-3 h-3"></i> WebGL Renderer</span>
313
  <span class="text-cyber-accent">Active</span>
314
  </div>
315
  <div class="flex justify-between">
316
+ <span class="flex items-center gap-1"><i data-lucide="activity" class="w-3 h-3"></i> Audio Sync</span>
317
  <span class="text-cyber-secondary" id="audioStatus">Waiting...</span>
318
  </div>
319
+ <div class="flex justify-between mt-1">
320
+ <span class="flex items-center gap-1"><i data-lucide="zap" class="w-3 h-3"></i> FPS</span>
321
+ <span id="fpsDisplay" class="text-cyber-accent">60</span>
322
+ </div>
323
  </div>
324
  </div>
325
 
326
  <!-- GIF Mode Settings -->
327
+ <div id="rightSettings" class="flex flex-col h-full hidden overflow-y-auto bg-black/20">
328
+ <div class="p-3 border-b border-cyber-border flex justify-between items-center bg-black/40">
329
+ <span class="text-xs font-bold text-cyber-alert uppercase tracking-wider">GIF CONFIGURATION</span>
330
+ <i data-lucide="settings" class="w-4 h-4 text-cyber-alert"></i>
331
  </div>
332
 
333
+ <div class="p-4 space-y-6">
334
  <!-- Animation -->
335
  <div>
336
+ <label class="text-[10px] uppercase text-gray-500 font-bold flex items-center gap-2 mb-2">
337
+ <i data-lucide="zap" class="w-3 h-3"></i> Transition Speed
338
+ </label>
339
+ <input type="range" id="transitionSpeed" min="50" max="2000" value="300" class="w-full mt-1 accent-cyber-accent">
340
+ <div class="flex justify-between text-[10px] text-gray-600 mt-1">
341
+ <span class="font-mono">Fast</span><span id="speedVal" class="font-mono text-cyber-accent">300ms</span><span class="font-mono">Slow</span>
342
  </div>
343
  </div>
344
 
345
  <div>
346
+ <label class="text-[10px] uppercase text-gray-500 font-bold flex items-center gap-2 mb-2">
347
+ <i data-lucide="clock" class="w-3 h-3"></i> Hold Duration
348
+ </label>
349
+ <input type="range" id="holdDuration" min="0" max="2000" value="100" class="w-full mt-1 accent-cyber-accent">
350
+ <div class="flex justify-between text-[10px] text-gray-600 mt-1">
351
+ <span class="font-mono">Instant</span><span id="holdVal" class="font-mono text-cyber-accent">100ms</span><span class="font-mono">Stare</span>
352
  </div>
353
  </div>
354
 
355
  <div>
356
+ <label class="text-[10px] uppercase text-gray-500 font-bold flex items-center gap-2 mb-2">
357
+ <i data-lucide="repeat" class="w-3 h-3"></i> Loops
358
+ </label>
359
+ <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">
360
  </div>
361
 
362
  <div class="border-t border-cyber-border my-2"></div>
363
 
364
  <!-- Filters -->
365
  <div>
366
+ <label class="text-[10px] uppercase text-gray-500 font-bold flex items-center gap-2 mb-2">
367
+ <i data-lucide="sliders-vertical" class="w-3 h-3"></i> Image Processing
368
+ </label>
369
+
370
+ <div class="space-y-3">
371
+ <div>
372
+ <label class="text-[10px] text-gray-600">Contrast</label>
373
+ <input type="range" id="contrast" min="0" max="300" value="100" class="w-full mt-1 accent-cyber-secondary">
374
+ </div>
375
+ <div>
376
+ <label class="text-[10px] text-gray-600">Brightness</label>
377
+ <input type="range" id="brightness" min="0" max="200" value="100" class="w-full mt-1 accent-cyber-secondary">
378
+ </div>
379
+ </div>
380
  </div>
381
 
382
  <div>
383
+ <label class="text-[10px] uppercase text-gray-500 font-bold block mb-2 flex items-center gap-2">
384
+ <i data-lucide="palette" class="w-3 h-3"></i> Spot Color Overlay
385
+ </label>
386
  <div class="flex gap-2 flex-wrap" id="spotColorPreview">
387
  <!-- Swatches injected via JS -->
388
  </div>
389
  </div>
390
 
391
+ <div class="border-t border-cyber-border my-2"></div>
392
+
393
+ <div class="space-y-3">
394
+ <label class="flex items-center gap-3 cursor-pointer group">
395
+ <div class="relative">
396
+ <input type="checkbox" id="grayscaleToggle" class="sr-only peer">
397
+ <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>
398
+ </div>
399
+ <span class="text-xs text-gray-400 group-hover:text-white transition-colors">Grayscale</span>
400
+ </label>
401
+
402
+ <label class="flex items-center gap-3 cursor-pointer group">
403
+ <div class="relative">
404
+ <input type="checkbox" id="glitchToggle" class="sr-only peer">
405
+ <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>
406
+ </div>
407
+ <span class="text-xs text-gray-400 group-hover:text-white transition-colors">Glitch Noise</span>
408
+ </label>
409
+
410
+ <label class="flex items-center gap-3 cursor-pointer group">
411
+ <div class="relative">
412
+ <input type="checkbox" id="rotateToggle" class="sr-only peer">
413
+ <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>
414
+ </div>
415
+ <span class="text-xs text-gray-400 group-hover:text-white transition-colors">Film Jitter</span>
416
+ </label>
417
  </div>
418
 
419
  <div class="border-t border-cyber-border my-4"></div>
420
 
421
  <!-- Status & Download -->
422
+ <div class="bg-black p-3 rounded border border-cyber-border shadow-inner">
423
+ <div class="flex justify-between text-[10px] mb-2 font-mono">
424
+ <span id="renderStatus">Ready to Render</span>
425
+ <span id="frameCount" class="text-gray-500">0/0</span>
426
  </div>
427
+ <div class="w-full bg-gray-800 h-2 rounded overflow-hidden mb-2 border border-gray-700">
428
+ <div id="renderProgress" class="h-full bg-gradient-to-r from-cyber-alert to-cyber-secondary w-0 transition-all duration-300"></div>
429
  </div>
430
+ <a id="downloadLink" href="#"
431
+ 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">
432
+ Download GIF
433
+ </a>
434
  </div>
 
 
 
435
  </div>
436
  </div>
437
 
 
445
  <!-- MODULE LOGIC -->
446
  <script type="module">
447
  import * as THREE from 'three';
448
+ import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
449
+ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
450
+ import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
451
+
452
+ // --- 1. IMAGE PROCESSOR MODULE (Simulating Python Logic) ---
453
+ const ImageProcessor = {
454
+ source: null,
455
+ processed: false,
456
+
457
+ analyze: function(sourceUrl) {
458
+ console.log(`Analysing image source: ${sourceUrl}...`);
459
+ // Simulation of image analysis
460
+ return {
461
+ status: "success",
462
+ confidence: 0.98,
463
+ dimensions: "600x600",
464
+ format: "JPEG"
465
+ };
466
+ },
467
+
468
+ processImage: function(imgElement, filters) {
469
+ // This function applies the logic defined in the GIF Engine settings
470
+ // to a standard Image object context if needed for pre-processing.
471
+ return imgElement;
472
+ }
473
+ };
474
+
475
+ // --- GLOBAL STATE ---
476
+ const State = {
477
+ mode: 'VIDEO', // 'VIDEO' or 'IMAGE'
478
+ fps: 0,
479
+ lastTime: 0
480
+ };
481
+
482
+ // --- 2. SHADER LIBRARY (GLSL SNIPPETS) ---
483
+ const SHADER_LIB = [
484
+ { 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); }` },
485
+ { 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); }` },
486
+ { 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); }` },
487
+ { 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); }` },
488
+ { 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); }` },
489
+ { 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); }` },
490
+ { 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); }` },
491
+ { 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; }` },
492
+ { 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); }` },
493
+ { 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); }` },
494
+ { 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); }` }
495
+ ];
496
+
497
+ // --- 3. VIDEO ENGINE (Three.js) ---
498
+ const VideoEngine = {
499
+ scene: null,
500
+ camera: null,
501
+ renderer: null,
502
+ composer: null,
503
+ video: null,
504
+ videoTex: null,
505
+ audioCtx: null,
506
+ analyser: null,
507
+ dataArray: null,
508
+ stack: [],
509
+ maxEffects: 8,
510
+ isPlaying: false,
511
+ lastFrameTime: 0,
512
+ frameCount: 0,
513
+
514
+ init: () => {
515
+ const container = document.getElementById('canvas-wrapper');
516
+ const width = container.clientWidth;
517
+ const height = container.clientHeight;
518
+
519
+ VideoEngine.scene = new THREE.Scene();
520
+ VideoEngine.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
521
+
522
+ VideoEngine.renderer = new THREE.WebGLRenderer({ antialias: false, preserveDrawingBuffer: true });
523
+ VideoEngine.renderer.setSize(width, height);
524
+ container.appendChild(VideoEngine.renderer.domElement);
525
 
526
+ VideoEngine.video = document.getElementById('videoSrc');
527
+ VideoEngine.videoTex = new THREE.VideoTexture(VideoEngine.video);
528
+ VideoEngine.videoTex.minFilter = THREE.LinearFilter;
529
+ VideoEngine.videoTex.magFilter = THREE.LinearFilter;
530
 
531
+ VideoEngine.composer = new EffectComposer(VideoEngine.renderer);
532
+ const renderPass = new RenderPass(VideoEngine.scene, VideoEngine.camera);
533
+
534
+ const geometry = new THREE.PlaneGeometry(2, 2);
535
+ const material = new THREE.MeshBasicMaterial({ map: VideoEngine.videoTex });
536
+ const quad = new THREE.Mesh(geometry, material);
537
+ VideoEngine.scene.add(quad);
538
+
539
+ VideoEngine.composer.addPass(renderPass);
540
+
541
+ // Setup Audio Context
542
+ VideoEngine.setupAudio();
543
+ VideoEngine.buildLibrary();
544
+ VideoEngine.animate();
545
+ },
546
+
547
+ setupAudio: () => {
548
+ try {
549
+ VideoEngine.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
550
+ VideoEngine.analyser = VideoEngine.audioCtx.createAnalyser();
551
+ VideoEngine.analyser.fftSize = 256;
552
+ VideoEngine.dataArray = new Uint8Array(VideoEngine.analyser.frequencyBinCount);
553
 
554
+ const audioEl = document.getElementById('audioSrc');
555
+ const source = VideoEngine.audioCtx.createMediaElementSource(audioEl);
556
+ source.connect(VideoEngine.analyser);
557
+ VideoEngine.analyser.connect(VideoEngine.audioCtx.destination);
558
+ } catch (e) {
559
+ console.warn("Audio initialization failed", e);
560
+ }
561
+ },
562
+
563
+ buildLibrary: () => {
564
+ const list = document.getElementById('libraryList');
565
+ list.innerHTML = '';
566
+ const categories = [...new Set(SHADER_LIB.map(s => s.cat))];
567
+
568
+ categories.forEach(cat => {
569
+ const header = document.createElement('div');
570
+ 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";
571
+ header.innerHTML = `<i data-lucide="layers" class="w-3 h-3"></i> ${cat}`;
572
+ list.appendChild(header);
573
 
574
+ SHADER_LIB.filter(s => s.cat === cat).forEach(shader => {
575
+ const el = document.createElement('div');
576
+ 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";
577
+ 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>`;
578
+ el.onclick = () => VideoEngine.addEffect(shader.id);
579
+ list.appendChild(el);
 
 
 
 
 
 
 
580
  });
581
+ });
582
+ lucide.createIcons();
583
+ },
584
+
585
+ addEffect: (id) => {
586
+ if (VideoEngine.stack.length >= VideoEngine.maxEffects) {
587
+ alert("Stack Full! Remove an effect to add more.");
588
+ return;
589
+ }
590
+ const def = SHADER_LIB.find(s => s.id === id);
591
+ if (!def) return;
592
+
593
+ const myUniforms = {
594
+ "tDiffuse": { value: null },
595
+ "amount": { value: 0.5 },
596
+ "time": { value: 0.0 },
597
+ "resolution": { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
598
+ "uAudio": { value: 0.0 }
599
+ };
600
+
601
+ const myShader = {
602
+ uniforms: myUniforms,
603
+ vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); }`,
604
+ fragmentShader: def.glsl
605
+ };
606
+
607
+ const pass = new ShaderPass(myShader);
608
+ pass.uid = Date.now() + Math.random();
609
+ pass.def = def;
610
+
611
+ VideoEngine.composer.addPass(pass);
612
+ VideoEngine.stack.push(pass);
613
+ VideoEngine.renderStackUI();
614
+ },
615
+
616
+ removeEffect: (uid) => {
617
+ const idx = VideoEngine.stack.findIndex(p => p.uid === uid);
618
+ if (idx > -1) {
619
+ VideoEngine.composer.removePass(VideoEngine.stack[idx]);
620
+ VideoEngine.stack.splice(idx, 1);
621
  VideoEngine.renderStackUI();
622
+ }
623
+ },
 
 
 
 
 
 
 
 
624
 
625
+ updateEffectAmount: (uid, val) => {
626
+ const pass = VideoEngine.stack.find(p => p.uid === uid);
627
+ if (pass) {
628
+ pass.uniforms.amount.value = parseFloat(val);
629
+ }
630
+ },
 
 
631
 
632
+ renderStackUI: () => {
633
+ const container = document.getElementById('stackList');
634
+ document.getElementById('stackCount').innerText = `${VideoEngine.stack.length}/${VideoEngine.maxEffects}`;
635
+ document.getElementById('activeEffectCount').innerText = VideoEngine.stack.length;
636
+
637
+ if (VideoEngine.stack.length === 0) {
638
+ container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-center text-gray-600 text-xs pb-10">
639
+ <i data-lucide="layers" class="w-10 h-10 mb-3 opacity-20"></i>
640
+ <p>Stack is empty.</p>
641
+ <p class="text-[10px]">Add effects from the left panel.</p>
642
+ </div>`;
643
+ return;
644
+ }
645
+
646
+ container.innerHTML = '';
647
+ VideoEngine.stack.forEach((pass, i) => {
648
+ const el = document.createElement('div');
649
+ el.className = "bg-black border border-cyber-border rounded p-3 mb-2 shadow-lg relative group";
650
+ el.innerHTML = `
651
+ <div class="flex justify-between items-center mb-2">
652
+ <div class="flex items-center gap-2">
653
+ <span class="font-bold text-cyber-secondary text-xs">${i+1}.</span>
654
+ <span class="text-xs font-medium text-gray-200">${pass.def.name}</span>
655
  </div>
656
+ <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>
657
+ </div>
658
+ <div class="flex items-center gap-2">
659
+ <i data-lucide="sliders-vertical" class="w-3 h-3 text-gray-600"></i>
660
+ <input type="range" class="w-full h-1 bg-gray-800 rounded appearance-none cursor-pointer accent-cyber-secondary"
661
  min="0" max="1" step="0.01" value="${pass.uniforms.amount.value}"
662
  oninput="window.updateVideoFx(${pass.uid}, this.value)">
663
+ </div>
664
+ <div class="flex justify-between text-[9px] text-gray-500 mt-1 font-mono">
665
+ <span>INTENSITY</span>
666
+ <span id="val-${pass.uid}">${(pass.uniforms.amount.value * 100).toFixed(0)}%</span>
667
+ </div>
668
+ `;
669
+ container.appendChild(el);
670
+ });
671
+ lucide.createIcons();
672
+ },
673
+
674
+ animate: () => {
675
+ if (State.mode !== 'VIDEO') return;
676
+ requestAnimationFrame(VideoEngine.animate);
677
+
678
+ // FPS Calculation
679
+ const now = performance.now();
680
+ const delta = now - State.lastTime;
681
+ if (delta >= 1000) {
682
+ State.lastTime = now;
683
+ State.fps = VideoEngine.frameCount;
684
+ VideoEngine.frameCount = 0;
685
+ document.getElementById('fpsDisplay').innerText = State.fps;
686
+ }
687
+ VideoEngine.frameCount++;
688
 
689
+ let audioLevel = 0;
690
+ if (VideoEngine.analyser) {
691
+ try {
692
  VideoEngine.analyser.getByteFrequencyData(VideoEngine.dataArray);
693
+ // Calculate average bass
694
  let sum = 0;
695
  for(let i=0; i<10; i++) sum += VideoEngine.dataArray[i];
696
  audioLevel = sum / 10 / 255;
697
+ } catch(e) { audioLevel = 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
 
700
+ const time = performance.now() * 0.001;
701
+ VideoEngine.stack.forEach(pass => {
702
+ if (pass.uniforms.time) pass.uniforms.time.value = time;
703
+ if (pass.uniforms.uAudio) pass.uniforms.uAudio.value = audioLevel;
704
+ });
705
 
706
+ // Sync Audio
707
+ const audioEl = document.getElementById('audioSrc');
708
+ if (audioEl && !VideoEngine.video.paused) {
709
+ if (Math.abs(VideoEngine.video.currentTime - audioEl.currentTime) > 0.5) {
710
+ audioEl.currentTime = VideoEngine.video.currentTime;
 
 
 
 
 
 
 
 
 
 
 
711
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
713
 
714
+ // Update Time Display
715
+ const m = Math.floor(VideoEngine.video.currentTime / 60);
716
+ const s = Math.floor(VideoEngine.video.currentTime % 60);
717
+ document.getElementById('timeDisplay').innerText = `${m}:${s.toString().padStart(2, '0')}`;
718
+
719
+ // Update Progress Bar
720
+ if (VideoEngine.video.duration > 0) {
721
+ const pct = (VideoEngine.video.currentTime / VideoEngine.video.duration) * 100;
722
+ document.getElementById('videoProgress').style.width = `${pct}%`;
 
 
 
 
 
 
723
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
724
 
725
+ VideoEngine.composer.render();
726
+ }
727
+ };
728
+
729
+ // --- 4. GIF ENGINE (Canvas 2D) ---
730
+ const GifEngine = {
731
+ canvas: null,
732
+ ctx: null,
733
+ images: [],
734
+ currentIndex: 0,
735
+ isRendering: false,
736
+ animationId: null,
737
+
738
+ filters: {
739
+ contrast: 100,
740
+ brightness: 100,
741
+ grayscale: false,
742
+ spotColor: { r: 255, g: 0, b: 85, active: true },
743
+ glitch: false,
744
+ rotation: false
745
+ },
746
+ animation: {
747
+ speed: 300,
748
+ hold: 100,
749
+ loops: 5
750
+ },
751
+
752
+ init: () => {
753
+ GifEngine.canvas = document.getElementById('artCanvas');
754
+ GifEngine.ctx = GifEngine.canvas.getContext('2d');
755
+ GifEngine.initSpotColors();
756
+
757
+ // Load default images
758
+ document.getElementById('loadImagesBtn').click();
759
+ },
760
+
761
+ initSpotColors: () => {
762
+ const colors = [
763
+ { c: '255,255,255', bg: '#ffffff', name: 'None' },
764
+ { c: '255,0,85', bg: '#ff0055', name: 'Red' },
765
+ { c: '0,204,255', bg: '#00ccff', name: 'Blue' },
766
+ { c: '255,204,0', bg: '#ffcc00', name: 'Gold' },
767
+ { c: '51,255,