Ezequield commited on
Commit
0cb2c0d
·
verified ·
1 Parent(s): 2caad02

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +632 -526
index.html CHANGED
@@ -3,35 +3,35 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>RetroVHS 1935 - Vintage Tape Effects</title>
7
- <!-- Import FontAwesome for Icons -->
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
- <!-- Import Retro Google Font -->
10
- <link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
11
 
12
  <style>
13
  :root {
14
- --vhs-black: #111;
15
- --vhs-dark-gray: #2a2a2a;
16
- --vhs-light-gray: #4a4a4a;
17
- --vhs-neon-blue: #00f0ff;
18
- --vhs-neon-red: #ff3333;
19
- --vhs-static: rgba(255, 255, 255, 0.05);
20
- --screen-glow: 0 0 20px rgba(0, 240, 255, 0.2);
21
- --crt-curvature: inset 0 0 100px rgba(0,0,0,0.9);
 
 
 
22
  }
23
 
24
  * {
25
  box-sizing: border-box;
26
  margin: 0;
27
  padding: 0;
 
28
  }
29
 
30
  body {
31
- background-color: #050505;
32
- background-image: radial-gradient(circle at center, #1a1a1a 0%, #000 100%);
33
- color: #eee;
34
- font-family: 'VT323', monospace;
35
  height: 100vh;
36
  display: flex;
37
  flex-direction: column;
@@ -40,669 +40,775 @@
40
 
41
  /* --- Header --- */
42
  header {
 
 
43
  display: flex;
44
- justify-content: space-between;
45
  align-items: center;
46
- padding: 1rem 2rem;
47
- background: rgba(0,0,0,0.8);
48
- border-bottom: 2px solid var(--vhs-light-gray);
49
- z-index: 10;
50
  }
51
 
52
- .brand {
53
- font-size: 1.8rem;
54
- letter-spacing: 2px;
55
- color: var(--vhs-neon-blue);
56
- text-shadow: 2px 2px 0px rgba(255,0,0,0.5);
 
 
57
  }
58
 
59
- .brand span {
60
- color: #fff;
 
 
 
61
  }
62
 
63
- .anycoder-link {
64
- font-size: 1.2rem;
65
- color: #aaa;
 
 
 
 
 
 
 
 
 
66
  text-decoration: none;
67
- transition: color 0.3s ease;
68
  }
69
 
70
- .anycoder-link:hover {
71
- color: var(--vhs-neon-blue);
72
- text-decoration: underline;
 
 
 
 
 
 
 
 
 
73
  }
74
 
75
  /* --- Main Layout --- */
76
  main {
77
  flex: 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  display: flex;
79
- justify-content: center;
80
  align-items: center;
81
- padding: 20px;
82
- perspective: 1000px;
 
 
 
 
 
83
  }
84
 
85
- .vcr-unit {
86
- display: grid;
87
- grid-template-columns: 1fr 300px;
88
- gap: 20px;
89
- background: var(--vhs-dark-gray);
90
- padding: 20px;
91
- border-radius: 10px;
92
- box-shadow:
93
- 0 20px 50px rgba(0,0,0,0.8),
94
- inset 0 1px 1px rgba(255,255,255,0.1);
95
- border: 1px solid #333;
96
- max-width: 1200px;
97
- width: 100%;
98
- height: 80vh;
99
- transform-style: preserve-3d;
100
- transition: transform 0.5s ease;
101
  }
102
 
103
- /* --- The CRT Screen --- */
104
- .screen-container {
 
 
 
 
 
 
 
 
105
  position: relative;
106
- background: #000;
107
- border-radius: 20px; /* CRT shape */
108
  overflow: hidden;
109
- border: 8px solid #1a1a1a;
110
- box-shadow: var(--crt-curvature);
111
  display: flex;
112
- justify-content: center;
113
  align-items: center;
 
114
  }
115
 
116
- /* The actual video element */
117
  video {
118
  width: 100%;
119
  height: 100%;
120
- object-fit: cover;
121
- opacity: 0; /* Hidden until loaded */
122
- transition: opacity 1s;
123
- filter: contrast(1.2) brightness(0.9) sepia(0.3) saturate(1.2);
124
  }
125
 
126
- video.active {
127
- opacity: 1;
 
 
 
 
 
 
128
  }
129
 
130
- /* Effects Layers */
131
- .scanlines {
132
- position: absolute;
133
- top: 0;
134
- left: 0;
135
- width: 100%;
136
- height: 100%;
137
- background: linear-gradient(
138
- to bottom,
139
- rgba(255,255,255,0),
140
- rgba(255,255,255,0) 50%,
141
- rgba(0,0,0,0.2) 50%,
142
- rgba(0,0,0,0.2)
143
- );
144
- background-size: 100% 4px;
145
- pointer-events: none;
146
- z-index: 2;
147
- }
148
 
149
- .vhs-overlay {
150
- position: absolute;
151
- top: 0;
152
- left: 0;
153
- width: 100%;
154
- height: 100%;
155
- background: rgba(18, 16, 16, 0.1);
156
- opacity: 0;
157
- z-index: 3;
158
- pointer-events: none;
159
- mix-blend-mode: overlay;
160
- animation: noise-flicker 0.2s infinite;
161
  }
162
 
163
- .tracking-error {
164
- position: absolute;
165
- top: 0;
166
- left: 0;
167
- width: 100%;
168
- height: 5px;
169
- background: rgba(255, 255, 255, 0.8);
170
- z-index: 4;
171
- box-shadow: 0 0 10px rgba(255,255,255,0.8);
172
- display: none;
173
  }
174
 
175
- .no-signal-text {
176
- position: absolute;
177
- color: var(--vhs-neon-blue);
178
- font-size: 2rem;
179
- animation: pulse 2s infinite;
180
- z-index: 1;
181
- text-align: center;
182
  }
183
 
184
- /* --- The Control Deck --- */
185
- .deck-panel {
186
- background: linear-gradient(to bottom, #333, #222);
187
- border-radius: 5px;
188
- padding: 15px;
189
- display: flex;
190
- flex-direction: column;
191
- gap: 15px;
192
- border: 1px solid #444;
193
- box-shadow: inset 0 0 20px #000;
194
  }
195
 
196
- .tape-slot {
197
- height: 80px;
198
- background: #111;
199
- border: 2px inset #444;
200
- margin-bottom: 10px;
201
- display: flex;
202
- justify-content: center;
203
- align-items: center;
204
- position: relative;
205
- }
206
 
207
- .tape-label {
208
- background: #e6cba5; /* Old paper color */
209
- color: #333;
210
- padding: 5px 10px;
211
- font-size: 0.9rem;
212
- font-weight: bold;
213
- transform: skewX(-10deg);
214
- box-shadow: 0 2px 5px rgba(0,0,0,0.5);
215
- border: 1px solid #8b7d6b;
216
- width: 80%;
217
- text-align: center;
218
- display: none;
219
- }
220
-
221
- .tape-label.visible {
222
- display: block;
223
- animation: insert-tape 0.5s ease-out forwards;
224
  }
225
 
226
- .lcd-display {
227
- background: #2b3a2b;
228
- border: 2px inset #111;
229
- padding: 10px;
230
- font-family: 'VT323', monospace;
231
- color: var(--vhs-neon-blue);
232
- display: flex;
233
- flex-direction: column;
234
- gap: 5px;
235
- box-shadow: inset 0 0 10px rgba(0,0,0,0.8);
236
  }
237
 
238
- .lcd-row {
 
 
239
  display: flex;
240
- justify-content: space-between;
241
- font-size: 1.2rem;
 
 
 
242
  }
243
 
244
- .rec-dot {
245
- color: var(--vhs-neon-red);
246
- animation: blink 1s infinite;
247
- display: none;
 
 
248
  }
249
- .rec-dot.on { display: inline-block; }
250
 
251
- .controls {
252
- display: grid;
253
- grid-template-columns: repeat(3, 1fr);
254
- gap: 10px;
255
- margin-top: auto;
 
256
  }
257
 
258
- .btn {
259
- background: #444;
260
- border: none;
261
- border-bottom: 4px solid #222;
262
- color: #fff;
263
- padding: 15px;
264
- border-radius: 5px;
265
- cursor: pointer;
266
- font-family: 'VT323', monospace;
267
- font-size: 1.2rem;
268
- transition: all 0.1s;
269
  display: flex;
270
- flex-direction: column;
271
  align-items: center;
272
- justify-content: center;
273
- gap: 5px;
 
 
274
  }
275
 
276
- .btn:active {
277
- transform: translateY(4px);
278
- border-bottom: 0px solid #222;
279
- background: #333;
280
  }
281
 
282
- .btn:hover {
283
- background: #555;
 
 
 
 
 
284
  }
285
 
286
- .btn-upload {
287
- grid-column: span 3;
288
- background: #2a3b2a;
289
- border: 1px solid #3d5c3d;
290
- color: #8fbc8f;
291
- }
292
-
293
- .btn-upload:hover {
294
- background: #3d5c3d;
295
- color: #fff;
296
- box-shadow: 0 0 10px rgba(143, 188, 143, 0.3);
297
  }
298
 
299
- /* --- Canvas for Noise --- */
300
- #noise-canvas {
301
  position: absolute;
302
  top: 0;
303
- left: 0;
304
- width: 100%;
305
- height: 100%;
306
- pointer-events: none;
307
- z-index: 5;
308
- opacity: 0.15;
 
 
 
 
 
 
 
309
  }
310
 
311
  /* --- Toast Notification --- */
312
  .toast {
313
  position: fixed;
314
  bottom: 20px;
315
- left: 50%;
316
- transform: translateX(-50%);
317
- background: rgba(0, 0, 0, 0.9);
318
- border: 1px solid var(--vhs-neon-blue);
319
- color: var(--vhs-neon-blue);
320
  padding: 10px 20px;
321
  border-radius: 4px;
322
- font-size: 1.2rem;
323
- opacity: 0;
324
- transition: opacity 0.3s;
325
- pointer-events: none;
326
- z-index: 100;
327
  }
328
- .toast.show { opacity: 1; }
329
 
330
- /* --- Animations --- */
331
- @keyframes blink {
332
- 0%, 100% { opacity: 1; }
333
- 50% { opacity: 0; }
334
  }
335
 
336
- @keyframes pulse {
337
- 0% { opacity: 0.6; }
338
- 50% { opacity: 1; }
339
- 100% { opacity: 0.6; }
 
 
 
 
 
340
  }
341
 
342
- @keyframes noise-flicker {
343
- 0% { opacity: 0.1; }
344
- 5% { opacity: 0.2; }
345
- 10% { opacity: 0.1; }
346
- 15% { opacity: 0.3; }
347
- 20% { opacity: 0.1; }
348
- 50% { opacity: 0.1; }
349
- 55% { opacity: 0.3; }
350
- 60% { opacity: 0; }
351
- 100% { opacity: 0.1; }
352
  }
353
 
354
- @keyframes insert-tape {
355
- from { transform: skewX(-10deg) translateY(-50px); opacity: 0; }
356
- to { transform: skewX(-10deg) translateY(0); opacity: 1; }
357
  }
358
 
359
- /* --- Responsive Design --- */
360
- @media (max-width: 900px) {
361
- .vcr-unit {
362
- grid-template-columns: 1fr;
363
- grid-template-rows: 40vh auto;
364
- height: auto;
365
- }
366
-
367
- main {
368
- overflow-y: auto;
369
- display: block; /* Allow scroll on mobile */
370
- }
371
  }
 
 
 
 
 
 
 
372
  </style>
373
  </head>
374
  <body>
375
 
 
376
  <header>
377
- <div class="brand"><i class="fas fa-film"></i> VHS<span>ARCHIVE</span></div>
378
- <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
379
- Built with anycoder <i class="fas fa-external-link-alt"></i>
380
- </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  </header>
382
 
 
383
  <main>
384
- <div class="vcr-unit">
385
- <!-- Left Side: The Screen -->
386
- <div class="screen-container" id="screen-container">
387
- <div class="no-signal-text" id="no-signal">
388
- <i class="fas fa-snowflake"></i><br>
389
- NO SIGNAL<br>
390
- INSERT TAPE
 
 
 
 
 
 
 
 
 
 
 
391
  </div>
392
- <video id="main-video" playsinline loop></video>
393
- <canvas id="noise-canvas"></canvas>
394
- <div class="scanlines"></div>
395
- <div class="vhs-overlay"></div>
396
- <div class="tracking-error" id="tracking-line"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  </div>
 
398
 
399
- <!-- Right Side: The Deck -->
400
- <div class="deck-panel">
401
- <div class="tape-slot">
402
- <div class="tape-label" id="tape-label">
403
- REC 1935<br>
404
- <small>ARCHIVAL FOOTAGE</small>
405
- </div>
 
 
 
 
 
 
 
 
 
 
 
406
  </div>
 
 
407
 
408
- <div class="lcd-display">
409
- <div class="lcd-row">
410
- <span>DATE: 1935-08-12</span>
411
- <span class="rec-dot" id="rec-dot">● REC</span>
412
- </div>
413
- <div class="lcd-row" style="justify-content: flex-end;">
414
- <span id="time-code">00:00:00</span>
415
- </div>
416
- <div class="lcd-row">
417
- <span id="status-text">STOP</span>
418
- <span>SP</span>
419
- </div>
420
  </div>
 
 
421
 
422
- <div class="controls">
423
- <input type="file" id="video-upload" accept="video/*" style="display: none;">
424
-
425
- <button class="btn btn-upload" onclick="document.getElementById('video-upload').click()">
426
- <i class="fas fa-upload"></i> LOAD TAPE
427
- </button>
428
-
429
- <button class="btn" id="btn-rewind">
430
- <i class="fas fa-backward"></i> REW
431
- </button>
432
-
433
- <button class="btn" id="btn-play">
434
- <i class="fas fa-play"></i> PLAY
435
- </button>
436
-
437
- <button class="btn" id="btn-ffwd">
438
- <i class="fas fa-forward"></i> FF
439
- </button>
440
-
441
- <button class="btn" id="btn-stop" style="grid-column: span 3; background: #522;">
442
- <i class="fas fa-stop"></i> STOP / EJECT
443
- </button>
444
  </div>
 
445
  </div>
446
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  </main>
448
 
449
- <div class="toast" id="toast">Message</div>
 
450
 
451
  <script>
452
- /**
453
- * VHS Application Logic
454
- * Handles video playback, UI state, and visual effects generation.
455
- */
456
-
457
- // Elements
458
- const videoInput = document.getElementById('video-upload');
459
- const videoElement = document.getElementById('main-video');
460
- const noiseCanvas = document.getElementById('noise-canvas');
461
- const ctx = noiseCanvas.getContext('2d');
462
- const noSignalText = document.getElementById('no-signal');
463
- const tapeLabel = document.getElementById('tape-label');
464
- const timeCodeDisplay = document.getElementById('time-code');
465
- const statusText = document.getElementById('status-text');
466
- const recDot = document.getElementById('rec-dot');
467
- const trackingLine = document.getElementById('tracking-line');
468
- const toastEl = document.getElementById('toast');
469
- const screenContainer = document.getElementById('screen-container');
470
-
471
- // Buttons
472
- const btnPlay = document.getElementById('btn-play');
473
- const btnStop = document.getElementById('btn-stop');
474
- const btnRewind = document.getElementById('btn-rewind');
475
- const btnFfwd = document.getElementById('btn-ffwd');
476
 
477
  // State
478
  let isPlaying = false;
479
- let animationFrameId;
480
- let noiseInterval;
481
- let trackingInterval;
482
- let playbackSpeed = 1.0;
483
-
484
- // --- Initialization ---
485
-
486
- function resizeCanvas() {
487
- noiseCanvas.width = screenContainer.offsetWidth;
488
- noiseCanvas.height = screenContainer.offsetHeight;
489
- }
490
-
491
- window.addEventListener('resize', resizeCanvas);
492
- resizeCanvas();
493
 
494
  // --- Event Listeners ---
495
 
496
- videoInput.addEventListener('change', handleFileUpload);
497
-
498
- btnPlay.addEventListener('click', togglePlay);
499
-
500
- btnStop.addEventListener('click', stopVideo);
 
 
 
 
 
 
501
 
502
- btnRewind.addEventListener('mousedown', () => changeSpeed(-3));
503
- btnRewind.addEventListener('mouseup', () => changeSpeed(1));
504
- btnRewind.addEventListener('mouseleave', () => changeSpeed(1));
 
 
505
 
506
- btnFfwd.addEventListener('mousedown', () => changeSpeed(3));
507
- btnFfwd.addEventListener('mouseup', () => changeSpeed(1));
508
- btnFfwd.addEventListener('mouseleave', () => changeSpeed(1));
 
509
 
510
- videoElement.addEventListener('timeupdate', updateTimeCode);
511
- videoElement.addEventListener('ended', () => {
512
  isPlaying = false;
513
- updatePlayButton();
514
- statusText.innerText = "STOP";
515
- recDot.classList.remove('on');
516
  });
517
 
518
- // --- Core Functions ---
519
-
520
- function handleFileUpload(e) {
521
- const file = e.target.files[0];
522
- if (!file) return;
 
 
 
 
 
 
 
523
 
524
- const fileURL = URL.createObjectURL(file);
525
- videoElement.src = fileURL;
526
-
527
- // UI Updates
528
- noSignalText.style.display = 'none';
529
- videoElement.classList.add('active');
530
- tapeLabel.classList.add('visible');
531
-
532
- showToast("TAPE LOADED: " + file.name.substring(0, 15).toUpperCase());
533
-
534
- // Simulate loading delay
535
- statusText.innerText = "LOADING...";
536
- setTimeout(() => {
537
- statusText.innerText = "READY";
538
- playVideo();
539
- }, 800);
540
- }
541
 
542
- function togglePlay() {
543
- if (!videoElement.src) {
544
- showToast("NO TAPE INSERTED");
545
- return;
 
546
  }
 
 
 
547
 
548
- if (isPlaying) {
549
- pauseVideo();
 
 
 
550
  } else {
551
- playVideo();
 
552
  }
553
  }
554
 
555
- function playVideo() {
556
- videoElement.play().then(() => {
557
- isPlaying = true;
558
- updatePlayButton();
559
- statusText.innerText = "PLAY >";
560
- recDot.classList.add('on');
561
- startEffects();
562
- }).catch(err => {
563
- console.error("Playback error:", err);
564
- showToast("PLAYBACK ERROR");
565
- });
566
  }
567
 
568
- function pauseVideo() {
569
- videoElement.pause();
570
- isPlaying = false;
571
- updatePlayButton();
572
- statusText.innerText = "PAUSE ||";
573
- recDot.classList.remove('on');
574
- stopEffects();
575
  }
576
 
577
- function stopVideo() {
578
- if (!videoElement.src) return;
 
 
 
 
 
 
 
 
579
 
580
- videoElement.pause();
581
- videoElement.currentTime = 0;
582
- isPlaying = false;
583
- updatePlayButton();
584
- statusText.innerText = "STOP";
585
- recDot.classList.remove('on');
586
- stopEffects();
 
 
 
 
 
 
 
 
 
 
587
 
588
- // Simulate slight static burst on stop
589
- drawNoiseFrame();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
590
  }
591
 
592
- function changeSpeed(speed) {
593
- if (!videoElement.src) return;
594
- videoElement.playbackRate = speed;
595
 
596
- if (speed === 1) {
597
- statusText.innerText = isPlaying ? "PLAY >" : "PAUSE ||";
598
- } else if (speed > 1) {
599
- statusText.innerText = "CUE >>";
600
- if(!isPlaying) playVideo();
601
- } else {
602
- statusText.innerText = "<< REVIEW";
603
- if(!isPlaying) playVideo();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
  }
 
605
  }
606
 
607
- function updatePlayButton() {
608
- const icon = btnPlay.querySelector('i');
609
- if (isPlaying) {
610
- icon.classList.remove('fa-play');
611
- icon.classList.add('fa-pause');
612
  } else {
613
- icon.classList.remove('fa-pause');
614
- icon.classList.add('fa-play');
615
  }
616
  }
617
 
618
- function updateTimeCode() {
619
- const time = videoElement.currentTime;
620
- const date = new Date(0);
621
- date.setSeconds(time);
622
- const timeString = date.toISOString().substr(11, 8);
623
- timeCodeDisplay.innerText = timeString;
624
- }
625
 
626
- function showToast(msg) {
627
- toastEl.innerText = msg;
628
- toastEl.classList.add('show');
629
  setTimeout(() => {
630
- toastEl.classList.remove('show');
631
  }, 3000);
632
  }
633
 
634
- // --- Visual Effects Engine (Canvas + CSS) ---
635
-
636
- function startEffects() {
637
- if (noiseInterval) clearInterval(noiseInterval);
638
- if (trackingInterval) clearInterval(trackingInterval);
639
-
640
- // Draw noise frequently
641
- noiseInterval = setInterval(() => {
642
- if (Math.random() > 0.5) drawNoiseFrame();
643
- }, 50);
644
-
645
- // Random tracking errors
646
- trackingInterval = setInterval(() => {
647
- if (Math.random() > 0.92) {
648
- triggerTrackingError();
649
- }
650
- }, 100);
651
- }
652
-
653
- function stopEffects() {
654
- clearInterval(noiseInterval);
655
- clearInterval(trackingInterval);
656
- ctx.clearRect(0, 0, noiseCanvas.width, noiseCanvas.height);
657
- trackingLine.style.display = 'none';
658
- }
659
-
660
- function drawNoiseFrame() {
661
- const w = noiseCanvas.width;
662
- const h = noiseCanvas.height;
663
- const idata = ctx.createImageData(w, h);
664
- const buffer32 = new Uint32Array(idata.data.buffer);
665
- const len = buffer32.length;
666
-
667
- for (let i = 0; i < len; i++) {
668
- if (Math.random() < 0.1) {
669
- // White noise
670
- buffer32[i] = 0xffffffff;
671
- }
672
  }
673
- ctx.putImageData(idata, 0, 0);
674
- }
675
 
676
- function triggerTrackingError() {
677
- // Display the horizontal line element
678
- const randomTop = Math.random() * 100;
679
- trackingLine.style.top = randomTop + '%';
680
- trackingLine.style.display = 'block';
681
 
682
- // Shake the screen container slightly
683
- screenContainer.style.transform = `translate(${Math.random()*4-2}px, ${Math.random()*4-2}px)`;
684
-
685
- // Remove effect after short duration
686
- setTimeout(() => {
687
- trackingLine.style.display = 'none';
688
- screenContainer.style.transform = 'none';
689
- }, 150);
690
- }
691
-
692
- // --- Chromatic Aberration (RGB Shift) Effect on Scroll/Move ---
693
- // We simulate this by updating CSS variables or applying subtle transforms
694
- document.addEventListener('mousemove', (e) => {
695
- if (!isPlaying) return;
696
-
697
- const x = (window.innerWidth - e.pageX * 2) / 100;
698
- const y = (window.innerHeight - e.pageY * 2) / 100;
699
-
700
- // Subtle RGB shift using text-shadow trick on a container or filter
701
- // For video, filter: drop-shadow is expensive.
702
- // Instead, we slightly offset the noise canvas.
703
 
704
- noiseCanvas.style.transform = `translate(${x}px, ${y}px)`;
705
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
706
 
707
  </script>
708
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>VegaEdit Pro - Video Effects Studio</title>
7
+ <!-- Importing FontAwesome for Icons -->
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
 
9
 
10
  <style>
11
  :root {
12
+ --bg-dark: #1e1e1e;
13
+ --bg-panel: #252526;
14
+ --bg-header: #333333;
15
+ --accent: #007acc;
16
+ --accent-hover: #0062a3;
17
+ --text-main: #d4d4d4;
18
+ --text-muted: #858585;
19
+ --border: #3e3e42;
20
+ --timeline-track: #1e1e1e;
21
+ --timeline-clips: #3a3d41;
22
+ --playhead: #f44336;
23
  }
24
 
25
  * {
26
  box-sizing: border-box;
27
  margin: 0;
28
  padding: 0;
29
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
30
  }
31
 
32
  body {
33
+ background-color: var(--bg-dark);
34
+ color: var(--text-main);
 
 
35
  height: 100vh;
36
  display: flex;
37
  flex-direction: column;
 
40
 
41
  /* --- Header --- */
42
  header {
43
+ height: 50px;
44
+ background-color: var(--bg-header);
45
  display: flex;
 
46
  align-items: center;
47
+ padding: 0 20px;
48
+ justify-content: space-between;
49
+ border-bottom: 1px solid var(--border);
 
50
  }
51
 
52
+ .logo {
53
+ font-weight: bold;
54
+ font-size: 1.2rem;
55
+ color: var(--accent);
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 10px;
59
  }
60
 
61
+ .logo i { font-size: 1.4rem; }
62
+
63
+ .header-controls {
64
+ display: flex;
65
+ gap: 15px;
66
  }
67
 
68
+ .btn {
69
+ background-color: var(--bg-panel);
70
+ color: var(--text-main);
71
+ border: 1px solid var(--border);
72
+ padding: 6px 12px;
73
+ border-radius: 4px;
74
+ cursor: pointer;
75
+ font-size: 0.9rem;
76
+ transition: all 0.2s;
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 8px;
80
  text-decoration: none;
 
81
  }
82
 
83
+ .btn:hover {
84
+ background-color: var(--border);
85
+ }
86
+
87
+ .btn-primary {
88
+ background-color: var(--accent);
89
+ border-color: var(--accent);
90
+ color: white;
91
+ }
92
+
93
+ .btn-primary:hover {
94
+ background-color: var(--accent-hover);
95
  }
96
 
97
  /* --- Main Layout --- */
98
  main {
99
  flex: 1;
100
+ display: grid;
101
+ grid-template-columns: 280px 1fr 300px;
102
+ grid-template-rows: 1fr 200px;
103
+ overflow: hidden;
104
+ }
105
+
106
+ /* Areas */
107
+ .sidebar-left { grid-row: 1 / 2; grid-column: 1 / 2; border-right: 1px solid var(--border); display: flex; flex-direction: column; }
108
+ .preview-area { grid-row: 1 / 2; grid-column: 2 / 3; background-color: #111; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; padding: 20px; }
109
+ .sidebar-right { grid-row: 1 / 2; grid-column: 3 / 4; border-left: 1px solid var(--border); padding: 15px; overflow-y: auto; }
110
+ .timeline-area { grid-row: 2 / 3; grid-column: 1 / 4; background-color: var(--bg-panel); border-top: 1px solid var(--border); display: flex; flex-direction: column; }
111
+
112
+ /* --- Sidebar Left (Effects Library) --- */
113
+ .panel-header {
114
+ padding: 10px 15px;
115
+ background-color: var(--bg-header);
116
+ font-weight: 600;
117
+ font-size: 0.9rem;
118
+ text-transform: uppercase;
119
+ letter-spacing: 0.5px;
120
+ border-bottom: 1px solid var(--border);
121
+ }
122
+
123
+ .effects-list {
124
+ padding: 10px;
125
+ overflow-y: auto;
126
+ }
127
+
128
+ .effect-item {
129
  display: flex;
 
130
  align-items: center;
131
+ padding: 10px;
132
+ margin-bottom: 5px;
133
+ background-color: var(--bg-dark);
134
+ border: 1px solid transparent;
135
+ border-radius: 4px;
136
+ cursor: grab;
137
+ transition: 0.2s;
138
  }
139
 
140
+ .effect-item:hover {
141
+ border-color: var(--accent);
142
+ background-color: #2d2d30;
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  }
144
 
145
+ .effect-item i { margin-right: 10px; color: var(--accent); width: 20px; text-align: center; }
146
+
147
+ /* --- Preview Area --- */
148
+ .video-container {
149
+ width: 100%;
150
+ max-width: 800px;
151
+ aspect-ratio: 16/9;
152
+ background-color: #000;
153
+ border-radius: 4px;
154
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
155
  position: relative;
 
 
156
  overflow: hidden;
 
 
157
  display: flex;
 
158
  align-items: center;
159
+ justify-content: center;
160
  }
161
 
 
162
  video {
163
  width: 100%;
164
  height: 100%;
165
+ object-fit: contain;
166
+ /* Filters will be applied here via JS */
 
 
167
  }
168
 
169
+ .upload-placeholder {
170
+ text-align: center;
171
+ color: var(--text-muted);
172
+ display: flex;
173
+ flex-direction: column;
174
+ align-items: center;
175
+ gap: 15px;
176
+ z-index: 10;
177
  }
178
 
179
+ .upload-placeholder i { font-size: 3rem; opacity: 0.5; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
+ /* --- Sidebar Right (Controls) --- */
182
+ .control-group {
183
+ margin-bottom: 20px;
184
+ background: var(--bg-dark);
185
+ padding: 10px;
186
+ border-radius: 4px;
187
+ border: 1px solid var(--border);
 
 
 
 
 
188
  }
189
 
190
+ .control-label {
191
+ display: flex;
192
+ justify-content: space-between;
193
+ font-size: 0.85rem;
194
+ margin-bottom: 8px;
195
+ color: var(--text-main);
 
 
 
 
196
  }
197
 
198
+ .value-display {
199
+ color: var(--accent);
200
+ font-family: monospace;
 
 
 
 
201
  }
202
 
203
+ /* Custom Range Slider */
204
+ input[type=range] {
205
+ -webkit-appearance: none;
206
+ width: 100%;
207
+ background: transparent;
 
 
 
 
 
208
  }
209
 
210
+ input[type=range]:focus { outline: none; }
 
 
 
 
 
 
 
 
 
211
 
212
+ input[type=range]::-webkit-slider-thumb {
213
+ -webkit-appearance: none;
214
+ height: 14px;
215
+ width: 14px;
216
+ border-radius: 50%;
217
+ background: var(--accent);
218
+ cursor: pointer;
219
+ margin-top: -5px;
220
+ box-shadow: 0 0 5px rgba(0,0,0,0.5);
 
 
 
 
 
 
 
 
221
  }
222
 
223
+ input[type=range]::-webkit-slider-runnable-track {
224
+ width: 100%;
225
+ height: 4px;
226
+ cursor: pointer;
227
+ background: var(--border);
228
+ border-radius: 2px;
 
 
 
 
229
  }
230
 
231
+ /* --- Timeline --- */
232
+ .timeline-controls {
233
+ height: 40px;
234
  display: flex;
235
+ align-items: center;
236
+ padding: 0 15px;
237
+ background-color: var(--bg-header);
238
+ gap: 15px;
239
+ border-bottom: 1px solid var(--border);
240
  }
241
 
242
+ .timeline-tracks {
243
+ flex: 1;
244
+ position: relative;
245
+ overflow-x: auto;
246
+ background-color: var(--timeline-track);
247
+ padding: 10px 0;
248
  }
 
249
 
250
+ .track {
251
+ height: 40px;
252
+ background-color: #2a2a2a;
253
+ margin: 5px 20px;
254
+ position: relative;
255
+ border-radius: 4px;
256
  }
257
 
258
+ .clip {
259
+ position: absolute;
260
+ left: 0;
261
+ top: 0;
262
+ height: 100%;
263
+ background-color: var(--timeline-clips);
264
+ border: 1px solid #555;
265
+ border-radius: 4px;
266
+ overflow: hidden;
 
 
267
  display: flex;
 
268
  align-items: center;
269
+ padding-left: 10px;
270
+ font-size: 0.75rem;
271
+ color: #ccc;
272
+ box-shadow: 1px 1px 3px rgba(0,0,0,0.3);
273
  }
274
 
275
+ .clip.active {
276
+ background-color: #4a4a50;
277
+ border-color: var(--accent);
 
278
  }
279
 
280
+ .playhead-container {
281
+ position: absolute;
282
+ top: 0;
283
+ bottom: 0;
284
+ left: 20px; /* Matches track margin */
285
+ right: 20px;
286
+ pointer-events: none;
287
  }
288
 
289
+ .playhead {
290
+ position: absolute;
291
+ top: 0;
292
+ bottom: 0;
293
+ width: 2px;
294
+ background-color: var(--playhead);
295
+ z-index: 10;
296
+ left: 0%;
 
 
 
297
  }
298
 
299
+ .playhead::before {
300
+ content: '';
301
  position: absolute;
302
  top: 0;
303
+ left: -5px;
304
+ border-left: 6px solid transparent;
305
+ border-right: 6px solid transparent;
306
+ border-top: 8px solid var(--playhead);
307
+ }
308
+
309
+ .time-display {
310
+ font-family: monospace;
311
+ font-size: 1rem;
312
+ color: var(--accent);
313
+ background: #000;
314
+ padding: 2px 6px;
315
+ border-radius: 2px;
316
  }
317
 
318
  /* --- Toast Notification --- */
319
  .toast {
320
  position: fixed;
321
  bottom: 20px;
322
+ right: 20px;
323
+ background-color: var(--accent);
324
+ color: white;
 
 
325
  padding: 10px 20px;
326
  border-radius: 4px;
327
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
328
+ transform: translateY(100px);
329
+ transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
330
+ z-index: 1000;
 
331
  }
 
332
 
333
+ .toast.show {
334
+ transform: translateY(0);
 
 
335
  }
336
 
337
+ /* --- Responsive --- */
338
+ @media (max-width: 900px) {
339
+ main {
340
+ grid-template-columns: 200px 1fr;
341
+ grid-template-rows: 1fr 200px;
342
+ }
343
+ .sidebar-right {
344
+ display: none; /* Hide properties on tablet for simplicity, or move to modal */
345
+ }
346
  }
347
 
348
+ @media (max-width: 600px) {
349
+ main {
350
+ grid-template-columns: 1fr;
351
+ grid-template-rows: auto 1fr auto;
352
+ overflow-y: auto;
353
+ }
354
+ .sidebar-left { display: none; } /* Hide library on mobile */
355
+ .timeline-area { height: 150px; }
356
+ .header-controls span { display: none; } /* Hide text on buttons */
 
357
  }
358
 
359
+ /* Glitch Effect Class (Special) */
360
+ .glitch-active {
361
+ animation: glitch-anim 0.3s infinite;
362
  }
363
 
364
+ @keyframes glitch-anim {
365
+ 0% { transform: translate(0) }
366
+ 20% { transform: translate(-2px, 2px) }
367
+ 40% { transform: translate(-2px, -2px) }
368
+ 60% { transform: translate(2px, 2px) }
369
+ 80% { transform: translate(2px, -2px) }
370
+ 100% { transform: translate(0) }
 
 
 
 
 
371
  }
372
+
373
+ /* Custom Scrollbar */
374
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
375
+ ::-webkit-scrollbar-track { background: var(--bg-dark); }
376
+ ::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }
377
+ ::-webkit-scrollbar-thumb:hover { background: #555; }
378
+
379
  </style>
380
  </head>
381
  <body>
382
 
383
+ <!-- Header -->
384
  <header>
385
+ <div class="logo">
386
+ <i class="fa-solid fa-film"></i>
387
+ <span>VegaEdit Pro</span>
388
+ </div>
389
+ <div class="header-controls">
390
+ <!-- Hidden File Input -->
391
+ <input type="file" id="videoInput" accept="video/*" style="display: none;">
392
+
393
+ <button class="btn btn-primary" onclick="document.getElementById('videoInput').click()">
394
+ <i class="fa-solid fa-upload"></i> <span>Open Video</span>
395
+ </button>
396
+
397
+ <button class="btn" id="snapshotBtn" title="Take Snapshot">
398
+ <i class="fa-solid fa-camera"></i>
399
+ </button>
400
+
401
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="btn" style="color: #aaa; font-size: 0.8rem; border: none;">
402
+ Built with anycoder
403
+ </a>
404
+ </div>
405
  </header>
406
 
407
+ <!-- Main Workspace -->
408
  <main>
409
+ <!-- Sidebar: Effects Library -->
410
+ <aside class="sidebar-left">
411
+ <div class="panel-header">FX Library</div>
412
+ <div class="effects-list">
413
+ <div class="effect-item" onclick="resetEffect('all')">
414
+ <i class="fa-solid fa-ban"></i> Reset All
415
+ </div>
416
+ <div class="effect-item" onclick="setPreset('bw')">
417
+ <i class="fa-solid fa-eye-dropper"></i> Black & White
418
+ </div>
419
+ <div class="effect-item" onclick="setPreset('sepia')">
420
+ <i class="fa-solid fa-mug-hot"></i> Vintage Sepia
421
+ </div>
422
+ <div class="effect-item" onclick="setPreset('cyber')">
423
+ <i class="fa-solid fa-robot"></i> Cyberpunk
424
+ </div>
425
+ <div class="effect-item" onclick="toggleGlitch()">
426
+ <i class="fa-solid fa-bolt"></i> Glitch Toggle
427
  </div>
428
+ <div class="effect-item" onclick="setPreset('vhs')">
429
+ <i class="fa-solid fa-tv"></i> VHS Static
430
+ </div>
431
+ <div class="effect-item" onclick="setPreset('invert')">
432
+ <i class="fa-solid fa-circle-half-stroke"></i> Invert Colors
433
+ </div>
434
+ </div>
435
+ <div class="panel-header">Project Media</div>
436
+ <div style="padding: 10px; font-size: 0.8rem; color: #666; text-align: center;">
437
+ Drag & Drop video here<br>(Not implemented in demo)
438
+ </div>
439
+ </aside>
440
+
441
+ <!-- Preview Area -->
442
+ <section class="preview-area">
443
+ <div class="video-container" id="videoWrapper">
444
+ <div class="upload-placeholder" id="uploadPlaceholder">
445
+ <i class="fa-solid fa-cloud-arrow-up"></i>
446
+ <h3>No Video Loaded</h3>
447
+ <p>Click "Open Video" to start editing</p>
448
+ <button class="btn btn-primary" style="margin-top:10px;" onclick="document.getElementById('videoInput').click()">Browse Files</button>
449
+ </div>
450
+ <video id="mainVideo" playsinline></video>
451
  </div>
452
+ </section>
453
 
454
+ <!-- Sidebar: Properties -->
455
+ <aside class="sidebar-right">
456
+ <div class="panel-header">Video Event FX</div>
457
+
458
+ <!-- Brightness -->
459
+ <div class="control-group">
460
+ <div class="control-label">
461
+ <span>Brightness</span>
462
+ <span class="value-display" id="val-brightness">100%</span>
463
+ </div>
464
+ <input type="range" id="brightness" min="0" max="200" value="100" oninput="updateEffect('brightness', this.value)">
465
+ </div>
466
+
467
+ <!-- Contrast -->
468
+ <div class="control-group">
469
+ <div class="control-label">
470
+ <span>Contrast</span>
471
+ <span class="value-display" id="val-contrast">100%</span>
472
  </div>
473
+ <input type="range" id="contrast" min="0" max="200" value="100" oninput="updateEffect('contrast', this.value)">
474
+ </div>
475
 
476
+ <!-- Saturation -->
477
+ <div class="control-group">
478
+ <div class="control-label">
479
+ <span>Saturation</span>
480
+ <span class="value-display" id="val-saturate">100%</span>
 
 
 
 
 
 
 
481
  </div>
482
+ <input type="range" id="saturate" min="0" max="200" value="100" oninput="updateEffect('saturate', this.value)">
483
+ </div>
484
 
485
+ <!-- Hue Rotate -->
486
+ <div class="control-group">
487
+ <div class="control-label">
488
+ <span>Hue Rotate</span>
489
+ <span class="value-display" id="val-hue-rotate">0deg</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  </div>
491
+ <input type="range" id="hue-rotate" min="0" max="360" value="0" oninput="updateEffect('hue-rotate', this.value)">
492
  </div>
493
+
494
+ <!-- Blur -->
495
+ <div class="control-group">
496
+ <div class="control-label">
497
+ <span>Blur</span>
498
+ <span class="value-display" id="val-blur">0px</span>
499
+ </div>
500
+ <input type="range" id="blur" min="0" max="20" value="0" step="0.5" oninput="updateEffect('blur', this.value)">
501
+ </div>
502
+
503
+ <!-- Sepia -->
504
+ <div class="control-group">
505
+ <div class="control-label">
506
+ <span>Sepia</span>
507
+ <span class="value-display" id="val-sepia">0%</span>
508
+ </div>
509
+ <input type="range" id="sepia" min="0" max="100" value="0" oninput="updateEffect('sepia', this.value)">
510
+ </div>
511
+
512
+ <!-- Invert -->
513
+ <div class="control-group">
514
+ <div class="control-label">
515
+ <span>Invert</span>
516
+ <span class="value-display" id="val-invert">0%</span>
517
+ </div>
518
+ <input type="range" id="invert" min="0" max="100" value="0" oninput="updateEffect('invert', this.value)">
519
+ </div>
520
+
521
+ </aside>
522
+
523
+ <!-- Timeline -->
524
+ <section class="timeline-area">
525
+ <div class="timeline-controls">
526
+ <button class="btn" id="playPauseBtn" title="Play/Pause (Space)">
527
+ <i class="fa-solid fa-play"></i>
528
+ </button>
529
+ <button class="btn" id="stopBtn" title="Stop">
530
+ <i class="fa-solid fa-stop"></i>
531
+ </button>
532
+ <div class="time-display" id="timeDisplay">00:00 / 00:00</div>
533
+ <div style="flex:1"></div>
534
+ <button class="btn" id="zoomOutBtn"><i class="fa-solid fa-magnifying-glass-minus"></i></button>
535
+ <button class="btn" id="zoomInBtn"><i class="fa-solid fa-magnifying-glass-plus"></i></button>
536
+ </div>
537
+
538
+ <div class="timeline-tracks" id="timelineTracks">
539
+ <div class="playhead-container">
540
+ <div class="playhead" id="playhead"></div>
541
+ </div>
542
+ <!-- Video Track -->
543
+ <div class="track">
544
+ <div class="clip" id="videoClip" style="width: 0%;">
545
+ <i class="fa-solid fa-film" style="margin-right:5px; opacity:0.5;"></i> Video 1
546
+ </div>
547
+ </div>
548
+ <!-- FX Track (Visual only for this demo) -->
549
+ <div class="track">
550
+ <div class="clip" style="width: 100%; background: #2d2d30; border-left: none; border-right: none; color: #555;">
551
+ <i class="fa-solid fa-wand-magic-sparkles"></i> FX Chain
552
+ </div>
553
+ </div>
554
+ </div>
555
+ </section>
556
  </main>
557
 
558
+ <!-- Toast Notification -->
559
+ <div class="toast" id="toast">Snapshot Saved!</div>
560
 
561
  <script>
562
+ // DOM Elements
563
+ const videoInput = document.getElementById('videoInput');
564
+ const video = document.getElementById('mainVideo');
565
+ const uploadPlaceholder = document.getElementById('uploadPlaceholder');
566
+ const playPauseBtn = document.getElementById('playPauseBtn');
567
+ const stopBtn = document.getElementById('stopBtn');
568
+ const timeDisplay = document.getElementById('timeDisplay');
569
+ const playhead = document.getElementById('playhead');
570
+ const timelineTracks = document.getElementById('timelineTracks');
571
+ const videoClip = document.getElementById('videoClip');
572
+ const toast = document.getElementById('toast');
573
+ const snapshotBtn = document.getElementById('snapshotBtn');
574
+ const videoWrapper = document.getElementById('videoWrapper');
 
 
 
 
 
 
 
 
 
 
 
575
 
576
  // State
577
  let isPlaying = false;
578
+ let effects = {
579
+ brightness: 100,
580
+ contrast: 100,
581
+ saturate: 100,
582
+ 'hue-rotate': 0,
583
+ blur: 0,
584
+ sepia: 0,
585
+ invert: 0
586
+ };
587
+ let isGlitching = false;
 
 
 
 
588
 
589
  // --- Event Listeners ---
590
 
591
+ // Upload Video
592
+ videoInput.addEventListener('change', function(e) {
593
+ const file = e.target.files[0];
594
+ if (file) {
595
+ const fileURL = URL.createObjectURL(file);
596
+ video.src = fileURL;
597
+ video.load();
598
+ uploadPlaceholder.style.display = 'none';
599
+ showToast('Video Loaded Successfully');
600
+ }
601
+ });
602
 
603
+ // Video Events
604
+ video.addEventListener('loadedmetadata', () => {
605
+ updateTimeDisplay();
606
+ videoClip.style.width = '100%'; // Visual representation
607
+ });
608
 
609
+ video.addEventListener('timeupdate', () => {
610
+ updateTimeDisplay();
611
+ updatePlayhead();
612
+ });
613
 
614
+ video.addEventListener('ended', () => {
 
615
  isPlaying = false;
616
+ playPauseBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
 
 
617
  });
618
 
619
+ // Controls
620
+ playPauseBtn.addEventListener('click', togglePlay);
621
+ stopBtn.addEventListener('click', stopVideo);
622
+
623
+ // Timeline Scrubbing
624
+ timelineTracks.addEventListener('click', (e) => {
625
+ if(!video.duration) return;
626
+ const rect = timelineTracks.getBoundingClientRect();
627
+ const offsetX = e.clientX - rect.left;
628
+ const percentage = Math.max(0, Math.min(1, offsetX / rect.width));
629
+ video.currentTime = percentage * video.duration;
630
+ });
631
 
632
+ // Snapshot
633
+ snapshotBtn.addEventListener('click', takeSnapshot);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
 
635
+ // Keyboard Shortcuts
636
+ document.addEventListener('keydown', (e) => {
637
+ if (e.code === 'Space') {
638
+ e.preventDefault();
639
+ togglePlay();
640
  }
641
+ });
642
+
643
+ // --- Functions ---
644
 
645
+ function togglePlay() {
646
+ if (!video.src) return;
647
+ if (video.paused) {
648
+ video.play();
649
+ playPauseBtn.innerHTML = '<i class="fa-solid fa-pause"></i>';
650
  } else {
651
+ video.pause();
652
+ playPauseBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
653
  }
654
  }
655
 
656
+ function stopVideo() {
657
+ video.pause();
658
+ video.currentTime = 0;
659
+ playPauseBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
660
+ }
661
+
662
+ function updateTimeDisplay() {
663
+ const current = formatTime(video.currentTime);
664
+ const total = formatTime(video.duration || 0);
665
+ timeDisplay.textContent = `${current} / ${total}`;
 
666
  }
667
 
668
+ function formatTime(seconds) {
669
+ const m = Math.floor(seconds / 60);
670
+ const s = Math.floor(seconds % 60);
671
+ return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
 
 
 
672
  }
673
 
674
+ function updatePlayhead() {
675
+ if (!video.duration) return;
676
+ const percentage = (video.currentTime / video.duration) * 100;
677
+ playhead.style.left = `${percentage}%`;
678
+ }
679
+
680
+ // --- Effects Logic ---
681
+
682
+ function updateEffect(type, value) {
683
+ effects[type] = value;
684
 
685
+ // Update text display
686
+ const unit = (type === 'blur') ? 'px' : (type === 'hue-rotate' ? 'deg' : '%');
687
+ document.getElementById(`val-${type}`).textContent = `${value}${unit}`;
688
+
689
+ renderEffects();
690
+ }
691
+
692
+ function renderEffects() {
693
+ let filterString = `
694
+ brightness(${effects.brightness}%)
695
+ contrast(${effects.contrast}%)
696
+ saturate(${effects.saturate}%)
697
+ hue-rotate(${effects['hue-rotate']}deg)
698
+ blur(${effects.blur}px)
699
+ sepia(${effects.sepia}%)
700
+ invert(${effects.invert}%)
701
+ `;
702
 
703
+ video.style.filter = filterString;
704
+ }
705
+
706
+ function resetEffect(type) {
707
+ if (type === 'all') {
708
+ // Reset sliders
709
+ for (const key in effects) {
710
+ effects[key] = (key === 'brightness' || key === 'contrast' || key === 'saturate') ? 100 : 0;
711
+ const input = document.getElementById(key);
712
+ if(input) input.value = effects[key];
713
+ updateEffect(key, effects[key]);
714
+ }
715
+ isGlitching = false;
716
+ video.classList.remove('glitch-active');
717
+ showToast('All Effects Reset');
718
+ }
719
  }
720
 
721
+ function setPreset(name) {
722
+ // First reset to base
723
+ resetEffect('all');
724
 
725
+ switch(name) {
726
+ case 'bw':
727
+ updateEffect('saturate', 0);
728
+ document.getElementById('saturate').value = 0;
729
+ break;
730
+ case 'sepia':
731
+ updateEffect('sepia', 100);
732
+ document.getElementById('sepia').value = 100;
733
+ break;
734
+ case 'cyber':
735
+ updateEffect('contrast', 130);
736
+ updateEffect('saturate', 150);
737
+ updateEffect('hue-rotate', 180);
738
+ document.getElementById('contrast').value = 130;
739
+ document.getElementById('saturate').value = 150;
740
+ document.getElementById('hue-rotate').value = 180;
741
+ break;
742
+ case 'invert':
743
+ updateEffect('invert', 100);
744
+ document.getElementById('invert').value = 100;
745
+ break;
746
+ case 'vhs':
747
+ updateEffect('blur', 0.5);
748
+ updateEffect('contrast', 120);
749
+ updateEffect('saturate', 80);
750
+ document.getElementById('blur').value = 0.5;
751
+ document.getElementById('contrast').value = 120;
752
+ document.getElementById('saturate').value = 80;
753
+ break;
754
  }
755
+ showToast(`Preset Applied: ${name.charAt(0).toUpperCase() + name.slice(1)}`);
756
  }
757
 
758
+ function toggleGlitch() {
759
+ isGlitching = !isGlitching;
760
+ if (isGlitching) {
761
+ video.classList.add('glitch-active');
762
+ showToast('Glitch Effect ON');
763
  } else {
764
+ video.classList.remove('glitch-active');
765
+ showToast('Glitch Effect OFF');
766
  }
767
  }
768
 
769
+ // --- Utilities ---
 
 
 
 
 
 
770
 
771
+ function showToast(message) {
772
+ toast.textContent = message;
773
+ toast.classList.add('show');
774
  setTimeout(() => {
775
+ toast.classList.remove('show');
776
  }, 3000);
777
  }
778
 
779
+ function takeSnapshot() {
780
+ if (!video.src || video.readyState < 2) {
781
+ showToast('No video to capture');
782
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
783
  }
 
 
784
 
785
+ // Create a canvas to draw the video frame with filters
786
+ const canvas = document.createElement('canvas');
787
+ canvas.width = video.videoWidth;
788
+ canvas.height = video.videoHeight;
789
+ const ctx = canvas.getContext('2d');
790
 
791
+ // Apply filters to context (Note: ctx.filter matches CSS filter syntax)
792
+ ctx.filter = video.style.filter;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
793
 
794
+ // Draw
795
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
796
+
797
+ // Convert to image and download
798
+ try {
799
+ const dataURL = canvas.toDataURL('image/png');
800
+ const link = document.createElement('a');
801
+ link.download = `snapshot_${Date.now()}.png`;
802
+ link.href = dataURL;
803
+ document.body.appendChild(link);
804
+ link.click();
805
+ document.body.removeChild(link);
806
+ showToast('Snapshot Downloaded!');
807
+ } catch (e) {
808
+ showToast('Error saving snapshot');
809
+ console.error(e);
810
+ }
811
+ }
812
 
813
  </script>
814
  </body>