HI7RAI commited on
Commit
2b3d2bb
·
verified ·
1 Parent(s): f528c5a

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +768 -19
index.html CHANGED
@@ -1,19 +1,768 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Anycoder Raytrace Morph FX</title>
7
+
8
+ <!-- Tailwind CSS for modern UI -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+
11
+ <!-- Three.js for advanced effects -->
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
13
+
14
+ <!-- Google Fonts -->
15
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;500;700&display=swap" rel="stylesheet">
16
+
17
+ <style>
18
+ :root {
19
+ --neon-primary: #00f3ff;
20
+ --neon-secondary: #bc13fe;
21
+ --bg-dark: #050505;
22
+ --panel-bg: rgba(20, 20, 30, 0.85);
23
+ }
24
+
25
+ body {
26
+ background-color: var(--bg-dark);
27
+ color: #ffffff;
28
+ font-family: 'Rajdhani', sans-serif;
29
+ overflow: hidden; /* Prevent scrolling, app-like feel */
30
+ margin: 0;
31
+ height: 100vh;
32
+ width: 100vw;
33
+ }
34
+
35
+ /* Custom Scrollbar */
36
+ ::-webkit-scrollbar {
37
+ width: 6px;
38
+ }
39
+ ::-webkit-scrollbar-track {
40
+ background: #111;
41
+ }
42
+ ::-webkit-scrollbar-thumb {
43
+ background: var(--neon-primary);
44
+ border-radius: 3px;
45
+ }
46
+
47
+ /* Canvas Container */
48
+ #canvas-wrapper {
49
+ position: relative;
50
+ width: 100%;
51
+ height: 100%;
52
+ display: flex;
53
+ justify-content: center;
54
+ align-items: center;
55
+ background: radial-gradient(circle at center, #1a1a2e 0%, #000000 100%);
56
+ box-shadow: inset 0 0 100px rgba(0,0,0,0.9);
57
+ }
58
+
59
+ canvas {
60
+ max-width: 100%;
61
+ max-height: 100%;
62
+ object-fit: contain;
63
+ box-shadow: 0 0 30px rgba(0, 243, 255, 0.1);
64
+ }
65
+
66
+ /* UI Overlays */
67
+ .hud-panel {
68
+ background: var(--panel-bg);
69
+ backdrop-filter: blur(10px);
70
+ border: 1px solid rgba(255, 255, 255, 0.1);
71
+ border-left: 3px solid var(--neon-primary);
72
+ transition: all 0.3s ease;
73
+ }
74
+
75
+ .hud-panel:hover {
76
+ border-left: 3px solid var(--neon-secondary);
77
+ box-shadow: 0 0 15px rgba(188, 19, 254, 0.2);
78
+ }
79
+
80
+ /* Range Slider Styling */
81
+ input[type=range] {
82
+ -webkit-appearance: none;
83
+ width: 100%;
84
+ background: transparent;
85
+ }
86
+ input[type=range]::-webkit-slider-thumb {
87
+ -webkit-appearance: none;
88
+ height: 16px;
89
+ width: 16px;
90
+ border-radius: 50%;
91
+ background: var(--neon-primary);
92
+ cursor: pointer;
93
+ margin-top: -6px;
94
+ box-shadow: 0 0 10px var(--neon-primary);
95
+ }
96
+ input[type=range]::-webkit-slider-runnable-track {
97
+ width: 100%;
98
+ height: 4px;
99
+ cursor: pointer;
100
+ background: #333;
101
+ border-radius: 2px;
102
+ }
103
+
104
+ /* Animations */
105
+ @keyframes pulse-border {
106
+ 0% { border-color: rgba(0, 243, 255, 0.3); }
107
+ 50% { border-color: rgba(0, 243, 255, 1); }
108
+ 100% { border-color: rgba(0, 243, 255, 0.3); }
109
+ }
110
+
111
+ .active-mode {
112
+ animation: pulse-border 2s infinite;
113
+ background: rgba(0, 243, 255, 0.1);
114
+ }
115
+
116
+ .loading-overlay {
117
+ position: absolute;
118
+ top: 0; left: 0; right: 0; bottom: 0;
119
+ background: rgba(0,0,0,0.8);
120
+ display: flex;
121
+ justify-content: center;
122
+ align-items: center;
123
+ z-index: 50;
124
+ display: none;
125
+ }
126
+
127
+ /* Typography */
128
+ .font-tech {
129
+ font-family: 'Orbitron', sans-serif;
130
+ }
131
+
132
+ .brand-link {
133
+ color: var(--neon-primary);
134
+ text-decoration: none;
135
+ position: relative;
136
+ }
137
+ .brand-link::after {
138
+ content: '';
139
+ position: absolute;
140
+ width: 0;
141
+ height: 1px;
142
+ bottom: -2px;
143
+ left: 0;
144
+ background-color: var(--neon-primary);
145
+ transition: width 0.3s;
146
+ }
147
+ .brand-link:hover::after {
148
+ width: 100%;
149
+ }
150
+
151
+ /* CRT Scanline Effect Overlay */
152
+ .scanlines {
153
+ position: absolute;
154
+ top: 0; left: 0; width: 100%; height: 100%;
155
+ background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.03), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.03));
156
+ background-size: 100% 4px, 6px 100%;
157
+ pointer-events: none;
158
+ z-index: 10;
159
+ }
160
+ </style>
161
+ </head>
162
+ <body class="flex flex-col h-screen">
163
+
164
+ <!-- Header -->
165
+ <header class="h-14 flex items-center justify-between px-6 bg-black border-b border-gray-800 z-20">
166
+ <div class="flex items-center gap-3">
167
+ <div class="w-3 h-3 bg-cyan-400 rounded-full shadow-[0_0_10px_#00f3ff]"></div>
168
+ <h1 class="text-xl font-tech font-bold tracking-wider text-white">RAYTRACE <span class="text-cyan-400">MORPH</span></h1>
169
+ </div>
170
+ <div class="text-xs text-gray-400 font-mono">
171
+ Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="brand-link font-bold">anycoder</a>
172
+ </div>
173
+ </header>
174
+
175
+ <!-- Main Workspace -->
176
+ <main class="flex-1 flex overflow-hidden relative">
177
+
178
+ <!-- Sidebar Controls -->
179
+ <aside class="w-80 bg-gray-900 border-r border-gray-800 flex flex-col overflow-y-auto z-20 shadow-2xl">
180
+
181
+ <!-- Input Section -->
182
+ <div class="p-4 border-b border-gray-800">
183
+ <h2 class="text-sm font-tech text-cyan-400 mb-3 uppercase tracking-widest">Source Input</h2>
184
+
185
+ <!-- Image Input -->
186
+ <div class="mb-4">
187
+ <label class="block text-xs text-gray-500 mb-1">IMAGES (Multi)</label>
188
+ <input type="file" id="imageInput" multiple accept="image/*" class="block w-full text-xs text-gray-400 file:mr-2 file:py-2 file:px-2 file:rounded file:border-0 file:text-xs file:font-semibold file:bg-cyan-900 file:text-cyan-400 hover:file:bg-cyan-800 cursor-pointer"/>
189
+ </div>
190
+
191
+ <!-- Video Input -->
192
+ <div class="mb-4">
193
+ <label class="block text-xs text-gray-500 mb-1">VIDEO SOURCE</label>
194
+ <input type="file" id="videoInput" accept="video/*" class="block w-full text-xs text-gray-400 file:mr-2 file:py-2 file:px-2 file:rounded file:border-0 file:text-xs file:font-semibold file:bg-purple-900 file:text-purple-400 hover:file:bg-purple-800 cursor-pointer"/>
195
+ </div>
196
+
197
+ <!-- Audio Input -->
198
+ <div>
199
+ <label class="block text-xs text-gray-500 mb-1">AUDIO (BPM SYNC)</label>
200
+ <input type="file" id="audioInput" accept="audio/*" class="block w-full text-xs text-gray-400 file:mr-2 file:py-2 file:px-2 file:rounded file:border-0 file:text-xs file:font-semibold file:bg-green-900 file:text-green-400 hover:file:bg-green-800 cursor-pointer"/>
201
+ <div id="bpmDisplay" class="text-right text-xs font-mono text-green-400 mt-1">BPM: AUTO</div>
202
+ </div>
203
+ </div>
204
+
205
+ <!-- FX Controls -->
206
+ <div class="p-4 border-b border-gray-800 space-y-5">
207
+ <h2 class="text-sm font-tech text-cyan-400 mb-1 uppercase tracking-widest">FX Parameters</h2>
208
+
209
+ <!-- FPS Slider -->
210
+ <div>
211
+ <div class="flex justify-between mb-1">
212
+ <label class="text-xs text-gray-400">Target FPS</label>
213
+ <input type="number" id="fpsInput" value="60" class="w-12 bg-black border border-gray-700 text-right text-xs text-cyan-400 focus:outline-none focus:border-cyan-500 rounded px-1">
214
+ </div>
215
+ <input type="range" id="fpsSlider" min="1" max="144" value="60">
216
+ </div>
217
+
218
+ <!-- Morph Intensity -->
219
+ <div>
220
+ <div class="flex justify-between mb-1">
221
+ <label class="text-xs text-gray-400">Morph Intensity</label>
222
+ <span id="morphVal" class="text-xs font-mono text-purple-400">50%</span>
223
+ </div>
224
+ <input type="range" id="morphSlider" min="0" max="100" value="50">
225
+ </div>
226
+
227
+ <!-- Contrast Shaping -->
228
+ <div>
229
+ <div class="flex justify-between mb-1">
230
+ <label class="text-xs text-gray-400">Contrast / Shaping</label>
231
+ <span id="contrastVal" class="text-xs font-mono text-yellow-400">1.0</span>
232
+ </div>
233
+ <input type="range" id="contrastSlider" min="0" max="300" value="100">
234
+ </div>
235
+
236
+ <!-- Transition Speed -->
237
+ <div>
238
+ <div class="flex justify-between mb-1">
239
+ <label class="text-xs text-gray-400">Transition Speed (ms)</label>
240
+ <span id="speedVal" class="text-xs font-mono text-white">1000ms</span>
241
+ </div>
242
+ <input type="range" id="speedSlider" min="100" max="5000" step="100" value="1000">
243
+ </div>
244
+
245
+ <!-- Aspect Ratio Lock -->
246
+ <div class="flex items-center justify-between">
247
+ <label class="text-xs text-gray-400">Force 9:16 Crop</label>
248
+ <button id="cropToggle" class="w-10 h-5 bg-gray-700 rounded-full relative transition-colors duration-300">
249
+ <div class="w-3 h-3 bg-white rounded-full absolute top-1 left-1 transition-transform duration-300"></div>
250
+ </button>
251
+ </div>
252
+ </div>
253
+
254
+ <!-- Action Buttons -->
255
+ <div class="p-4 mt-auto">
256
+ <button id="playPauseBtn" class="w-full py-3 bg-cyan-600 hover:bg-cyan-500 text-white font-tech font-bold rounded shadow-[0_0_15px_rgba(8,145,178,0.5)] transition-all mb-2">
257
+ START RENDER
258
+ </button>
259
+ <button id="resetBtn" class="w-full py-2 border border-gray-600 hover:border-white text-gray-400 hover:text-white text-xs rounded transition-all">
260
+ RESET SYSTEM
261
+ </button>
262
+ </div>
263
+ </aside>
264
+
265
+ <!-- Canvas Area -->
266
+ <div id="canvas-wrapper" class="flex-1 relative">
267
+ <div class="scanlines"></div>
268
+
269
+ <!-- Main Canvas -->
270
+ <canvas id="mainCanvas" width="1080" height="1920"></canvas>
271
+
272
+ <!-- Hidden Video Element for processing -->
273
+ <video id="sourceVideo" loop muted playsinline style="display: none;"></video>
274
+
275
+ <!-- HUD Overlay inside Canvas Area -->
276
+ <div class="absolute top-4 right-4 text-right pointer-events-none">
277
+ <div class="text-4xl font-tech font-bold text-white/20" id="fpsCounter">00</div>
278
+ <div class="text-xs text-cyan-400/60 font-mono">REALTIME FPS</div>
279
+ </div>
280
+
281
+ <!-- Loading Indicator -->
282
+ <div id="loading" class="loading-overlay">
283
+ <div class="text-center">
284
+ <div class="w-12 h-12 border-4 border-cyan-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
285
+ <div class="text-cyan-400 font-mono text-sm tracking-widest">PROCESSING DATA...</div>
286
+ </div>
287
+ </div>
288
+
289
+ <!-- Hidden Elements for Audio Analysis -->
290
+ <audio id="audioPlayer" style="display:none;"></audio>
291
+ </div>
292
+
293
+ </main>
294
+
295
+ <script>
296
+ /**
297
+ * APPLICATION STATE & CONFIGURATION
298
+ */
299
+ const state = {
300
+ isPlaying: false,
301
+ fps: 60,
302
+ targetFps: 60,
303
+ lastFrameTime: 0,
304
+ frameInterval: 1000 / 60,
305
+
306
+ // Assets
307
+ images: [],
308
+ currentImageIndex: 0,
309
+ nextImageIndex: 1,
310
+ videoMode: false,
311
+
312
+ // FX Parameters
313
+ morphAmount: 0.5, // 0 to 1
314
+ contrast: 1.0,
315
+ transitionDuration: 1000, // ms
316
+ transitionProgress: 0, // 0 to 1
317
+ isTransitioning: false,
318
+ transitionStartTime: 0,
319
+
320
+ // Layout
321
+ forceCrop: false,
322
+ aspectRatio: 9/16,
323
+
324
+ // Audio
325
+ audioContext: null,
326
+ analyser: null,
327
+ dataArray: null,
328
+ bpm: 0,
329
+ bassEnergy: 0
330
+ };
331
+
332
+ // DOM Elements
333
+ const canvas = document.getElementById('mainCanvas');
334
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
335
+ const video = document.getElementById('sourceVideo');
336
+ const audioPlayer = document.getElementById('audioPlayer');
337
+ const loading = document.getElementById('loading');
338
+ const fpsCounter = document.getElementById('fpsCounter');
339
+
340
+ // Resize Canvas to fit wrapper (maintain high internal resolution)
341
+ function resizeCanvas() {
342
+ const wrapper = document.getElementById('canvas-wrapper');
343
+ const rect = wrapper.getBoundingClientRect();
344
+
345
+ // We keep internal resolution at 1080x1920 (9:16) for consistency
346
+ // but scale it via CSS.
347
+ // If user wants full scale, we can adjust, but let's keep the buffer fixed for performance.
348
+ canvas.width = 1080;
349
+ canvas.height = 1920;
350
+ }
351
+ resizeCanvas();
352
+
353
+ /**
354
+ * IMAGE HANDLING
355
+ */
356
+ document.getElementById('imageInput').addEventListener('change', async (e) => {
357
+ const files = Array.from(e.target.files);
358
+ if (files.length === 0) return;
359
+
360
+ loading.style.display = 'flex';
361
+ state.images = [];
362
+ state.videoMode = false;
363
+ video.pause();
364
+
365
+ for (let file of files) {
366
+ const bitmap = await createImageBitmap(file);
367
+ state.images.push(bitmap);
368
+ }
369
+
370
+ state.currentImageIndex = 0;
371
+ state.nextImageIndex = 1;
372
+ loading.style.display = 'none';
373
+
374
+ // Draw initial frame
375
+ if (!state.isPlaying) drawFrame(0);
376
+ });
377
+
378
+ /**
379
+ * VIDEO HANDLING
380
+ */
381
+ document.getElementById('videoInput').addEventListener('change', (e) => {
382
+ const file = e.target.files[0];
383
+ if (!file) return;
384
+
385
+ const url = URL.createObjectURL(file);
386
+ video.src = url;
387
+ state.videoMode = true;
388
+ state.images = []; // Clear images
389
+
390
+ video.onloadeddata = () => {
391
+ video.play();
392
+ if (!state.isPlaying) {
393
+ state.isPlaying = true;
394
+ loop();
395
+ }
396
+ };
397
+ });
398
+
399
+ /**
400
+ * AUDIO HANDLING & BPM ANALYSIS
401
+ */
402
+ document.getElementById('audioInput').addEventListener('change', (e) => {
403
+ const file = e.target.files[0];
404
+ if (!file) return;
405
+
406
+ const url = URL.createObjectURL(file);
407
+ audioPlayer.src = url;
408
+
409
+ if (!state.audioContext) {
410
+ initAudio();
411
+ }
412
+
413
+ audioPlayer.play();
414
+ });
415
+
416
+ function initAudio() {
417
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
418
+ state.audioContext = new AudioContext();
419
+ const source = state.audioContext.createMediaElementSource(audioPlayer);
420
+ state.analyser = state.audioContext.createAnalyser();
421
+
422
+ state.analyser.fftSize = 256;
423
+ source.connect(state.analyser);
424
+ state.analyser.connect(state.audioContext.destination);
425
+
426
+ state.dataArray = new Uint8Array(state.analyser.frequencyBinCount);
427
+ }
428
+
429
+ function analyzeAudio() {
430
+ if (!state.analyser) return 0;
431
+
432
+ state.analyser.getByteFrequencyData(state.dataArray);
433
+
434
+ // Calculate bass energy (lower frequencies)
435
+ let bassSum = 0;
436
+ for(let i = 0; i < 10; i++) {
437
+ bassSum += state.dataArray[i];
438
+ }
439
+ state.bassEnergy = bassSum / 10;
440
+
441
+ // Simple beat detection simulation (threshold based)
442
+ const isBeat = state.bassEnergy > 200; // Threshold
443
+ return state.bassEnergy / 255; // Normalized 0-1
444
+ }
445
+
446
+ /**
447
+ * UI CONTROLS
448
+ */
449
+ // FPS
450
+ const fpsSlider = document.getElementById('fpsSlider');
451
+ const fpsInput = document.getElementById('fpsInput');
452
+ function updateFps(val) {
453
+ state.targetFps = parseInt(val);
454
+ state.frameInterval = 1000 / state.targetFps;
455
+ fpsSlider.value = val;
456
+ fpsInput.value = val;
457
+ }
458
+ fpsSlider.addEventListener('input', (e) => updateFps(e.target.value));
459
+ fpsInput.addEventListener('change', (e) => updateFps(e.target.value));
460
+
461
+ // Morph
462
+ document.getElementById('morphSlider').addEventListener('input', (e) => {
463
+ state.morphAmount = e.target.value / 100;
464
+ document.getElementById('morphVal').innerText = e.target.value + '%';
465
+ });
466
+
467
+ // Contrast
468
+ document.getElementById('contrastSlider').addEventListener('input', (e) => {
469
+ state.contrast = e.target.value / 100;
470
+ document.getElementById('contrastVal').innerText = state.contrast.toFixed(1);
471
+ });
472
+
473
+ // Speed
474
+ document.getElementById('speedSlider').addEventListener('input', (e) => {
475
+ state.transitionDuration = parseInt(e.target.value);
476
+ document.getElementById('speedVal').innerText = state.transitionDuration + 'ms';
477
+ });
478
+
479
+ // Crop Toggle
480
+ const cropToggle = document.getElementById('cropToggle');
481
+ const cropKnob = cropToggle.querySelector('div');
482
+ cropToggle.addEventListener('click', () => {
483
+ state.forceCrop = !state.forceCrop;
484
+ if (state.forceCrop) {
485
+ cropToggle.classList.remove('bg-gray-700');
486
+ cropToggle.classList.add('bg-cyan-600');
487
+ cropKnob.classList.add('translate-x-5');
488
+ } else {
489
+ cropToggle.classList.add('bg-gray-700');
490
+ cropToggle.classList.remove('bg-cyan-600');
491
+ cropKnob.classList.remove('translate-x-5');
492
+ }
493
+ });
494
+
495
+ // Play/Pause
496
+ const playBtn = document.getElementById('playPauseBtn');
497
+ playBtn.addEventListener('click', () => {
498
+ if (state.images.length === 0 && !state.videoMode) {
499
+ alert("Please upload images or a video first.");
500
+ return;
501
+ }
502
+ state.isPlaying = !state.isPlaying;
503
+ playBtn.innerText = state.isPlaying ? "STOP RENDER" : "START RENDER";
504
+ playBtn.classList.toggle('bg-red-600');
505
+ playBtn.classList.toggle('bg-cyan-600');
506
+
507
+ if (state.isPlaying) {
508
+ state.lastFrameTime = performance.now();
509
+ loop();
510
+ }
511
+ });
512
+
513
+ // Reset
514
+ document.getElementById('resetBtn').addEventListener('click', () => {
515
+ state.isPlaying = false;
516
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
517
+ playBtn.innerText = "START RENDER";
518
+ playBtn.classList.add('bg-cyan-600');
519
+ playBtn.classList.remove('bg-red-600');
520
+ });
521
+
522
+ /**
523
+ * CORE RENDERING LOGIC
524
+ */
525
+ function loop(timestamp) {
526
+ if (!state.isPlaying) return;
527
+
528
+ requestAnimationFrame(loop);
529
+
530
+ const elapsed = timestamp - state.lastFrameTime;
531
+
532
+ // FPS Throttling Logic
533
+ if (elapsed > state.frameInterval) {
534
+ state.lastFrameTime = timestamp - (elapsed % state.frameInterval);
535
+
536
+ // Calculate real FPS
537
+ const currentFps = Math.round(1000 / elapsed);
538
+ fpsCounter.innerText = currentFps;
539
+
540
+ // Update Logic
541
+ updateLogic(timestamp);
542
+
543
+ // Draw Frame
544
+ drawFrame(timestamp);
545
+ }
546
+ }
547
+
548
+ function updateLogic(timestamp) {
549
+ const audioLevel = analyzeAudio();
550
+
551
+ // If in video mode, transition logic is handled differently or ignored for simple playback
552
+ if (state.videoMode) {
553
+ // In video mode, we might apply effects based on audio
554
+ return;
555
+ }
556
+
557
+ // Image Slideshow Logic
558
+ if (!state.isTransitioning) {
559
+ // Check if time to transition (Auto exposure based on BPM or fixed time)
560
+ // For this demo, we use the transition slider as the "length of picture"
561
+ // We can add randomization or audio trigger here.
562
+
563
+ // Simple timer for demo purposes:
564
+ if (!state.lastTransitionTime) state.lastTransitionTime = timestamp;
565
+
566
+ // If audio is playing, try to sync to beats (bass energy spike)
567
+ if (audioLevel > 0.6 && (timestamp - state.lastTransitionTime > 500)) {
568
+ startTransition(timestamp);
569
+ }
570
+ // Fallback to timer if no audio
571
+ else if ((timestamp - state.lastTransitionTime) > (state.transitionDuration * 2)) {
572
+ startTransition(timestamp);
573
+ }
574
+ } else {
575
+ // Handle Transition Progress
576
+ const progress = timestamp - state.transitionStartTime;
577
+ state.transitionProgress = Math.min(progress / state.transitionDuration, 1);
578
+
579
+ if (state.transitionProgress >= 1) {
580
+ // Transition Complete
581
+ state.isTransitioning = false;
582
+ state.currentImageIndex = state.nextImageIndex;
583
+ state.nextImageIndex = (state.currentImageIndex + 1) % state.images.length;
584
+ state.transitionProgress = 0;
585
+ state.lastTransitionTime = timestamp;
586
+ }
587
+ }
588
+ }
589
+
590
+ function startTransition(timestamp) {
591
+ state.isTransitioning = true;
592
+ state.transitionStartTime = timestamp;
593
+ }
594
+
595
+ /**
596
+ * DRAWING & EFFECTS
597
+ */
598
+ function drawFrame(timestamp) {
599
+ // 1. Clear Canvas
600
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
601
+
602
+ // 2. Get Sources
603
+ let source1, source2;
604
+ let w1, h1, w2, h2;
605
+
606
+ if (state.videoMode) {
607
+ source1 = video;
608
+ w1 = video.videoWidth;
609
+ h1 = video.videoHeight;
610
+ source2 = null; // No next frame for video in this simple mode
611
+ } else {
612
+ if (state.images.length === 0) return;
613
+ source1 = state.images[state.currentImageIndex];
614
+ w1 = source1.width;
615
+ h1 = source1.height;
616
+
617
+ if (state.images.length > 1) {
618
+ source2 = state.images[state.nextImageIndex];
619
+ w2 = source2.width;
620
+ h2 = source2.height;
621
+ }
622
+ }
623
+
624
+ // 3. Calculate Crop Coordinates (Object-fit: cover logic)
625
+ const drawParams1 = calculateCover(canvas.width, canvas.height, w1, h1);
626
+
627
+ // 4. Draw Image 1
628
+ ctx.save();
629
+ ctx.filter = `contrast(${state.contrast}) brightness(${1 + (state.bassEnergy/5)})`;
630
+ ctx.drawImage(source1, drawParams1.x, drawParams1.y, drawParams1.w, drawParams1.h);
631
+ ctx.restore();
632
+
633
+ // 5. Draw Image 2 (Morph Transition)
634
+ if (state.isTransitioning && source2) {
635
+ const drawParams2 = calculateCover(canvas.width, canvas.height, w2, h2);
636
+
637
+ ctx.save();
638
+ ctx.globalAlpha = state.transitionProgress; // Simple Fade
639
+
640
+ // Advanced Morphing Effect
641
+ // We use a composite operation or slight scaling/translation based on "Morph Amount"
642
+
643
+ // Raytrace/Morph Simulation:
644
+ // As we transition, we displace pixels or scale the second image
645
+
646
+ const scaleEffect = 1 + (Math.sin(state.transitionProgress * Math.PI) * state.morphAmount * 0.1);
647
+ const centerX = canvas.width / 2;
648
+ const centerY = canvas.height / 2;
649
+
650
+ ctx.translate(centerX, centerY);
651
+ ctx.scale(scaleEffect, scaleEffect);
652
+ ctx.translate(-centerX, -centerY);
653
+
654
+ // Apply Contrast to incoming image too
655
+ ctx.filter = `contrast(${state.contrast})`;
656
+
657
+ // "Sliding Morph" effect logic
658
+ // We shift the X position based on the morph slider to create a sliding feel
659
+ const slideX = (1 - state.transitionProgress) * (canvas.width * state.morphAmount);
660
+ ctx.drawImage(source2, drawParams2.x + slideX, drawParams2.y, drawParams2.w, drawParams2.h);
661
+
662
+ ctx.restore();
663
+ }
664
+
665
+ // 6. Apply Raytrace Overlay / Post-Processing
666
+ applyPostProcessing(timestamp);
667
+ }
668
+
669
+ function calculateCover(canvasW, canvasH, imgW, imgH) {
670
+ const imgRatio = imgW / imgH;
671
+ const canvasRatio = canvasW / canvasH;
672
+ let renderW, renderH, offsetX, offsetY;
673
+
674
+ if (state.forceCrop) {
675
+ // Force 9:16 crop logic (already canvas is 9:16, so this is standard cover)
676
+ if (imgRatio > canvasRatio) {
677
+ renderH = canvasH;
678
+ renderW = imgW * (canvasH / imgH);
679
+ offsetX = (canvasW - renderW) / 2;
680
+ offsetY = 0;
681
+ } else {
682
+ renderW = canvasW;
683
+ renderH = imgH * (canvasW / imgW);
684
+ offsetX = 0;
685
+ offsetY = (canvasH - renderH) / 2;
686
+ }
687
+ } else {
688
+ // Contain or stretch? Let's do Contain to see full image
689
+ if (imgRatio > canvasRatio) {
690
+ renderW = canvasW;
691
+ renderH = imgH * (canvasW / imgW);
692
+ offsetX = 0;
693
+ offsetY = (canvasH - renderH) / 2;
694
+ } else {
695
+ renderH = canvasH;
696
+ renderW = imgW * (canvasH / imgH);
697
+ offsetX = (canvasW - renderW) / 2;
698
+ offsetY = 0;
699
+ }
700
+ }
701
+ return { x: offsetX, y: offsetY, w: renderW, h: renderH };
702
+ }
703
+
704
+ function applyPostProcessing(timestamp) {
705
+ // We simulate "Raytracing" and "Multifx" using Canvas Composite operations and gradients
706
+
707
+ // 1. Scanlines / CRT (Already CSS, but let's add a canvas layer)
708
+ // ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
709
+ // for(let i=0; i<canvas.height; i+=4) {
710
+ // ctx.fillRect(0, i, canvas.width, 1);
711
+ // }
712
+
713
+ // 2. Vignette (Raytrace light falloff)
714
+ const gradient = ctx.createRadialGradient(canvas.width/2, canvas.height/2, canvas.height/3, canvas.width/2, canvas.height/2, canvas.height);
715
+ gradient.addColorStop(0, "rgba(0,0,0,0)");
716
+ gradient.addColorStop(1, "rgba(0,0,0,0.8)");
717
+ ctx.fillStyle = gradient;
718
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
719
+
720
+ // 3. Chromatic Aberration (Simulated via drawing channels with offset - expensive in 2D canvas, simplified here)
721
+ // We use a colored overlay that shifts slightly based on bass
722
+ if (state.bassEnergy > 0.1) {
723
+ const shift = state.bassEnergy * 10;
724
+ ctx.save();
725
+ ctx.globalCompositeOperation = 'screen';
726
+ ctx.fillStyle = `rgba(255, 0, 0, 0.05)`;
727
+ ctx.fillRect(-shift, 0, canvas.width, canvas.height);
728
+ ctx.fillStyle = `rgba(0, 255, 255, 0.05)`;
729
+ ctx.fillRect(shift, 0, canvas.width, canvas.height);
730
+ ctx.restore();
731
+ }
732
+
733
+ // 4. "Kontradt Shaping" visualizer overlay
734
+ // Draw a waveform at the bottom
735
+ if (state.analyser) {
736
+ ctx.save();
737
+ ctx.beginPath();
738
+ ctx.strokeStyle = `rgba(0, 243, 255, ${state.bassEnergy})`;
739
+ ctx.lineWidth = 2;
740
+
741
+ const sliceWidth = canvas.width / state.dataArray.length;
742
+ let x = 0;
743
+
744
+ for(let i = 0; i < state.dataArray.length; i++) {
745
+ const v = state.dataArray[i] / 128.0;
746
+ const y = v * (canvas.height / 4) + (canvas.height - 200); // Draw at bottom
747
+
748
+ if(i === 0) ctx.moveTo(x, y);
749
+ else ctx.lineTo(x, y);
750
+
751
+ x += sliceWidth;
752
+ }
753
+ ctx.stroke();
754
+ ctx.restore();
755
+ }
756
+ }
757
+
758
+ // Initial Draw
759
+ ctx.fillStyle = "#111";
760
+ ctx.fillRect(0,0, canvas.width, canvas.height);
761
+ ctx.fillStyle = "#333";
762
+ ctx.font = "30px Orbitron";
763
+ ctx.textAlign = "center";
764
+ ctx.fillText("NO SIGNAL", canvas.width/2, canvas.height/2);
765
+
766
+ </script>
767
+ </body>
768
+ </html>