HI7RAI commited on
Commit
7759e88
·
verified ·
1 Parent(s): 20a83c6

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +541 -696
index.html CHANGED
@@ -4,25 +4,30 @@
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>VisionFX Studio - Mathematische Bildsteuerung</title>
8
 
9
  <!-- Externe Bibliotheken (CDN) -->
10
  <!-- FontAwesome für Icons -->
11
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
12
  <!-- Math.js für Formelverarbeitung -->
13
  <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/11.8.0/math.min.js"></script>
 
 
14
 
15
  <style>
16
  :root {
17
- --bg-dark: #121212;
18
- --bg-panel: #1e1e1e;
 
19
  --primary: #bb86fc;
20
  --secondary: #03dac6;
 
21
  --text-main: #e0e0e0;
22
  --text-muted: #a0a0a0;
23
  --border: #333;
24
- --danger: #cf6679;
25
  --font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
 
 
26
  }
27
 
28
  * {
@@ -42,16 +47,18 @@
42
  overflow: hidden;
43
  }
44
 
45
- /* Header */
46
  header {
47
  background-color: var(--bg-panel);
48
  border-bottom: 1px solid var(--border);
49
- padding: 0.8rem 1.5rem;
50
  display: flex;
51
  justify-content: space-between;
52
  align-items: center;
53
- height: 60px;
54
  flex-shrink: 0;
 
 
55
  }
56
 
57
  .logo {
@@ -61,31 +68,35 @@
61
  display: flex;
62
  align-items: center;
63
  gap: 10px;
 
64
  }
65
 
 
 
66
  .header-actions {
67
  display: flex;
68
- gap: 15px;
69
  }
70
 
71
  .btn {
72
- background-color: var(--bg-dark);
73
  border: 1px solid var(--border);
74
  color: var(--text-main);
75
- padding: 0.5rem 1rem;
76
  border-radius: 4px;
77
  cursor: pointer;
78
- font-size: 0.9rem;
79
  transition: all 0.2s ease;
80
  display: flex;
81
  align-items: center;
82
- gap: 8px;
83
  text-decoration: none;
84
  }
85
 
86
  .btn:hover {
87
  border-color: var(--primary);
88
  color: var(--primary);
 
89
  }
90
 
91
  .btn-primary {
@@ -100,22 +111,50 @@
100
  color: #000;
101
  }
102
 
103
- /* Main Layout */
104
  main {
105
  display: flex;
106
  flex: 1;
107
  overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
 
110
- /* Sidebar - Filter Controls */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  .sidebar {
112
- width: 380px;
113
  background-color: var(--bg-panel);
114
- border-right: 1px solid var(--border);
115
  display: flex;
116
  flex-direction: column;
117
  flex-shrink: 0;
118
- transition: transform 0.3s ease;
119
  z-index: 10;
120
  }
121
 
@@ -126,6 +165,7 @@
126
  display: flex;
127
  justify-content: space-between;
128
  align-items: center;
 
129
  }
130
 
131
  .filter-list {
@@ -134,84 +174,84 @@
134
  padding: 1rem;
135
  }
136
 
 
 
 
 
 
 
137
  .filter-item {
138
- background-color: var(--bg-dark);
139
  border: 1px solid var(--border);
140
  border-radius: 6px;
141
- margin-bottom: 1rem;
142
- padding: 1rem;
143
- transition: border-color 0.2s;
144
  }
145
 
146
  .filter-item.active {
147
  border-color: var(--secondary);
 
148
  }
149
 
150
  .filter-header {
151
  display: flex;
152
  justify-content: space-between;
153
  align-items: center;
154
- margin-bottom: 0.8rem;
155
  }
156
 
157
  .filter-title {
158
- font-size: 0.95rem;
159
  font-weight: 600;
160
- color: var(--secondary);
 
 
 
 
 
 
 
 
 
 
 
 
161
  }
162
 
 
163
  .toggle-switch {
164
  position: relative;
165
  display: inline-block;
166
- width: 34px;
167
- height: 20px;
168
  }
169
-
170
- .toggle-switch input {
171
- opacity: 0;
172
- width: 0;
173
- height: 0;
174
- }
175
-
176
  .slider {
177
  position: absolute;
178
  cursor: pointer;
179
- top: 0;
180
- left: 0;
181
- right: 0;
182
- bottom: 0;
183
  background-color: #444;
184
  transition: .4s;
185
- border-radius: 20px;
186
  }
187
-
188
  .slider:before {
189
  position: absolute;
190
  content: "";
191
- height: 14px;
192
- width: 14px;
193
- left: 3px;
194
- bottom: 3px;
195
  background-color: white;
196
  transition: .4s;
197
  border-radius: 50%;
198
  }
 
 
199
 
200
- input:checked+.slider {
201
- background-color: var(--primary);
202
- }
203
-
204
- input:checked+.slider:before {
205
- transform: translateX(14px);
206
- }
207
-
208
- .control-group {
209
- margin-bottom: 0.8rem;
210
- }
211
-
212
  .control-group label {
213
  display: block;
214
- font-size: 0.8rem;
215
  color: var(--text-muted);
216
  margin-bottom: 4px;
217
  }
@@ -221,20 +261,16 @@
221
  -webkit-appearance: none;
222
  background: transparent;
223
  }
224
-
225
  .range-slider::-webkit-slider-thumb {
226
  -webkit-appearance: none;
227
- height: 14px;
228
- width: 14px;
229
  border-radius: 50%;
230
  background: var(--secondary);
231
  cursor: pointer;
232
  margin-top: -5px;
233
  }
