anycoder-90441144 / index.html
HI7RAI's picture
Upload folder using huggingface_hub
6848f40 verified
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VisionFX Studio - Pro Bild & Video Editor</title>
<!-- Externe Bibliotheken (CDN) -->
<!-- FontAwesome für Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Math.js für Formelverarbeitung -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/11.8.0/math.min.js"></script>
<!-- Fabric.js für Canvas Manipulation -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
<style>
:root {
--bg-dark: #121212;
--bg-panel: #1e1e1e;
--primary: #bb86fc;
--secondary: #03dac6;
--text-main: #e0e0e0;
--text-muted: #a0a0a0;
--border: #333;
--danger: #cf6679;
--font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
outline: none;
}
body {
font-family: var(--font-family);
background-color: var(--bg-dark);
color: var(--text-main);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header */
header {
background-color: var(--bg-panel);
border-bottom: 1px solid var(--border);
padding: 0.8rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
flex-shrink: 0;
}
.logo {
font-size: 1.2rem;
font-weight: 700;
color: var(--primary);
display: flex;
align-items: center;
gap: 10px;
}
.header-actions {
display: flex;
gap: 15px;
}
.btn {
background-color: var(--bg-dark);
border: 1px solid var(--border);
color: var(--text-main);
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.btn-primary {
background-color: var(--primary);
color: #000;
border: none;
font-weight: 600;
}
.btn-primary:hover {
background-color: #a370db;
color: #000;
}
/* Main Layout */
main {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar - Filter Controls */
.sidebar {
width: 380px;
background-color: var(--bg-panel);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
transition: transform 0.3s ease;
z-index: 10;
}
.sidebar-header {
padding: 1rem;
border-bottom: 1px solid var(--border);
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.filter-list {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.filter-item {
background-color: var(--bg-dark);
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 1rem;
padding: 1rem;
transition: border-color 0.2s;
}
.filter-item.active {
border-color: var(--secondary);
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.8rem;
}
.filter-title {
font-size: 0.95rem;
font-weight: 600;
color: var(--secondary);
}
.toggle-switch {
position: relative;
display: inline-block;
width: 34px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #444;
transition: .4s;
border-radius: 20px;
}
.slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--primary);
}
input:checked + .slider:before {
transform: translateX(14px);
}
.control-group {
margin-bottom: 0.8rem;
}
.control-group label {
display: block;
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 4px;
}
.range-slider {
width: 100%;
-webkit-appearance: none;
background: transparent;
}
.range-slider::-webkit-slider-thumb {
-webkit-appearance: none;
height: 14px;
width: 14px;
border-radius: 50%;
background: var(--secondary);
cursor: pointer;
margin-top: -5px;
}
.range-slider::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: #444;
border-radius: 2px;
}
.math-input {
width: 100%;
background-color: #222;
border: 1px solid var(--border);
color: var(--primary);
padding: 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
}
.math-input:focus {
border-color: var(--primary);
}
.math-hint {
font-size: 0.7rem;
color: var(--text-muted);
margin-top: 2px;
}
/* Canvas Area */
.canvas-container {
flex: 1;
background-color: #000;
background-image:
linear-gradient(45deg, #151515 25%, transparent 25%),
linear-gradient(-45deg, #151515 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #151515 75%),
linear-gradient(-45deg, transparent 75%, #151515 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
canvas {
box-shadow: 0 0 20px rgba(0,0,0,0.5);
max-width: 95%;
max-height: 95%;
}
/* Footer / Timeline */
.timeline {
height: 50px;
background-color: var(--bg-panel);
border-top: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 1rem;
justify-content: space-between;
}
.playback-controls {
display: flex;
gap: 10px;
align-items: center;
}
.time-display {
font-family: monospace;
color: var(--secondary);
}
/* Mobile Responsive */
@media (max-width: 768px) {
.sidebar {
position: absolute;
height: 100%;
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
.menu-toggle {
display: block;
}
}
/* Helper Classes */
.hidden { display: none; }
/* Loading Overlay */
#loading {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.8);
color: white;
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
flex-direction: column;
gap: 15px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255,255,255,0.1);
border-top: 4px solid var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.drop-zone {
position: absolute;
top: 20px; left: 20px; right: 20px; bottom: 20px;
border: 2px dashed var(--text-muted);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: var(--text-muted);
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
}
.canvas-container.drag-over .drop-zone {
opacity: 1;
background: rgba(0,0,0,0.5);
pointer-events: all;
}
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="logo">
<i class="fa-solid fa-layer-group"></i> VisionFX Studio
</div>
<div class="header-actions">
<button class="btn" id="toggleSidebarBtn" title="Einstellungen">
<i class="fa-solid fa-sliders"></i>
</button>
<button class="btn" onclick="document.getElementById('fileInput').click()">
<i class="fa-solid fa-upload"></i> Import
</button>
<input type="file" id="fileInput" class="hidden" accept="image/*,video/*">
<button class="btn btn-primary" id="exportBtn">
<i class="fa-solid fa-download"></i> Export
</button>
</div>
</header>
<!-- Main Content -->
<main>
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<span>Effekte & Filter</span>
<button class="btn" style="padding: 2px 8px; font-size: 0.8rem;" id="resetAllBtn">Reset</button>
</div>
<div class="filter-list" id="filterList">
<!-- Filter items werden hier per JS generiert -->
</div>
<div style="padding: 1rem; border-top: 1px solid var(--border); font-size: 0.8rem; color: var(--text-muted);">
<p><strong>Verfügbare Variablen (Math.js):</strong></p>
<ul style="margin-left: 20px; margin-top: 5px;">
<li><code>t</code>: Zeit in ms (für Animation)</li>
<li><code>x</code>: Horizontaler Wert (0-1)</li>
<li><code>v</code>: Slider Wert</li>
</ul>
<p style="margin-top: 5px;">Beispiel: <code>sin(t/500) * v</code></p>
</div>
</aside>
<!-- Canvas Area -->
<div class="canvas-container" id="canvasWrapper">
<div id="loading" class="hidden">
<div class="spinner"></div>
<span>Verarbeite...</span>
</div>
<div class="drop-zone" id="dropZone">
<i class="fa-solid fa-cloud-arrow-down fa-3x"></i>
<p style="margin-top: 10px;">Datei hier ablegen</p>
</div>
<canvas id="mainCanvas"></canvas>
</div>
</main>
<!-- Timeline / Playback -->
<div class="timeline">
<div class="playback-controls">
<button class="btn" id="playPauseBtn"><i class="fa-solid fa-play"></i></button>
<button class="btn" id="stopBtn"><i class="fa-solid fa-stop"></i></button>
<span class="time-display" id="timeDisplay">00:00:00</span>
</div>
<div>
<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>
</div>
</div>
<script>
/**
* VisionFX Studio - Core Logic
* Verwendet Fabric.js für Basis-Canvas-Operationen,
* aber implementiert einen benutzerdefinierten Render-Loop für "Math.js" Effekte.
*/
// --- Konfiguration & State ---
const config = {
width: 1920,
height: 1080,
bgColor: '#000000'
};
const state = {
isPlaying: false,
startTime: 0,
elapsedTime: 0,
mediaType: 'none', // 'image' | 'video'
mediaSource: null, // Image Objekt oder Video Element
animationId: null,
filters: {} // Speichert aktuelle Filterzustände
};
// --- DOM Elemente ---
const canvasEl = document.getElementById('mainCanvas');
const ctx = canvasEl.getContext('2d');
const filterListEl = document.getElementById('filterList');
const fileInput = document.getElementById('fileInput');
const playPauseBtn = document.getElementById('playPauseBtn');
const stopBtn = document.getElementById('stopBtn');
const timeDisplay = document.getElementById('timeDisplay');
const loadingEl = document.getElementById('loading');
const dropZone = document.getElementById('dropZone');
const canvasWrapper = document.getElementById('canvasWrapper');
const sidebar = document.getElementById('sidebar');
// --- Filter Registry (Alle bekannten Effekte) ---
// Definiert die Struktur für UI-Generierung und Logik
const filterRegistry = [
{
id: 'brightness',
name: 'Helligkeit',
type: 'basic',
min: -100, max: 100, val: 0,
formula: 'v',
apply: (ctx, val) => { ctx.filter = `brightness(${100 + val}%)`; }
},
{
id: 'contrast',
name: 'Kontrast',
type: 'basic',
min: -100, max: 100, val: 0,
formula: 'v',
apply: (ctx, val) => { ctx.filter = `contrast(${100 + val}%)`; }
},
{
id: 'grayscale',
name: 'Schwarz Weiß',
type: 'basic',
min: 0, max: 100, val: 0,
formula: 'v',
apply: (ctx, val) => { ctx.filter = `grayscale(${val}%)`; }
},
{
id: 'sepia',
name: 'Sepia',
type: 'basic',
min: 0, max: 100, val: 0,
formula: 'v',
apply: (ctx, val) => { ctx.filter = `sepia(${val}%)`; }
},
{
id: 'hueRotate',
name: 'Farbrotation (Soul)',
type: 'basic',
min: 0, max: 360, val: 0,
formula: 'v',
apply: (ctx, val) => { ctx.filter = `hue-rotate(${val}deg)`; }
},
{
id: 'invert',
name: 'Invertieren',
type: 'basic',
min: 0, max: 100, val: 0,
formula: 'v',
apply: (ctx, val) => { ctx.filter = `invert(${val}%)`; }
},
{
id: 'blur',
name: 'Weichzeichner',
type: 'basic',
min: 0, max: 20, val: 0,
formula: 'v',
apply: (ctx, val) => { ctx.filter = `blur(${val}px)`; }
},
{
id: 'pixelate',
name: 'Pixelate (8-Bit)',
type: 'custom',
min: 1, max: 50, val: 1,
formula: 'v',
apply: (ctx, val, width, height) => {
if(val <= 1) return;
// Pixelate Trick: Skaliere runter und zeichne mit smoothing=false
const w = width / val;
const h = height / val;
// Wir speichern das aktuelle canvas image, manipulieren es und geben es zurück?
// Da dies ein Post-Process ist, machen wir es im Haupt-Loop.
}
},
{
id: 'glitch',
name: 'Glitch Verzerrung',
type: 'anim',
min: 0, max: 100, val: 0,
formula: 'v', // oder 'sin(t/100)*v' für automatisches Glitch
apply: (ctx, val, w, h, t) => {
// Wird im Render Loop manuell angewendet
}
},
{
id: 'strobe',
name: 'Strobe Light',
type: 'anim',
min: 0, max: 100, val: 0,
formula: 'sin(t/50) * v', // Schnelles Blitzen
apply: (ctx, val) => {
// Wird im Overlay angewendet
}
},
{
id: 'scanlines',
name: 'Video Scanlines',
type: 'overlay',
min: 0, max: 100, val: 0,
formula: 'v',
apply: (ctx, val) => {}
},
{
id: 'vignette',
name: 'Vignette',
type: 'overlay',
min: 0, max: 100, val: 0,
formula: 'v',
apply: (ctx, val) => {}
},
{
id: 'rgbSplit',
name: 'RGB Shift (Chroma)',
type: 'custom',
min: 0, max: 50, val: 0,
formula: 'v',
apply: (ctx, val) => {}
}
];
// --- Initialisierung ---
function init() {
// Setup Events
fileInput.addEventListener('change', handleFileSelect);
playPauseBtn.addEventListener('click', togglePlay);
stopBtn.addEventListener('click', stopMedia);
document.getElementById('toggleSidebarBtn').addEventListener('click', () => {
sidebar.classList.toggle('open');
});
document.getElementById('resetAllBtn').addEventListener('click', resetFilters);
document.getElementById('exportBtn').addEventListener('click', exportMedia);
// Drag & Drop
canvasWrapper.addEventListener('dragover', (e) => { e.preventDefault(); canvasWrapper.classList.add('drag-over'); });
canvasWrapper.addEventListener('dragleave', () => canvasWrapper.classList.remove('drag-over'));
canvasWrapper.addEventListener('drop', handleDrop);
// UI Generierung
generateFilterUI();
// Canvas Startzustand
canvasEl.width = config.width;
canvasEl.height = config.height;
ctx.fillStyle = config.bgColor;
ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
// Start Render Loop
requestAnimationFrame(renderLoop);
// Libraries loggen
console.log("Verfügbare Libraries:");
console.log("Fabric.js:", typeof fabric !== 'undefined');
console.log("Math.js:", typeof math !== 'undefined');
}
// --- UI Generierung ---
function generateFilterUI() {
filterListEl.innerHTML = '';
filterRegistry.forEach(f => {
const item = document.createElement('div');
item.className = 'filter-item';
item.id = `filter-${f.id}`;
item.innerHTML = `
<div class="filter-header">
<span class="filter-title">${f.name}</span>
<label class="toggle-switch">
<input type="checkbox" class="filter-toggle" data-id="${f.id}">
<span class="slider"></span>
</label>
</div>
<div class="control-group">
<label>Intensität (Slider)</label>
<input type="range" class="range-slider param-slider"
data-id="${f.id}" min="${f.min}" max="${f.max}" value="${f.val}" step="0.1">
</div>
<div class="control-group">
<label>Math.js Formel (t=Zeit, v=Wert)</label>
<input type="text" class="math-input param-formula"
data-id="${f.id}" value="${f.formula}">
<div class="math-hint">z.B. sin(t/1000) * 50</div>
</div>
`;
filterListEl.appendChild(item);
// State Initialisierung
state.filters[f.id] = {
active: false,
value: f.val,
formula: f.formula,
computedValue: f.val
};
// Event Listeners für Controls
const toggle = item.querySelector('.filter-toggle');
const slider = item.querySelector('.param-slider');
const formulaInput = item.querySelector('.param-formula');
toggle.addEventListener('change', (e) => {
state.filters[f.id].active = e.target.checked;
item.classList.toggle('active', e.target.checked);
});
slider.addEventListener('input', (e) => {
state.filters[f.id].value = parseFloat(e.target.value);
});
formulaInput.addEventListener('input', (e) => {
state.filters[f.id].formula = e.target.value;
});
});
}
function resetFilters() {
document.querySelectorAll('.filter-toggle').forEach(el => el.checked = false);
Object.keys(state.filters).forEach(key => {
state.filters[key].active = false;
state.filters[key].value = filterRegistry.find(f => f.id === key).val;
document.querySelector(`.filter-item#filter-${key}`).classList.remove('active');
document.querySelector(`.filter-item#filter-${key} .param-slider`).value = state.filters[key].value;
});
}
// --- Math Engine ---
function evaluateFilterFormula(filterState, time) {
try {
// Scope: t (zeit in ms), v (slider wert)
const scope = {
t: time,
v: filterState.value,
x: Math.random(), // Zufall für Glitch
pi: Math.PI,
e: Math.E
};
const result = math.evaluate(filterState.formula, scope);
return result;
} catch (err) {
// Fallback bei Syntaxfehler
return filterState.value;
}
}
// --- File Handling ---
function handleFileSelect(e) {
const file = e.target.files[0];
if (file) loadMedia(file);
}
function handleDrop(e) {
e.preventDefault();
canvasWrapper.classList.remove('drag-over');
if (e.dataTransfer.files.length) {
loadMedia(e.dataTransfer.files[0]);
}
}
function loadMedia(file) {
loadingEl.classList.remove('hidden');
const url = URL.createObjectURL(file);
if (file.type.startsWith('image/')) {
const img = new Image();
img.onload = () => {
state.mediaType = 'image';
state.mediaSource = img;
// Canvas Größe anpassen
canvasEl.width = img.width;
canvasEl.height = img.height;
loadingEl.classList.add('hidden');
renderFrame(); // Einmal rendern
};
img.src = url;
} else if (file.type.startsWith('video/')) {
const video = document.createElement('video');
video.src = url;
video.muted = true;
video.loop = true;
video.onloadeddata = () => {
state.mediaType = 'video';
state.mediaSource = video;
canvasEl.width = video.videoWidth;
canvasEl.height = video.videoHeight;
loadingEl.classList.add('hidden');
video.play();
state.isPlaying = true;
updatePlayIcon();
};
}
}
// --- Rendering Engine ---
function togglePlay() {
if (state.mediaType !== 'video') return;
if (state.isPlaying) {
state.mediaSource.pause();
state.isPlaying = false;
} else {
state.mediaSource.play();
state.isPlaying = true;
}
updatePlayIcon();
}
function stopMedia() {
if (state.mediaType === 'video') {
state.mediaSource.pause();
state.mediaSource.currentTime = 0;
state.isPlaying = false;
}
state.elapsedTime = 0;
updatePlayIcon();
}
function updatePlayIcon() {
playPauseBtn.innerHTML = state.isPlaying ? '<i class="fa-solid fa-pause"></i>' : '<i class="fa-solid fa-play"></i>';
}
function renderLoop(timestamp) {
if (!state.startTime) state.startTime = timestamp;
const current = timestamp - state.startTime;
if (state.isPlaying || state.mediaType === 'image') {
renderFrame(timestamp);
}
// Update Zeit Anzeige
if (state.mediaType === 'video' && state.mediaSource) {
const secs = state.mediaSource.currentTime;
const m = Math.floor(secs / 60).toString().padStart(2, '0');
const s = Math.floor(secs % 60).toString().padStart(2, '0');
const ms = Math.floor((secs % 1) * 100).toString().padStart(2, '0');
timeDisplay.innerText = `${m}:${s}:${ms}`;
}
requestAnimationFrame(renderLoop);
}
function renderFrame(timestamp = 0) {
// 1. Berechne alle Werte basierend auf Formeln
filterRegistry.forEach(f => {
const s = state.filters[f.id];
if (s.active) {
s.computedValue = evaluateFilterFormula(s, timestamp);
} else {
s.computedValue = 0; // oder neutraler wert
}
});
// 2. Canvas Reset
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
ctx.save();
// 3. Basis-Filter (CSS Context Filter) anwenden
// Wir sammeln alle 'basic' Filter
let basicFilterString = '';
const brightness = state.filters['brightness'].active ? `brightness(${100 + state.filters['brightness'].computedValue}%)` : `brightness(100%)`;
const contrast = state.filters['contrast'].active ? `contrast(${100 + state.filters['contrast'].computedValue}%)` : `contrast(100%)`;
const grayscale = state.filters['grayscale'].active ? `grayscale(${state.filters['grayscale'].computedValue}%)` : `grayscale(0%)`;
const sepia = state.filters['sepia'].active ? `sepia(${state.filters['sepia'].computedValue}%)` : `sepia(0%)`;
const hue = state.filters['hueRotate'].active ? `hue-rotate(${state.filters['hueRotate'].computedValue}deg)` : `hue-rotate(0deg)`;
const invert = state.filters['invert'].active ? `invert(${state.filters['invert'].computedValue}%)` : `invert(0%)`;
const blur = state.filters['blur'].active ? `blur(${state.filters['blur'].computedValue}px)` : `blur(0px)`;
ctx.filter = `${brightness} ${contrast} ${grayscale} ${sepia} ${hue} ${invert} ${blur}`;
// 4. Pixelate (Special Handling vor dem Zeichnen)
const pixelateVal = state.filters['pixelate'].computedValue;
if (state.mediaSource) {
if (state.mediaType === 'image') {
if (pixelateVal > 1) {
// Pixelate Effekt durch Downscaling
const w = canvasEl.width / pixelateVal;
const h = canvasEl.height / pixelateVal;
// Zeichne verkleinert in Offscreen Canvas (oder direkt hier mit Trick)
// Einfacher Trick: imageSmoothingEnabled = false und skalierung
ctx.imageSmoothingEnabled = false;
ctx.drawImage(state.mediaSource, 0, 0, w, h);
ctx.drawImage(canvasEl, 0, 0, w, h, 0, 0, canvasEl.width, canvasEl.height);
} else {
ctx.drawImage(state.mediaSource, 0, 0);
}
} else if (state.mediaType === 'video') {
if (pixelateVal > 1) {
const w = canvasEl.width / pixelateVal;
const h = canvasEl.height / pixelateVal;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(state.mediaSource, 0, 0, w, h);
ctx.drawImage(canvasEl, 0, 0, w, h, 0, 0, canvasEl.width, canvasEl.height);
} else {
ctx.drawImage(state.mediaSource, 0, 0, canvasEl.width, canvasEl.height);
}
}
}
ctx.restore(); // Filter resetten für Overlays
// 5. Custom Effekte & Overlays (Pixel Manipulation / Compositing)
// RGB Split
if (state.filters['rgbSplit'].active && state.filters['rgbSplit'].computedValue > 0) {
const offset = state.filters['rgbSplit'].computedValue;
const w = canvasEl.width;
const h = canvasEl.height;
// Erstelle临时 Kopien
// Da wir in JS keine performante Pixel-Manipulation pro Frame ohne WebGL wollen,
// nutzen wir Composite Operations für einen schnellen RGB Shift Trick
ctx.save();
ctx.globalCompositeOperation = 'screen';
// Rot Kanal simulieren (tint)
// Echter RGB Shift braucht imageData, was langsam ist. Wir machen einen "Shift" Versatz.
// Zeichne Canvas leicht versetzt mit Colorize (via Filter) wäre besser, aber komplex.
// Wir nutzen hier einen einfachen Slice-Shift Trick:
ctx.globalAlpha = 0.5;
ctx.translate(offset, 0);
ctx.drawImage(canvasEl, 0, 0); // Rot-ish shift
ctx.translate(-offset*2, 0);
ctx.drawImage(canvasEl, 0, 0); // Cyan-ish shift
ctx.restore();
}
// Strobe Flash
if (state.filters['strobe'].active) {
const val = state.filters['strobe'].computedValue; // -100 bis 100
if (val > 10) {
ctx.fillStyle = `rgba(255, 255, 255, ${Math.min(val / 100, 1)})`;
ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
} else if (val < -10) {
ctx.fillStyle = `rgba(0, 0, 0, ${Math.min(Math.abs(val) / 100, 1)})`;
ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
}
}
// Scanlines Overlay
if (state.filters['scanlines'].active) {
const opacity = state.filters['scanlines'].computedValue / 100;
ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`;
for (let y = 0; y < canvasEl.height; y += 4) {
ctx.fillRect(0, y, canvasEl.width, 2);
}
}
// Vignette
if (state.filters['vignette'].active) {
const val = state.filters['vignette'].computedValue;
const gradient = ctx.createRadialGradient(
canvasEl.width / 2, canvasEl.height / 2, canvasEl.height / 3,
canvasEl.width / 2, canvasEl.height / 2, canvasEl.height
);
gradient.addColorStop(0, "rgba(0,0,0,0)");
gradient.addColorStop(1, `rgba(0,0,0,${val / 100})`);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
}
// Glitch (Horizontal Slices)
if (state.filters['glitch'].active) {
const intensity = state.filters['glitch'].computedValue;
if (intensity > 0.1 && Math.random() < 0.2) { // Nur manchmal triggern für Glitch Look
const sliceHeight = Math.random() * 50 + 10;
const sliceY = Math.random() * canvasEl.height;
const offset = (Math.random() - 0.5) * intensity * 4;
try {
// Slice ausschneiden
const sliceData = ctx.getImageData