234
-
235
  .range-slider::-webkit-slider-runnable-track {
236
- width: 100%;
237
- height: 4px;
238
  cursor: pointer;
239
  background: #444;
240
  border-radius: 2px;
@@ -242,52 +278,25 @@
242
 
243
  .math-input {
244
  width: 100%;
245
- background-color: #222;
246
  border: 1px solid var(--border);
247
  color: var(--primary);
248
  padding: 6px;
249
  border-radius: 4px;
250
  font-family: 'Courier New', monospace;
251
- font-size: 0.85rem;
252
- }
253
-
254
- .math-input:focus {
255
- border-color: var(--primary);
256
  }
 
257
 
258
  .math-hint {
259
- font-size: 0.7rem;
260
- color: var(--text-muted);
261
- margin-top: 2px;
262
- }
263
-
264
- /* Canvas Area */
265
- .canvas-container {
266
- flex: 1;
267
- background-color: #000;
268
- background-image:
269
- linear-gradient(45deg, #151515 25%, transparent 25%),
270
- linear-gradient(-45deg, #151515 25%, transparent 25%),
271
- linear-gradient(45deg, transparent 75%, #151515 75%),
272
- linear-gradient(-45deg, transparent 75%, #151515 75%);
273
- background-size: 20px 20px;
274
- background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
275
- display: flex;
276
- justify-content: center;
277
- align-items: center;
278
- position: relative;
279
- overflow: hidden;
280
  }
281
 
282
- canvas {
283
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
284
- max-width: 95%;
285
- max-height: 95%;
286
- }
287
-
288
- /* Footer / Timeline */
289
  .timeline {
290
- height: 50px;
291
  background-color: var(--bg-panel);
292
  border-top: 1px solid var(--border);
293
  display: flex;
@@ -295,92 +304,65 @@
295
  padding: 0 1rem;
296
  justify-content: space-between;
297
  }
 
 
298
 
299
- .playback-controls {
300
- display: flex;
301
- gap: 10px;
302
- align-items: center;
303
- }
304
-
305
- .time-display {
306
- font-family: monospace;
307
- color: var(--secondary);
308
- }
309
-
310
- /* Mobile Responsive */
311
- @media (max-width: 768px) {
312
- .sidebar {
313
- position: absolute;
314
- height: 100%;
315
- transform: translateX(-100%);
316
- }
317
-
318
- .sidebar.open {
319
- transform: translateX(0);
320
- }
321
-
322
- .menu-toggle {
323
- display: block;
324
- }
325
- }
326
-
327
- /* Helper Classes */
328
- .hidden {
329
- display: none;
330
- }
331
-
332
- /* Loading Overlay */
333
  #loading {
334
- position: absolute;
335
- top: 0;
336
- left: 0;
337
- right: 0;
338
- bottom: 0;
339
- background: rgba(0, 0, 0, 0.8);
340
  color: white;
341
- display: flex;
342
- justify-content: center;
343
- align-items: center;
344
- z-index: 100;
345
- flex-direction: column;
346
- gap: 15px;
347
  }
348
-
349
  .spinner {
350
- width: 40px;
351
- height: 40px;
352
  border: 4px solid rgba(255, 255, 255, 0.1);
353
  border-top: 4px solid var(--primary);
354
  border-radius: 50%;
355
  animation: spin 1s linear infinite;
356
  }
 
357
 
358
- @keyframes spin {
359
- 0% { transform: rotate(0deg); }
360
- 100% { transform: rotate(360deg); }
 
 
 
361
  }
 
362
 
363
- .drop-zone {
 
364
  position: absolute;
365
- top: 20px;
366
- left: 20px;
367
- right: 20px;
368
- bottom: 20px;
369
- border: 2px dashed var(--text-muted);
370
- display: flex;
371
- justify-content: center;
372
- align-items: center;
373
- flex-direction: column;
374
- color: var(--text-muted);
 
375
  pointer-events: none;
376
  opacity: 0;
377
  transition: opacity 0.3s;
378
  }
 
379
 
380
- .canvas-container.drag-over .drop-zone {
381
- opacity: 1;
382
- background: rgba(0, 0, 0, 0.5);
383
- pointer-events: all;
 
 
 
 
384
  }
385
  </style>
386
  </head>
@@ -393,55 +375,60 @@
393
  <i class="fa-solid fa-layer-group"></i> VisionFX Studio
394
  </div>
395
  <div class="header-actions">
396
- <button class="btn" id="toggleSidebarBtn" title="Einstellungen">
397
- <i class="fa-solid fa-sliders"></i>
398
  </button>
399
  <button class="btn" onclick="document.getElementById('fileInput').click()">
400
- <i class="fa-solid fa-upload"></i> Import
401
  </button>
402
  <input type="file" id="fileInput" class="hidden" accept="image/*,video/*">
403
  <button class="btn btn-primary" id="exportBtn">
404
- <i class="fa-solid fa-download"></i> Export
405
  </button>
406
  </div>
407
  </header>
408
 
409
  <!-- Main Content -->
410
  <main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  <!-- Sidebar -->
412
  <aside class="sidebar" id="sidebar">
413
  <div class="sidebar-header">
414
- <span>Effekte & Filter</span>
415
- <button class="btn" style="padding: 2px 8px; font-size: 0.8rem;" id="resetAllBtn">Reset</button>
416
  </div>
417
 
418
  <div class="filter-list" id="filterList">
419
  <!-- Filter items werden hier per JS generiert -->
420
  </div>
421
 
422
- <div style="padding: 1rem; border-top: 1px solid var(--border); font-size: 0.8rem; color: var(--text-muted);">
423
- <p><strong>Verfügbare Variablen (Math.js):</strong></p>
424
- <ul style="margin-left: 20px; margin-top: 5px;">
425
- <li><code>t</code>: Zeit in ms (für Animation)</li>
426
- <li><code>x</code>: Horizontaler Wert (0-1)</li>
427
- <li><code>v</code>: Slider Wert</li>
428
  </ul>
429
  <p style="margin-top: 5px;">Beispiel: <code>sin(t/500) * v</code></p>
430
  </div>
431
  </aside>
432
-
433
- <!-- Canvas Area -->
434
- <div class="canvas-container" id="canvasWrapper">
435
- <div id="loading" class="hidden">
436
- <div class="spinner"></div>
437
- <span>Verarbeite...</span>
438
- </div>
439
- <div class="drop-zone" id="dropZone">
440
- <i class="fa-solid fa-cloud-arrow-down fa-3x"></i>
441
- <p style="margin-top: 10px;">Datei hier ablegen</p>
442
- </div>
443
- <canvas id="mainCanvas"></canvas>
444
- </div>
445
  </main>
446
 
447
  <!-- Timeline / Playback -->
@@ -452,518 +439,376 @@
452
  <span class="time-display" id="timeDisplay">00:00:00</span>
453
  </div>
454
  <div>
455
- <span style="font-size: 0.8rem; color: var(--text-muted);">Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: var(--primary); text-decoration: none;">anycoder</a></span>
 
 
456
  </div>
457
  </div>
458
 
459
  <script>
460
  /**
461
- * VisionFX Studio - Core Logic
462
- * Verwendet Canvas API für Rendering und Math.js für mathematische Steuerung.
463
- */
464
-
465
- // --- Konfiguration & State ---
466
- const config = {
467
- width: 1920,
468
- height: 1080,
469
- bgColor: '#000000'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  };
471
-
472
- const state = {
473
- isPlaying: false,
474
- startTime: 0,
475
- elapsedTime: 0,
476
- mediaType: 'none', // 'image' | 'video'
477
- mediaSource: null, // Image Objekt oder Video Element
478
- animationId: null,
479
- filters: {} // Speichert aktuelle Filterzustände
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  };
 
 
 
 
 
481
 
482
- // --- DOM Elemente ---
483
- const canvasEl = document.getElementById('mainCanvas');
484
- const ctx = canvasEl.getContext('2d', { willReadFrequently: true });
485
- const filterListEl = document.getElementById('filterList');
486
- const fileInput = document.getElementById('fileInput');
487
- const playPauseBtn = document.getElementById('playPauseBtn');
488
- const stopBtn = document.getElementById('stopBtn');
489
- const timeDisplay = document.getElementById('timeDisplay');
490
- const loadingEl = document.getElementById('loading');
491
- const dropZone = document.getElementById('dropZone');
492
- const canvasWrapper = document.getElementById('canvasWrapper');
493
- const sidebar = document.getElementById('sidebar');
494
-
495
- // --- Filter Registry (Alle bekannten Effekte) ---
496
- // Definiert die Struktur für UI-Generierung und Logik
497
- const filterRegistry = [
498
- {
499
- id: 'brightness',
500
- name: 'Helligkeit',
501
- type: 'basic',
502
- min: -100, max: 100, val: 0,
503
- formula: 'v',
504
- apply: (ctx, val) => { ctx.filter = `brightness(${100 + val}%)`; }
505
- },
506
- {
507
- id: 'contrast',
508
- name: 'Kontrast',
509
- type: 'basic',
510
- min: -100, max: 100, val: 0,
511
- formula: 'v',
512
- apply: (ctx, val) => { ctx.filter = `contrast(${100 + val}%)`; }
513
- },
514
- {
515
- id: 'grayscale',
516
- name: 'Schwarz Weiß',
517
- type: 'basic',
518
- min: 0, max: 100, val: 0,
519
- formula: 'v',
520
- apply: (ctx, val) => { ctx.filter = `grayscale(${val}%)`; }
521
- },
522
- {
523
- id: 'sepia',
524
- name: 'Sepia',
525
- type: 'basic',
526
- min: 0, max: 100, val: 0,
527
- formula: 'v',
528
- apply: (ctx, val) => { ctx.filter = `sepia(${val}%)`; }
529
- },
530
- {
531
- id: 'hueRotate',
532
- name: 'Farbrotation (Soul)',
533
- type: 'basic',
534
- min: 0, max: 360, val: 0,
535
- formula: 'v',
536
- apply: (ctx, val) => { ctx.filter = `hue-rotate(${val}deg)`; }
537
- },
538
- {
539
- id: 'invert',
540
- name: 'Invertieren',
541
- type: 'basic',
542
- min: 0, max: 100, val: 0,
543
- formula: 'v',
544
- apply: (ctx, val) => { ctx.filter = `invert(${val}%)`; }
545
- },
546
- {
547
- id: 'blur',
548
- name: 'Weichzeichner',
549
- type: 'basic',
550
- min: 0, max: 20, val: 0,
551
- formula: 'v',
552
- apply: (ctx, val) => { ctx.filter = `blur(${val}px)`; }
553
- },
554
- {
555
- id: 'pixelate',
556
- name: 'Pixelate (8-Bit)',
557
- type: 'custom',
558
- min: 1, max: 50, val: 1,
559
- formula: 'v',
560
- apply: (ctx, val, width, height) => {
561
- if(val <= 1) return;
562
- // Pixelate Trick: Skaliere runter und zeichne mit smoothing=false
563
- const w = width / val;
564
- const h = height / val;
565
- // Wir speichern das aktuelle canvas image, manipulieren es und geben es zurück?
566
- // Da dies ein Post-Process ist, machen wir es im Haupt-Loop.
567
- }
568
- },
569
- {
570
- id: 'glitch',
571
- name: 'Glitch Verzerrung',
572
- type: 'anim',
573
- min: 0, max: 100, val: 0,
574
- formula: 'v', // oder 'sin(t/100)*v' für automatisches Glitch
575
- apply: (ctx, val, w, h, t) => {
576
- // Wird im Render Loop manuell angewendet
577
- }
578
- },
579
- {
580
- id: 'strobe',
581
- name: 'Strobe Light',
582
- type: 'anim',
583
- min: 0, max: 100, val: 0,
584
- formula: 'sin(t/50) * v', // Schnelles Blitzen
585
- apply: (ctx, val) => {
586
- // Wird im Overlay angewendet
587
- }
588
- },
589
- {
590
- id: 'scanlines',
591
- name: 'Video Scanlines',
592
- type: 'overlay',
593
- min: 0, max: 100, val: 0,
594
- formula: 'v',
595
- apply: (ctx, val) => {}
596
- },
597
- {
598
- id: 'vignette',
599
- name: 'Vignette',
600
- type: 'overlay',
601
- min: 0, max: 100, val: 0,
602
- formula: 'v',
603
- apply: (ctx, val) => {}
604
- },
605
- {
606
- id: 'rgbSplit',
607
- name: 'RGB Shift (Chroma)',
608
- type: 'custom',
609
- min: 0, max: 50, val: 0,
610
- formula: 'v',
611
- apply: (ctx, val) => {}
612
- }
613
- ];
614
-
615
- // --- Initialisierung ---
616
- function init() {
617
- // Setup Events
618
- fileInput.addEventListener('change', handleFileSelect);
619
- playPauseBtn.addEventListener('click', togglePlay);
620
- stopBtn.addEventListener('click', stopMedia);
621
- document.getElementById('toggleSidebarBtn').addEventListener('click', () => {
622
- sidebar.classList.toggle('open');
623
- });
624
- document.getElementById('resetAllBtn').addEventListener('click', resetFilters);
625
- document.getElementById('exportBtn').addEventListener('click', exportMedia);
626
-
627
- // Drag & Drop
628
- canvasWrapper.addEventListener('dragover', (e) => { e.preventDefault(); canvasWrapper.classList.add('drag-over'); });
629
- canvasWrapper.addEventListener('dragleave', () => canvasWrapper.classList.remove('drag-over'));
630
- canvasWrapper.addEventListener('drop', handleDrop);
631
-
632
- // UI Generierung
633
- generateFilterUI();
634
-
635
- // Canvas Startzustand
636
- canvasEl.width = config.width;
637
- canvasEl.height = config.height;
638
- ctx.fillStyle = config.bgColor;
639
- ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
640
-
641
- // Start Render Loop
642
- requestAnimationFrame(renderLoop);
643
-
644
- // Libraries loggen
645
- console.log("Verfügbare Libraries:");
646
- console.log("Math.js:", typeof math !== 'undefined');
647
- }
648
-
649
- // --- UI Generierung ---
650
- function generateFilterUI() {
651
- filterListEl.innerHTML = '';
652
-
653
- filterRegistry.forEach(f => {
654
- const item = document.createElement('div');
655
- item.className = 'filter-item';
656
- item.id = `filter-${f.id}`;
657
-
658
- item.innerHTML = `
659
- <div class="filter-header">
660
- <span class="filter-title">${f.name}</span>
661
- <label class="toggle-switch">
662
- <input type="checkbox" class="filter-toggle" data-id="${f.id}">
663
- <span class="slider"></span>
664
- </label>
665
- </div>
666
- <div class="control-group">
667
- <label>Intensität (Slider)</label>
668
- <input type="range" class="range-slider param-slider"
669
- data-id="${f.id}" min="${f.min}" max="${f.max}" value="${f.val}" step="0.1">
670
- </div>
671
- <div class="control-group">
672
- <label>Math.js Formel (t=Zeit, v=Wert)</label>
673
- <input type="text" class="math-input param-formula"
674
- data-id="${f.id}" value="${f.formula}">
675
- <div class="math-hint">z.B. sin(t/1000) * 50</div>
676
- </div>
677
- `;
678
-
679
- filterListEl.appendChild(item);
680
-
681
- // State Initialisierung
682
- state.filters[f.id] = {
683
- active: false,
684
- value: f.val,
685
- formula: f.formula,
686
- computedValue: f.val
687
- };
688
-
689
- // Event Listeners für Controls
690
- const toggle = item.querySelector('.filter-toggle');
691
- const slider = item.querySelector('.param-slider');
692
- const formulaInput = item.querySelector('.param-formula');
693
-
694
- toggle.addEventListener('change', (e) => {
695
- state.filters[f.id].active = e.target.checked;
696
- item.classList.toggle('active', e.target.checked);
697
- });
698
-
699
- slider.addEventListener('input', (e) => {
700
- state.filters[f.id].value = parseFloat(e.target.value);
701
- });
702
-
703
- formulaInput.addEventListener('input', (e) => {
704
- state.filters[f.id].formula = e.target.value;
705
- });
706
- });
707
- }
708
-
709
- function resetFilters() {
710
- document.querySelectorAll('.filter-toggle').forEach(el => el.checked = false);
711
- Object.keys(state.filters).forEach(key => {
712
- state.filters[key].active = false;
713
- state.filters[key].value = filterRegistry.find(f => f.id === key).val;
714
- document.querySelector(`.filter-item#filter-${key}`).classList.remove('active');
715
- document.querySelector(`.filter-item#filter-${key} .param-slider`).value = state.filters[key].value;
716
- });
717
- }
718
-
719
- // --- Math Engine ---
720
- function evaluateFilterFormula(filterState, time) {
721
- try {
722
- // Scope: t (zeit in ms), v (slider wert)
723
- const scope = {
724
- t: time,
725
- v: filterState.value,
726
- x: Math.random(), // Zufall für Glitch
727
- pi: Math.PI,
728
- e: Math.E
729
- };
730
- const result = math.evaluate(filterState.formula, scope);
731
- return result;
732
- } catch (err) {
733
- // Fallback bei Syntaxfehler
734
- return filterState.value;
735
- }
736
- }
737
-
738
- // --- File Handling ---
739
- function handleFileSelect(e) {
740
- const file = e.target.files[0];
741
- if (file) loadMedia(file);
742
- }
743
-
744
- function handleDrop(e) {
745
- e.preventDefault();
746
- canvasWrapper.classList.remove('drag-over');
747
- if (e.dataTransfer.files.length) {
748
- loadMedia(e.dataTransfer.files[0]);
749
- }
750
- }
751
-
752
- function loadMedia(file) {
753
- loadingEl.classList.remove('hidden');
754
- const url = URL.createObjectURL(file);
755
-
756
- if (file.type.startsWith('image/')) {
757
- const img = new Image();
758
- img.onload = () => {
759
- state.mediaType = 'image';
760
- state.mediaSource = img;
761
- // Canvas Größe anpassen
762
- canvasEl.width = img.width;
763
- canvasEl.height = img.height;
764
- loadingEl.classList.add('hidden');
765
- renderFrame(); // Einmal rendern
766
- };
767
- img.src = url;
768
- } else if (file.type.startsWith('video/')) {
769
- const video = document.createElement('video');
770
- video.src = url;
771
- video.muted = true;
772
- video.loop = true;
773
- video.onloadeddata = () => {
774
- state.mediaType = 'video';
775
- state.mediaSource = video;
776
- canvasEl.width = video.videoWidth;
777
- canvasEl.height = video.videoHeight;
778
- loadingEl.classList.add('hidden');
779
- video.play();
780
- state.isPlaying = true;
781
- updatePlayIcon();
782
- };
783
- }
784
- }
785
-
786
- // --- Rendering Engine ---
787
- function togglePlay() {
788
- if (state.mediaType !== 'video') return;
789
-
790
- if (state.isPlaying) {
791
- state.mediaSource.pause();
792
- state.isPlaying = false;
793
- } else {
794
- state.mediaSource.play();
795
- state.isPlaying = true;
796
- }
797
- updatePlayIcon();
798
- }
799
-
800
- function stopMedia() {
801
- if (state.mediaType === 'video') {
802
- state.mediaSource.pause();
803
- state.mediaSource.currentTime = 0;
804
- state.isPlaying = false;
805
- }
806
- state.elapsedTime = 0;
807
- updatePlayIcon();
808
- }
809
-
810
- function updatePlayIcon() {
811
- playPauseBtn.innerHTML = state.isPlaying ? '<i class="fa-solid fa-pause"></i>' : '<i class="fa-solid fa-play"></i>';
812
- }
813
-
814
- function renderLoop(timestamp) {
815
- if (!state.startTime) state.startTime = timestamp;
816
- const current = timestamp - state.startTime;
817
-
818
- if (state.isPlaying || state.mediaType === 'image') {
819
- renderFrame(timestamp);
820
- }
821
-
822
- // Update Zeit Anzeige
823
- if (state.mediaType === 'video' && state.mediaSource) {
824
- const secs = state.mediaSource.currentTime;
825
- const m = Math.floor(secs / 60).toString().padStart(2, '0');
826
- const s = Math.floor(secs % 60).toString().padStart(2, '0');
827
- const ms = Math.floor((secs % 1) * 100).toString().padStart(2, '0');
828
- timeDisplay.innerText = `${m}:${s}:${ms}`;
829
- }
830
-
831
- requestAnimationFrame(renderLoop);
832
- }
833
-
834
- function renderFrame(timestamp = 0) {
835
- // 1. Berechne alle Werte basierend auf Formeln
836
- filterRegistry.forEach(f => {
837
- const s = state.filters[f.id];
838
- if (s.active) {
839
- s.computedValue = evaluateFilterFormula(s, timestamp);
840
- } else {
841
- s.computedValue = 0; // oder neutraler wert
842
- }
843
- });
844
-
845
- // 2. Canvas Reset
846
- ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
847
- ctx.save();
848
-
849
- // 3. Basis-Filter (CSS Context Filter) anwenden
850
- // Wir sammeln alle 'basic' Filter
851
- let basicFilterString = '';
852
-
853
- const brightness = state.filters['brightness'].active ? `brightness(${100 + state.filters['brightness'].computedValue}%)` : `brightness(100%)`;
854
- const contrast = state.filters['contrast'].active ? `contrast(${100 + state.filters['contrast'].computedValue}%)` : `contrast(100%)`;
855
- const grayscale = state.filters['grayscale'].active ? `grayscale(${state.filters['grayscale'].computedValue}%)` : `grayscale(0%)`;
856
- const sepia = state.filters['sepia'].active ? `sepia(${state.filters['sepia'].computedValue}%)` : `sepia(0%)`;
857
- const hue = state.filters['hueRotate'].active ? `hue-rotate(${state.filters['hueRotate'].computedValue}deg)` : `hue-rotate(0deg)';
858
- const invert = state.filters['invert'].active ? `invert(${state.filters['invert'].computedValue}%)` : `invert(0%)`;
859
- const blur = state.filters['blur'].active ? `blur(${state.filters['blur'].computedValue}px)` : `blur(0px)';
860
-
861
- ctx.filter = `${brightness} ${contrast} ${grayscale} ${sepia} ${hue} ${invert} ${blur}`;
862
-
863
- // 4. Pixelate (Special Handling vor dem Zeichnen)
864
- const pixelateVal = state.filters['pixelate'].computedValue;
865
-
866
- if (state.mediaSource) {
867
- if (state.mediaType === 'image') {
868
- if (pixelateVal > 1) {
869
- // Pixelate Effekt durch Downscaling
870
- const w = canvasEl.width / pixelateVal;
871
- const h = canvasEl.height / pixelateVal;
872
- // Zeichne verkleinert in Offscreen Canvas (oder direkt hier mit Trick)
873
- // Einfacher Trick: imageSmoothingEnabled = false und skalierung
874
- ctx.imageSmoothingEnabled = false;
875
- ctx.drawImage(state.mediaSource, 0, 0, w, h);
876
- ctx.drawImage(canvasEl, 0, 0, w, h, 0, 0, canvasEl.width, canvasEl.height);
877
- } else {
878
- ctx.drawImage(state.mediaSource, 0, 0);
879
- }
880
- } else if (state.mediaType === 'video') {
881
- if (pixelateVal > 1) {
882
- const w = canvasEl.width / pixelateVal;
883
- const h = canvasEl.height / pixelateVal;
884
- ctx.imageSmoothingEnabled = false;
885
- ctx.drawImage(state.mediaSource, 0, 0, w, h);
886
- ctx.drawImage(canvasEl, 0, 0, w, h, 0, 0, canvasEl.width, canvasEl.height);
887
- } else {
888
- ctx.drawImage(state.mediaSource, 0, 0, canvasEl.width, canvasEl.height);
889
- }
890
- }
891
- }
892
-
893
- ctx.restore(); // Filter resetten für Overlays
894
-
895
- // 5. Custom Effekte & Overlays (Pixel Manipulation / Compositing)
896
-
897
- // RGB Split
898
- if (state.filters['rgbSplit'].active && state.filters['rgbSplit'].computedValue > 0) {
899
- const offset = state.filters['rgbSplit'].computedValue;
900
- const w = canvasEl.width;
901
- const h = canvasEl.height;
902
-
903
- // Erstelle临时 Kopien
904
- // Da wir in JS keine performante Pixel-Manipulation pro Frame ohne WebGL wollen,
905
- // nutzen wir Composite Operations für einen schnellen RGB Shift Trick
906
- ctx.save();
907
- ctx.globalCompositeOperation = 'screen';
908
-
909
- // Rot Kanal simulieren (tint)
910
- // Echter RGB Shift braucht imageData, was langsam ist. Wir machen einen "Shift" Versatz.
911
- // Zeichne Canvas leicht versetzt mit Colorize (via Filter) wäre besser, aber komplex.
912
- // Wir nutzen hier einen einfachen Slice-Shift Trick:
913
- ctx.globalAlpha = 0.5;
914
- ctx.translate(offset, 0);
915
- ctx.drawImage(canvasEl, 0, 0); // Rot-ish shift
916
- ctx.translate(-offset*2, 0);
917
- ctx.drawImage(canvasEl, 0, 0); // Cyan-ish shift
918
- ctx.restore();
919
- }
920
-
921
- // Strobe Flash
922
- if (state.filters['strobe'].active) {
923
- const val = state.filters['strobe'].computedValue; // -100 bis 100
924
- if (val > 10) {
925
- ctx.fillStyle = `rgba(255, 255, 255, ${Math.min(val / 100, 1)})`;
926
- ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
927
- } else if (val < -10) {
928
- ctx.fillStyle = `rgba(0, 0, 0, ${Math.min(Math.abs(val) / 100, 1)})`;
929
- ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
930
- }
931
- }
932
-
933
- // Scanlines Overlay
934
- if (state.filters['scanlines'].active) {
935
- const opacity = state.filters['scanlines'].computedValue / 100;
936
- ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`;
937
- for (let y = 0; y < canvasEl.height; y += 4) {
938
- ctx.fillRect(0, y, canvasEl.width, 2);
939
- }
940
- }
941
-
942
- // Vignette
943
- if (state.filters['vignette'].active) {
944
- const val = state.filters['vignette'].computedValue;
945
- const gradient = ctx.createRadialGradient(
946
- canvasEl.width / 2, canvasEl.height / 2, canvasEl.height / 3,
947
- canvasEl.width / 2, canvasEl.height / 2, canvasEl.height
948
- );
949
- gradient.addColorStop(0, "rgba(0,0,0,0)");
950
- gradient.addColorStop(1, `rgba(0,0,0,${val / 100})`);
951
- ctx.fillStyle = gradient;
952
- ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
953
- }
954
-
955
- // Glitch (Horizontal Slices)
956
- if (state.filters['glitch'].active) {
957
- const intensity = state.filters['glitch'].computedValue;
958
- if (intensity > 0.1 && Math.random() < 0.2) { // Nur manchmal triggern für Glitch Look
959
- const sliceHeight = Math.random() * 50 + 10;
960
- const sliceY = Math.random() * canvasEl.height;
961
- const offset = (Math.random() - 0.5) * intensity * 4;
962
-
963
- try {
964
- // Slice ausschneiden
965
- const sliceData = ctx.getImageData(0, sliceY, canvasEl.width, sliceHeight);
966
- // Slice versetzt wieder zeichnen
967
- ctx.putImageData(sliceData, offset, sliceY);
968
- } catch(e) {
969
- // Ignore cross-origin
 
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>VisionFX Studio Pro - WebGL & Math Engine</title>
8
 
9
  <!-- Externe Bibliotheken (CDN) -->
10
  <!-- FontAwesome für Icons -->
11
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
12
  <!-- Math.js für Formelverarbeitung -->
13
  <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/11.8.0/math.min.js"></script>
14
+ <!-- glfx.js für WebGL Bildverarbeitung (Filter, Verzerrung) -->
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/glfx/0.0.4/glfx.min.js"></script>
16
 
17
  <style>
18
  :root {
19
+ --bg-dark: #0f0f13;
20
+ --bg-panel: #1a1a20;
21
+ --bg-input: #25252e;
22
  --primary: #bb86fc;
23
  --secondary: #03dac6;
24
+ --accent: #cf6679;
25
  --text-main: #e0e0e0;
26
  --text-muted: #a0a0a0;
27
  --border: #333;
 
28
  --font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
29
+ --header-height: 60px;
30
+ --footer-height: 50px;
31
  }
32
 
33
  * {
 
47
  overflow: hidden;
48
  }
49
 
50
+ /* --- Header --- */
51
  header {
52
  background-color: var(--bg-panel);
53
  border-bottom: 1px solid var(--border);
54
+ padding: 0 1.5rem;
55
  display: flex;
56
  justify-content: space-between;
57
  align-items: center;
58
+ height: var(--header-height);
59
  flex-shrink: 0;
60
+ z-index: 20;
61
+ box-shadow: 0 2px 10px rgba(0,0,0,0.3);
62
  }
63
 
64
  .logo {
 
68
  display: flex;
69
  align-items: center;
70
  gap: 10px;
71
+ letter-spacing: 0.5px;
72
  }
73
 
74
+ .logo i { color: var(--secondary); }
75
+
76
  .header-actions {
77
  display: flex;
78
+ gap: 10px;
79
  }
80
 
81
  .btn {
82
+ background-color: var(--bg-input);
83
  border: 1px solid var(--border);
84
  color: var(--text-main);
85
+ padding: 0.4rem 0.8rem;
86
  border-radius: 4px;
87
  cursor: pointer;
88
+ font-size: 0.85rem;
89
  transition: all 0.2s ease;
90
  display: flex;
91
  align-items: center;
92
+ gap: 6px;
93
  text-decoration: none;
94
  }
95
 
96
  .btn:hover {
97
  border-color: var(--primary);
98
  color: var(--primary);
99
+ background-color: #2f2f3a;
100
  }
101
 
102
  .btn-primary {
 
111
  color: #000;
112
  }
113
 
114
+ /* --- Main Layout --- */
115
  main {
116
  display: flex;
117
  flex: 1;
118
  overflow: hidden;
119
+ position: relative;
120
+ }
121
+
122
+ /* --- Canvas Area --- */
123
+ .canvas-container {
124
+ flex: 1;
125
+ background-color: #000;
126
+ background-image:
127
+ radial-gradient(circle at center, #1a1a2e 0%, #000 100%);
128
+ display: flex;
129
+ justify-content: center;
130
+ align-items: center;
131
+ position: relative;
132
+ overflow: hidden;
133
  }
134
 
135
+ /* Canvas Wrapper to handle scaling */
136
+ .canvas-wrapper {
137
+ box-shadow: 0 0 30px rgba(0, 0, 0, 0.6);
138
+ max-width: 95%;
139
+ max-height: 95%;
140
+ position: relative;
141
+ }
142
+
143
+ canvas {
144
+ display: block;
145
+ max-width: 100%;
146
+ max-height: 100%;
147
+ }
148
+
149
+ /* --- Sidebar --- */
150
  .sidebar {
151
+ width: 400px;
152
  background-color: var(--bg-panel);
153
+ border-left: 1px solid var(--border);
154
  display: flex;
155
  flex-direction: column;
156
  flex-shrink: 0;
157
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
158
  z-index: 10;
159
  }
160
 
 
165
  display: flex;
166
  justify-content: space-between;
167
  align-items: center;
168
+ background: rgba(26, 26, 32, 0.95);
169
  }
170
 
171
  .filter-list {
 
174
  padding: 1rem;
175
  }
176
 
177
+ /* Scrollbar Styling */
178
+ .filter-list::-webkit-scrollbar { width: 6px; }
179
+ .filter-list::-webkit-scrollbar-track { background: var(--bg-dark); }
180
+ .filter-list::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
181
+
182
+ /* --- Filter Item UI --- */
183
  .filter-item {
184
+ background-color: var(--bg-input);
185
  border: 1px solid var(--border);
186
  border-radius: 6px;
187
+ margin-bottom: 12px;
188
+ padding: 12px;
189
+ transition: all 0.2s;
190
  }
191
 
192
  .filter-item.active {
193
  border-color: var(--secondary);
194
+ box-shadow: 0 0 10px rgba(3, 218, 198, 0.1);
195
  }
196
 
197
  .filter-header {
198
  display: flex;
199
  justify-content: space-between;
200
  align-items: center;
201
+ margin-bottom: 10px;
202
  }
203
 
204
  .filter-title {
205
+ font-size: 0.9rem;
206
  font-weight: 600;
207
+ color: var(--text-main);
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 8px;
211
+ }
212
+
213
+ .filter-type-badge {
214
+ font-size: 0.6rem;
215
+ padding: 2px 4px;
216
+ border-radius: 2px;
217
+ background: #333;
218
+ color: #888;
219
+ text-transform: uppercase;
220
  }
221
 
222
+ /* Toggle Switch */
223
  .toggle-switch {
224
  position: relative;
225
  display: inline-block;
226
+ width: 32px;
227
+ height: 18px;
228
  }
229
+ .toggle-switch input { opacity: 0; width: 0; height: 0; }
 
 
 
 
 
 
230
  .slider {
231
  position: absolute;
232
  cursor: pointer;
233
+ top: 0; left: 0; right: 0; bottom: 0;
 
 
 
234
  background-color: #444;
235
  transition: .4s;
236
+ border-radius: 18px;
237
  }
 
238
  .slider:before {
239
  position: absolute;
240
  content: "";
241
+ height: 12px; width: 12px;
242
+ left: 3px; bottom: 3px;
 
 
243
  background-color: white;
244
  transition: .4s;
245
  border-radius: 50%;
246
  }
247
+ input:checked + .slider { background-color: var(--primary); }
248
+ input:checked + .slider:before { transform: translateX(14px); }
249
 
250
+ /* Inputs */
251
+ .control-group { margin-bottom: 8px; }
 
 
 
 
 
 
 
 
 
 
252
  .control-group label {
253
  display: block;
254
+ font-size: 0.75rem;
255
  color: var(--text-muted);
256
  margin-bottom: 4px;
257
  }
 
261
  -webkit-appearance: none;
262
  background: transparent;
263
  }
 
264
  .range-slider::-webkit-slider-thumb {
265
  -webkit-appearance: none;
266
+ height: 14px; width: 14px;
 
267
  border-radius: 50%;
268
  background: var(--secondary);
269
  cursor: pointer;
270
  margin-top: -5px;
271
  }
 
272
  .range-slider::-webkit-slider-runnable-track {
273
+ width: 100%; height: 4px;
 
274
  cursor: pointer;
275
  background: #444;
276
  border-radius: 2px;
 
278
 
279
  .math-input {
280
  width: 100%;
281
+ background-color: #15151a;
282
  border: 1px solid var(--border);
283
  color: var(--primary);
284
  padding: 6px;
285
  border-radius: 4px;
286
  font-family: 'Courier New', monospace;
287
+ font-size: 0.8rem;
288
+ margin-top: 4px;
 
 
 
289
  }
290
+ .math-input:focus { border-color: var(--secondary); }
291
 
292
  .math-hint {
293
+ font-size: 0.7rem; color: var(--text-muted); margin-top: 2px;
294
+ display: flex; justify-content: space-between;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  }
296
 
297
+ /* --- Footer / Timeline --- */
 
 
 
 
 
 
298
  .timeline {
299
+ height: var(--footer-height);
300
  background-color: var(--bg-panel);
301
  border-top: 1px solid var(--border);
302
  display: flex;
 
304
  padding: 0 1rem;
305
  justify-content: space-between;
306
  }
307
+ .playback-controls { display: flex; gap: 10px; align-items: center; }
308
+ .time-display { font-family: monospace; color: var(--secondary); font-size: 0.9rem; }
309
 
310
+ /* --- Components --- */
311
+ .hidden { display: none !important; }
312
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  #loading {
314
+ position: absolute; top: 0; left: 0; right: 0; bottom: 0;
315
+ background: rgba(15, 15, 19, 0.9);
 
 
 
 
316
  color: white;
317
+ display: flex; justify-content: center; align-items: center;
318
+ z-index: 50; flex-direction: column; gap: 15px;
 
 
 
 
319
  }
 
320
  .spinner {
321
+ width: 40px; height: 40px;
 
322
  border: 4px solid rgba(255, 255, 255, 0.1);
323
  border-top: 4px solid var(--primary);
324
  border-radius: 50%;
325
  animation: spin 1s linear infinite;
326
  }
327
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
328
 
329
+ .drop-zone {
330
+ position: absolute; top: 20px; left: 20px; right: 20px; bottom: 20px;
331
+ border: 2px dashed var(--text-muted);
332
+ display: flex; justify-content: center; align-items: center;
333
+ flex-direction: column; color: var(--text-muted);
334
+ pointer-events: none; opacity: 0; transition: opacity 0.3s;
335
  }
336
+ .canvas-container.drag-over .drop-zone { opacity: 1; background: rgba(0, 0, 0, 0.5); pointer-events: all; }
337
 
338
+ /* --- Double Exposure Badge --- */
339
+ .badge-double-exp {
340
  position: absolute;
341
+ top: 10px; right: 10px;
342
+ background: rgba(255, 255, 255, 0.1);
343
+ backdrop-filter: blur(4px);
344
+ color: var(--primary);
345
+ padding: 4px 8px;
346
+ font-size: 0.7rem;
347
+ font-weight: bold;
348
+ border: 1px solid var(--primary);
349
+ border-radius: 2px;
350
+ text-transform: uppercase;
351
+ letter-spacing: 1px;
352
  pointer-events: none;
353
  opacity: 0;
354
  transition: opacity 0.3s;
355
  }
356
+ .badge-double-exp.visible { opacity: 1; }
357
 
358
+ /* --- Responsive --- */
359
+ @media (max-width: 768px) {
360
+ .sidebar {
361
+ position: absolute; height: 100%; transform: translateX(100%);
362
+ box-shadow: -5px 0 15px rgba(0,0,0,0.5);
363
+ }
364
+ .sidebar.open { transform: translateX(0); }
365
+ .header-actions span { display: none; } /* Hide button text on mobile */
366
  }
367
  </style>
368
  </head>
 
375
  <i class="fa-solid fa-layer-group"></i> VisionFX Studio
376
  </div>
377
  <div class="header-actions">
378
+ <button class="btn" id="toggleSidebarBtn" title="Effekte">
379
+ <i class="fa-solid fa-sliders"></i> <span>Effekte</span>
380
  </button>
381
  <button class="btn" onclick="document.getElementById('fileInput').click()">
382
+ <i class="fa-solid fa-upload"></i> <span>Import</span>
383
  </button>
384
  <input type="file" id="fileInput" class="hidden" accept="image/*,video/*">
385
  <button class="btn btn-primary" id="exportBtn">
386
+ <i class="fa-solid fa-download"></i> <span>Export</span>
387
  </button>
388
  </div>
389
  </header>
390
 
391
  <!-- Main Content -->
392
  <main>
393
+ <!-- Canvas Area -->
394
+ <div class="canvas-container" id="canvasWrapper">
395
+ <div id="loading" class="hidden">
396
+ <div class="spinner"></div>
397
+ <span id="loadingText">Verarbeite...</span>
398
+ </div>
399
+ <div class="drop-zone" id="dropZone">
400
+ <i class="fa-solid fa-cloud-arrow-down fa-3x"></i>
401
+ <p style="margin-top: 10px;">Datei hier ablegen (Bild/Video)</p>
402
+ </div>
403
+
404
+ <div class="canvas-wrapper">
405
+ <div id="doubleExpBadge" class="badge-double-exp">Double Exposure Mode</div>
406
+ <!-- Canvas wird hier dynamisch erstellt -->
407
+ <canvas id="mainCanvas"></canvas>
408
+ </div>
409
+ </div>
410
+
411
  <!-- Sidebar -->
412
  <aside class="sidebar" id="sidebar">
413
  <div class="sidebar-header">
414
+ <span><i class="fa-solid fa-wave-square"></i> FX Rack</span>
415
+ <button class="btn" style="padding: 2px 8px; font-size: 0.75rem;" id="resetAllBtn">Reset</button>
416
  </div>
417
 
418
  <div class="filter-list" id="filterList">
419
  <!-- Filter items werden hier per JS generiert -->
420
  </div>
421
 
422
+ <div style="padding: 1rem; border-top: 1px solid var(--border); font-size: 0.75rem; color: var(--text-muted); background: var(--bg-panel);">
423
+ <p><strong>Math.js Variablen:</strong></p>
424
+ <ul style="margin-left: 20px; margin-top: 5px; list-style: none;">
425
+ <li><code style="color:var(--secondary)">t</code>: Zeit (ms)</li>
426
+ <li><code style="color:var(--secondary)">v</code>: Slider-Wert</li>
427
+ <li><code style="color:var(--secondary)">x,y</code>: Zufall (0-1)</li>
428
  </ul>
429
  <p style="margin-top: 5px;">Beispiel: <code>sin(t/500) * v</code></p>
430
  </div>
431
  </aside>
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  </main>
433
 
434
  <!-- Timeline / Playback -->
 
439
  <span class="time-display" id="timeDisplay">00:00:00</span>
440
  </div>
441
  <div>
442
+ <span style="font-size: 0.75rem; color: var(--text-muted);">
443
+ Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: var(--primary); text-decoration: none; font-weight: bold;">anycoder</a>
444
+ </span>
445
  </div>
446
  </div>
447
 
448
  <script>
449
  /**
450
+ * VisionFX Studio Pro - Core Logic
451
+ * Hybrid Rendering: WebGL (glfx.js) für Filter, Canvas 2D für Overlays/UI
452
+ */
453
+
454
+ // --- Konfiguration & State ---
455
+ const config = {
456
+ width: 1280,
457
+ height: 720,
458
+ bgColor: '#000000'
459
+ };
460
+
461
+ const state = {
462
+ isPlaying: false,
463
+ startTime: 0,
464
+ elapsedTime: 0,
465
+ mediaType: 'none', // 'image' | 'video'
466
+ mediaSource: null, // Image Objekt oder Video Element
467
+ animationId: null,
468
+ filters: {}, // Speichert aktuelle Filterzustände
469
+ canvas: null, // Der Haupt-Canvas
470
+ texture: null, // WebGL Texture
471
+ fxCanvas: null, // WebGL Context (glfx)
472
+ ctx: null // 2D Fallback (für Overlays)
473
+ };
474
+
475
+ // --- DOM Elemente ---
476
+ const canvasWrapper = document.getElementById('canvasWrapper');
477
+ const filterListEl = document.getElementById('filterList');
478
+ const fileInput = document.getElementById('fileInput');
479
+ const playPauseBtn = document.getElementById('playPauseBtn');
480
+ const stopBtn = document.getElementById('stopBtn');
481
+ const timeDisplay = document.getElementById('timeDisplay');
482
+ const loadingEl = document.getElementById('loading');
483
+ const dropZone = document.getElementById('dropZone');
484
+ const sidebar = document.getElementById('sidebar');
485
+ const doubleExpBadge = document.getElementById('doubleExpBadge');
486
+
487
+ // --- Filter Registry (Auto-Discovery Simulation) ---
488
+ // Wir definieren hier, welche Effekte verfügbar sind.
489
+ // In einer echten "Discovery" Umgebung würde man die Objekte von glfx.js scannen.
490
+ const filterRegistry = [
491
+ // --- WebGL Filter (glfx.js) ---
492
+ {
493
+ id: 'brightnessContrast',
494
+ name: 'Helligkeit & Kontrast',
495
+ type: 'webgl',
496
+ params: [
497
+ { name: 'brightness', min: -1, max: 1, def: 0, label: 'Helligkeit' },
498
+ { name: 'contrast', min: -1, max: 1, def: 0, label: 'Kontrast' }
499
+ ]
500
+ },
501
+ {
502
+ id: 'hueSaturation',
503
+ name: 'Farbe & Sättigung',
504
+ type: 'webgl',
505
+ params: [
506
+ { name: 'hue', min: -1, max: 1, def: 0, label: 'Farbrotation' },
507
+ { name: 'saturation', min: -1, max: 1, def: 0, label: 'Sättigung' }
508
+ ]
509
+ },
510
+ {
511
+ id: 'vignette',
512
+ name: 'Vignette',
513
+ type: 'webgl',
514
+ params: [
515
+ { name: 'size', min: 0, max: 1, def: 0.5, label: 'Größe' },
516
+ { name: 'amount', min: 0, max: 1, def: 0.5, label: 'Intensität' }
517
+ ]
518
+ },
519
+ {
520
+ id: 'blur',
521
+ name: 'Weichzeichner (Gaussian)',
522
+ type: 'webgl',
523
+ params: [
524
+ { name: 'strength', min: 0, max: 20, def: 0, label: 'Stärke' }
525
+ ]
526
+ },
527
+ {
528
+ id: 'noise',
529
+ name: 'Rauschen (Film Grain)',
530
+ type: 'webgl',
531
+ params: [
532
+ { name: 'amount', min: 0, max: 1, def: 0, label: 'Menge' }
533
+ ]
534
+ },
535
+ {
536
+ id: 'denoise',
537
+ name: 'Rauschentfernung',
538
+ type: 'webgl',
539
+ params: [
540
+ { name: 'strength', min: 0, max: 1, def: 0, label: 'Stärke' }
541
+ ]
542
+ },
543
+ {
544
+ id: 'sepia',
545
+ name: 'Sepia',
546
+ type: 'webgl',
547
+ params: [
548
+ { name: 'amount', min: 0, max: 1, def: 0, label: 'Menge' }
549
+ ]
550
+ },
551
+ {
552
+ id: 'hexagonalPixelate',
553
+ name: 'Hexagon Pixelate',
554
+ type: 'webgl',
555
+ params: [
556
+ { name: 'scale', min: 2, max: 50, def: 10, label: 'Blockgröße' }
557
+ ]
558
+ },
559
+ {
560
+ id: 'ink',
561
+ name: 'Tinte / Kanten',
562
+ type: 'webgl',
563
+ params: [
564
+ { name: 'strength', min: 0, max: 1, def: 0.25, label: 'Stärke' }
565
+ ]
566
+ },
567
+ {
568
+ id: 'swirl',
569
+ name: 'Wirbel Verzerrung',
570
+ type: 'webgl',
571
+ params: [
572
+ { name: 'strength', min: -10, max: 10, def: 0, label: 'Stärke' },
573
+ { name: 'radius', min: 0, max: 500, def: 200, label: 'Radius' }
574
+ ]
575
+ },
576
+ {
577
+ id: 'bulgePinch',
578
+ name: 'Wölbung',
579
+ type: 'webgl',
580
+ params: [
581
+ { name: 'strength', min: -1, max: 1, def: 0, label: 'Stärke' },
582
+ { name: 'radius', min: 0, max: 500, def: 200, label: 'Radius' }
583
+ ]
584
+ },
585
+ // --- Custom / Hybrid Effekte ---
586
+ {
587
+ id: 'glitch',
588
+ name: 'Digital Glitch',
589
+ type: 'custom',
590
+ params: [
591
+ { name: 'amount', min: 0, max: 100, def: 0, label: 'Intensität' }
592
+ ]
593
+ },
594
+ {
595
+ id: 'rgbSplit',
596
+ name: 'RGB Shift (Chroma)',
597
+ type: 'custom',
598
+ params: [
599
+ { name: 'offset', min: 0, max: 50, def: 0, label: 'Versatz' }
600
+ ]
601
+ },
602
+ {
603
+ id: 'scanlines',
604
+ name: 'TV Scanlines',
605
+ type: 'custom',
606
+ params: [
607
+ { name: 'opacity', min: 0, max: 1, def: 0.3, label: 'Deckkraft' }
608
+ ]
609
+ },
610
+ {
611
+ id: 'doubleExposure',
612
+ name: 'Double Exposure',
613
+ type: 'custom',
614
+ params: [
615
+ { name: 'mix', min: 0, max: 1, def: 0, label: 'Mischung' }
616
+ ]
617
+ }
618
+ ];
619
+
620
+ // --- Initialisierung ---
621
+ function init() {
622
+ // Canvas Setup
623
+ state.canvas = document.getElementById('mainCanvas');
624
+ state.ctx = state.canvas.getContext('2d');
625
+
626
+ // Initial WebGL Canvas vorbereiten (wird beim ersten Media Load aktiviert)
627
+ if (typeof fx !== 'undefined') {
628
+ state.fxCanvas = fx.canvas();
629
+ }
630
+
631
+ // Events
632
+ fileInput.addEventListener('change', handleFileSelect);
633
+ playPauseBtn.addEventListener('click', togglePlay);
634
+ stopBtn.addEventListener('click', stopMedia);
635
+ document.getElementById('toggleSidebarBtn').addEventListener('click', () => sidebar.classList.toggle('open'));
636
+ document.getElementById('resetAllBtn').addEventListener('click', resetFilters);
637
+ document.getElementById('exportBtn').addEventListener('click', exportMedia);
638
+
639
+ // Drag & Drop
640
+ canvasWrapper.addEventListener('dragover', (e) => { e.preventDefault(); canvasWrapper.classList.add('drag-over'); });
641
+ canvasWrapper.addEventListener('dragleave', () => canvasWrapper.classList.remove('drag-over'));
642
+ canvasWrapper.addEventListener('drop', handleDrop);
643
+
644
+ // UI Generierung
645
+ generateFilterUI();
646
+
647
+ // Canvas Startzustand
648
+ resizeCanvas(800, 600);
649
+ state.ctx.fillStyle = config.bgColor;
650
+ state.ctx.fillRect(0, 0, state.canvas.width, state.canvas.height);
651
+
652
+ // Start Render Loop
653
+ requestAnimationFrame(renderLoop);
654
+
655
+ console.log("VisionFX initialized. Libraries: Math.js, glfx.js");
656
+ }
657
+
658
+ // --- UI Generierung (Dynamic) ---
659
+ function generateFilterUI() {
660
+ filterListEl.innerHTML = '';
661
+
662
+ filterRegistry.forEach(f => {
663
+ const item = document.createElement('div');
664
+ item.className = 'filter-item';
665
+ item.id = `filter-${f.id}`;
666
+
667
+ // Typ Badge
668
+ const typeLabel = f.type === 'webgl' ? 'GPU' : 'CPU';
669
+
670
+ let paramsHtml = '';
671
+ f.params.forEach(p => {
672
+ paramsHtml += `
673
+ <div class="control-group">
674
+ <label>${p.label}</label>
675
+ <input type="range" class="range-slider param-slider"
676
+ data-filter="${f.id}" data-param="${p.name}"
677
+ min="${p.min}" max="${p.max}" value="${p.def}" step="0.01">
678
+ <div class="math-hint">
679
+ <span>Val: <span class="val-display" id="val-${f.id}-${p.name}">${p.def}</span></span>
680
+ </div>
681
+ <input type="text" class="math-input param-formula"
682
+ data-filter="${f.id}" data-param="${p.name}"
683
+ placeholder="Formel (z.B. sin(t)*v)" value="">
684
+ </div>
685
+ `;
686
+ });
687
+
688
+ item.innerHTML = `
689
+ <div class="filter-header">
690
+ <div class="filter-title">
691
+ ${f.name} <span class="filter-type-badge">${typeLabel}</span>
692
+ </div>
693
+ <label class="toggle-switch">
694
+ <input type="checkbox" class="filter-toggle" data-id="${f.id}">
695
+ <span class="slider"></span>
696
+ </label>
697
+ </div>
698
+ ${paramsHtml}
699
+ `;
700
+
701
+ filterListEl.appendChild(item);
702
+
703
+ // State Initialisierung
704
+ state.filters[f.id] = {
705
+ active: false,
706
+ type: f.type,
707
+ params: {}
708
  };
709
+
710
+ f.params.forEach(p => {
711
+ state.filters[f.id].params[p.name] = {
712
+ value: p.def,
713
+ formula: '',
714
+ computed: p.def
715
+ };
716
+ });
717
+
718
+ // Event Listeners
719
+ const toggle = item.querySelector('.filter-toggle');
720
+ toggle.addEventListener('change', (e) => {
721
+ state.filters[f.id].active = e.target.checked;
722
+ item.classList.toggle('active', e.target.checked);
723
+
724
+ // Spezielles Handling für Double Exposure Badge
725
+ if(f.id === 'doubleExposure') {
726
+ doubleExpBadge.classList.toggle('visible', e.target.checked);
727
+ }
728
+ });
729
+
730
+ // Sliders
731
+ item.querySelectorAll('.param-slider').forEach(slider => {
732
+ slider.addEventListener('input', (e) => {
733
+ const pName = e.target.dataset.param;
734
+ state.filters[f.id].params[pName].value = parseFloat(e.target.value);
735
+ document.getElementById(`val-${f.id}-${pName}`).innerText = parseFloat(e.target.value).toFixed(2);
736
+ });
737
+ });
738
+
739
+ // Formulas
740
+ item.querySelectorAll('.param-formula').forEach(input => {
741
+ input.addEventListener('input', (e) => {
742
+ const pName = e.target.dataset.param;
743
+ state.filters[f.id].params[pName].formula = e.target.value;
744
+ });
745
+ });
746
+ });
747
+ }
748
+
749
+ function resetFilters() {
750
+ document.querySelectorAll('.filter-toggle').forEach(el => el.checked = false);
751
+ document.querySelectorAll('.filter-item').forEach(el => el.classList.remove('active'));
752
+ doubleExpBadge.classList.remove('visible');
753
+
754
+ Object.keys(state.filters).forEach(key => {
755
+ state.filters[key].active = false;
756
+ const defParams = filterRegistry.find(f => f.id === key).params;
757
+ defParams.forEach(p => {
758
+ state.filters[key].params[p.name].value = p.def;
759
+ state.filters[key].params[p.name].formula = '';
760
+ // Update UI
761
+ const slider = document.querySelector(`.param-slider[data-filter="${key}"][data-param="${p.name}"]`);
762
+ const display = document.getElementById(`val-${key}-${p.name}`);
763
+ const formula = document.querySelector(`.param-formula[data-filter="${key}"][data-param="${p.name}"]`);
764
+ if(slider) slider.value = p.def;
765
+ if(display) display.innerText = p.def;
766
+ if(formula) formula.value = '';
767
+ });
768
+ });
769
+ }
770
+
771
+ // --- Math Engine ---
772
+ function evaluateFormula(formula, time, sliderVal) {
773
+ if (!formula || formula.trim() === '') return sliderVal;
774
+
775
+ try {
776
+ const scope = {
777
+ t: time,
778
+ v: sliderVal,
779
+ x: Math.random(),
780
+ y: Math.random(),
781
+ pi: Math.PI
782
  };
783
+ return math.evaluate(formula, scope);
784
+ } catch (err) {
785
+ return sliderVal; // Fallback bei Fehler
786
+ }
787
+ }
788
 
789
+ // --- File Handling ---
790
+ function handleFileSelect(e) {
791
+ const file = e.target.files[0];
792
+ if (file) loadMedia(file);
793
+ }
794
+
795
+ function handleDrop(e) {
796
+ e.preventDefault();
797
+ canvasWrapper.classList.remove('drag-over');
798
+ if (e.dataTransfer.files.length) loadMedia(e.dataTransfer.files[0]);
799
+ }
800
+
801
+ function loadMedia(file) {
802
+ loadingEl.classList.remove('hidden');
803
+ const url = URL.createObjectURL(file);
804
+
805
+ if (file.type.startsWith('image/')) {
806
+ const img = new Image();
807
+ img.onload = () => {
808
+ state.mediaType = 'image';
809
+ state.mediaSource = img;
810
+ setupCanvasForMedia(img.width, img.height);
811
+ state.texture = state.fxCanvas.texture(img);
812
+ loadingEl.classList.add('hidden');
813
+ renderFrame(0); // Init render
814
+ };