anycoder-6c03be3a / index.html
HI7RAI's picture
Upload folder using huggingface_hub
7759e88 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 - WebGL & Math Engine</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>
<!-- glfx.js für WebGL Bildverarbeitung (Filter, Verzerrung) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/glfx/0.0.4/glfx.min.js"></script>
<style>
:root {
--bg-dark: #0f0f13;
--bg-panel: #1a1a20;
--bg-input: #25252e;
--primary: #bb86fc;
--secondary: #03dac6;
--accent: #cf6679;
--text-main: #e0e0e0;
--text-muted: #a0a0a0;
--border: #333;
--font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--header-height: 60px;
--footer-height: 50px;
}
* {
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 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
height: var(--header-height);
flex-shrink: 0;
z-index: 20;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.logo {
font-size: 1.2rem;
font-weight: 700;
color: var(--primary);
display: flex;
align-items: center;
gap: 10px;
letter-spacing: 0.5px;
}
.logo i { color: var(--secondary); }
.header-actions {
display: flex;
gap: 10px;
}
.btn {
background-color: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-main);
padding: 0.4rem 0.8rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
text-decoration: none;
}
.btn:hover {
border-color: var(--primary);
color: var(--primary);
background-color: #2f2f3a;
}
.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;
position: relative;
}
/* --- Canvas Area --- */
.canvas-container {
flex: 1;
background-color: #000;
background-image:
radial-gradient(circle at center, #1a1a2e 0%, #000 100%);
display: flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
/* Canvas Wrapper to handle scaling */
.canvas-wrapper {
box-shadow: 0 0 30px rgba(0, 0, 0, 0.6);
max-width: 95%;
max-height: 95%;
position: relative;
}
canvas {
display: block;
max-width: 100%;
max-height: 100%;
}
/* --- Sidebar --- */
.sidebar {
width: 400px;
background-color: var(--bg-panel);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
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;
background: rgba(26, 26, 32, 0.95);
}
.filter-list {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
/* Scrollbar Styling */
.filter-list::-webkit-scrollbar { width: 6px; }
.filter-list::-webkit-scrollbar-track { background: var(--bg-dark); }
.filter-list::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
/* --- Filter Item UI --- */
.filter-item {
background-color: var(--bg-input);
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 12px;
padding: 12px;
transition: all 0.2s;
}
.filter-item.active {
border-color: var(--secondary);
box-shadow: 0 0 10px rgba(3, 218, 198, 0.1);
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.filter-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-main);
display: flex;
align-items: center;
gap: 8px;
}
.filter-type-badge {
font-size: 0.6rem;
padding: 2px 4px;
border-radius: 2px;
background: #333;
color: #888;
text-transform: uppercase;
}
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 32px;
height: 18px;
}
.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: 18px;
}
.slider:before {
position: absolute;
content: "";
height: 12px; width: 12px;
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); }
/* Inputs */
.control-group { margin-bottom: 8px; }
.control-group label {
display: block;
font-size: 0.75rem;
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: #15151a;
border: 1px solid var(--border);
color: var(--primary);
padding: 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.8rem;
margin-top: 4px;
}
.math-input:focus { border-color: var(--secondary); }
.math-hint {
font-size: 0.7rem; color: var(--text-muted); margin-top: 2px;
display: flex; justify-content: space-between;
}
/* --- Footer / Timeline --- */
.timeline {
height: var(--footer-height);
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); font-size: 0.9rem; }
/* --- Components --- */
.hidden { display: none !important; }
#loading {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(15, 15, 19, 0.9);
color: white;
display: flex; justify-content: center; align-items: center;
z-index: 50; 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; }
/* --- Double Exposure Badge --- */
.badge-double-exp {
position: absolute;
top: 10px; right: 10px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(4px);
color: var(--primary);
padding: 4px 8px;
font-size: 0.7rem;
font-weight: bold;
border: 1px solid var(--primary);
border-radius: 2px;
text-transform: uppercase;
letter-spacing: 1px;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
}
.badge-double-exp.visible { opacity: 1; }
/* --- Responsive --- */
@media (max-width: 768px) {
.sidebar {
position: absolute; height: 100%; transform: translateX(100%);
box-shadow: -5px 0 15px rgba(0,0,0,0.5);
}
.sidebar.open { transform: translateX(0); }
.header-actions span { display: none; } /* Hide button text on mobile */
}
</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="Effekte">
<i class="fa-solid fa-sliders"></i> <span>Effekte</span>
</button>
<button class="btn" onclick="document.getElementById('fileInput').click()">
<i class="fa-solid fa-upload"></i> <span>Import</span>
</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> <span>Export</span>
</button>
</div>
</header>
<!-- Main Content -->
<main>
<!-- Canvas Area -->
<div class="canvas-container" id="canvasWrapper">
<div id="loading" class="hidden">
<div class="spinner"></div>
<span id="loadingText">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 (Bild/Video)</p>
</div>
<div class="canvas-wrapper">
<div id="doubleExpBadge" class="badge-double-exp">Double Exposure Mode</div>
<!-- Canvas wird hier dynamisch erstellt -->
<canvas id="mainCanvas"></canvas>
</div>
</div>
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<span><i class="fa-solid fa-wave-square"></i> FX Rack</span>
<button class="btn" style="padding: 2px 8px; font-size: 0.75rem;" 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.75rem; color: var(--text-muted); background: var(--bg-panel);">
<p><strong>Math.js Variablen:</strong></p>
<ul style="margin-left: 20px; margin-top: 5px; list-style: none;">
<li><code style="color:var(--secondary)">t</code>: Zeit (ms)</li>
<li><code style="color:var(--secondary)">v</code>: Slider-Wert</li>
<li><code style="color:var(--secondary)">x,y</code>: Zufall (0-1)</li>
</ul>
<p style="margin-top: 5px;">Beispiel: <code>sin(t/500) * v</code></p>
</div>
</aside>
</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.75rem; color: var(--text-muted);">
Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: var(--primary); text-decoration: none; font-weight: bold;">anycoder</a>
</span>
</div>
</div>
<script>
/**
* VisionFX Studio Pro - Core Logic
* Hybrid Rendering: WebGL (glfx.js) für Filter, Canvas 2D für Overlays/UI
*/
// --- Konfiguration & State ---
const config = {
width: 1280,
height: 720,
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
canvas: null, // Der Haupt-Canvas
texture: null, // WebGL Texture
fxCanvas: null, // WebGL Context (glfx)
ctx: null // 2D Fallback (für Overlays)
};
// --- DOM Elemente ---
const canvasWrapper = document.getElementById('canvasWrapper');
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 sidebar = document.getElementById('sidebar');
const doubleExpBadge = document.getElementById('doubleExpBadge');
// --- Filter Registry (Auto-Discovery Simulation) ---
// Wir definieren hier, welche Effekte verfügbar sind.
// In einer echten "Discovery" Umgebung würde man die Objekte von glfx.js scannen.
const filterRegistry = [
// --- WebGL Filter (glfx.js) ---
{
id: 'brightnessContrast',
name: 'Helligkeit & Kontrast',
type: 'webgl',
params: [
{ name: 'brightness', min: -1, max: 1, def: 0, label: 'Helligkeit' },
{ name: 'contrast', min: -1, max: 1, def: 0, label: 'Kontrast' }
]
},
{
id: 'hueSaturation',
name: 'Farbe & Sättigung',
type: 'webgl',
params: [
{ name: 'hue', min: -1, max: 1, def: 0, label: 'Farbrotation' },
{ name: 'saturation', min: -1, max: 1, def: 0, label: 'Sättigung' }
]
},
{
id: 'vignette',
name: 'Vignette',
type: 'webgl',
params: [
{ name: 'size', min: 0, max: 1, def: 0.5, label: 'Größe' },
{ name: 'amount', min: 0, max: 1, def: 0.5, label: 'Intensität' }
]
},
{
id: 'blur',
name: 'Weichzeichner (Gaussian)',
type: 'webgl',
params: [
{ name: 'strength', min: 0, max: 20, def: 0, label: 'Stärke' }
]
},
{
id: 'noise',
name: 'Rauschen (Film Grain)',
type: 'webgl',
params: [
{ name: 'amount', min: 0, max: 1, def: 0, label: 'Menge' }
]
},
{
id: 'denoise',
name: 'Rauschentfernung',
type: 'webgl',
params: [
{ name: 'strength', min: 0, max: 1, def: 0, label: 'Stärke' }
]
},
{
id: 'sepia',
name: 'Sepia',
type: 'webgl',
params: [
{ name: 'amount', min: 0, max: 1, def: 0, label: 'Menge' }
]
},
{
id: 'hexagonalPixelate',
name: 'Hexagon Pixelate',
type: 'webgl',
params: [
{ name: 'scale', min: 2, max: 50, def: 10, label: 'Blockgröße' }
]
},
{
id: 'ink',
name: 'Tinte / Kanten',
type: 'webgl',
params: [
{ name: 'strength', min: 0, max: 1, def: 0.25, label: 'Stärke' }
]
},
{
id: 'swirl',
name: 'Wirbel Verzerrung',
type: 'webgl',
params: [
{ name: 'strength', min: -10, max: 10, def: 0, label: 'Stärke' },
{ name: 'radius', min: 0, max: 500, def: 200, label: 'Radius' }
]
},
{
id: 'bulgePinch',
name: 'Wölbung',
type: 'webgl',
params: [
{ name: 'strength', min: -1, max: 1, def: 0, label: 'Stärke' },
{ name: 'radius', min: 0, max: 500, def: 200, label: 'Radius' }
]
},
// --- Custom / Hybrid Effekte ---
{
id: 'glitch',
name: 'Digital Glitch',
type: 'custom',
params: [
{ name: 'amount', min: 0, max: 100, def: 0, label: 'Intensität' }
]
},
{
id: 'rgbSplit',
name: 'RGB Shift (Chroma)',
type: 'custom',
params: [
{ name: 'offset', min: 0, max: 50, def: 0, label: 'Versatz' }
]
},
{
id: 'scanlines',
name: 'TV Scanlines',
type: 'custom',
params: [
{ name: 'opacity', min: 0, max: 1, def: 0.3, label: 'Deckkraft' }
]
},
{
id: 'doubleExposure',
name: 'Double Exposure',
type: 'custom',
params: [
{ name: 'mix', min: 0, max: 1, def: 0, label: 'Mischung' }
]
}
];
// --- Initialisierung ---
function init() {
// Canvas Setup
state.canvas = document.getElementById('mainCanvas');
state.ctx = state.canvas.getContext('2d');
// Initial WebGL Canvas vorbereiten (wird beim ersten Media Load aktiviert)
if (typeof fx !== 'undefined') {
state.fxCanvas = fx.canvas();
}
// 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
resizeCanvas(800, 600);
state.ctx.fillStyle = config.bgColor;
state.ctx.fillRect(0, 0, state.canvas.width, state.canvas.height);
// Start Render Loop
requestAnimationFrame(renderLoop);
console.log("VisionFX initialized. Libraries: Math.js, glfx.js");
}
// --- UI Generierung (Dynamic) ---
function generateFilterUI() {
filterListEl.innerHTML = '';
filterRegistry.forEach(f => {
const item = document.createElement('div');
item.className = 'filter-item';
item.id = `filter-${f.id}`;
// Typ Badge
const typeLabel = f.type === 'webgl' ? 'GPU' : 'CPU';
let paramsHtml = '';
f.params.forEach(p => {
paramsHtml += `
<div class="control-group">
<label>${p.label}</label>
<input type="range" class="range-slider param-slider"
data-filter="${f.id}" data-param="${p.name}"
min="${p.min}" max="${p.max}" value="${p.def}" step="0.01">
<div class="math-hint">
<span>Val: <span class="val-display" id="val-${f.id}-${p.name}">${p.def}</span></span>
</div>
<input type="text" class="math-input param-formula"
data-filter="${f.id}" data-param="${p.name}"
placeholder="Formel (z.B. sin(t)*v)" value="">
</div>
`;
});
item.innerHTML = `
<div class="filter-header">
<div class="filter-title">
${f.name} <span class="filter-type-badge">${typeLabel}</span>
</div>
<label class="toggle-switch">
<input type="checkbox" class="filter-toggle" data-id="${f.id}">
<span class="slider"></span>
</label>
</div>
${paramsHtml}
`;
filterListEl.appendChild(item);
// State Initialisierung
state.filters[f.id] = {
active: false,
type: f.type,
params: {}
};
f.params.forEach(p => {
state.filters[f.id].params[p.name] = {
value: p.def,
formula: '',
computed: p.def
};
});
// Event Listeners
const toggle = item.querySelector('.filter-toggle');
toggle.addEventListener('change', (e) => {
state.filters[f.id].active = e.target.checked;
item.classList.toggle('active', e.target.checked);
// Spezielles Handling für Double Exposure Badge
if(f.id === 'doubleExposure') {
doubleExpBadge.classList.toggle('visible', e.target.checked);
}
});
// Sliders
item.querySelectorAll('.param-slider').forEach(slider => {
slider.addEventListener('input', (e) => {
const pName = e.target.dataset.param;
state.filters[f.id].params[pName].value = parseFloat(e.target.value);
document.getElementById(`val-${f.id}-${pName}`).innerText = parseFloat(e.target.value).toFixed(2);
});
});
// Formulas
item.querySelectorAll('.param-formula').forEach(input => {
input.addEventListener('input', (e) => {
const pName = e.target.dataset.param;
state.filters[f.id].params[pName].formula = e.target.value;
});
});
});
}
function resetFilters() {
document.querySelectorAll('.filter-toggle').forEach(el => el.checked = false);
document.querySelectorAll('.filter-item').forEach(el => el.classList.remove('active'));
doubleExpBadge.classList.remove('visible');
Object.keys(state.filters).forEach(key => {
state.filters[key].active = false;
const defParams = filterRegistry.find(f => f.id === key).params;
defParams.forEach(p => {
state.filters[key].params[p.name].value = p.def;
state.filters[key].params[p.name].formula = '';
// Update UI
const slider = document.querySelector(`.param-slider[data-filter="${key}"][data-param="${p.name}"]`);
const display = document.getElementById(`val-${key}-${p.name}`);
const formula = document.querySelector(`.param-formula[data-filter="${key}"][data-param="${p.name}"]`);
if(slider) slider.value = p.def;
if(display) display.innerText = p.def;
if(formula) formula.value = '';
});
});
}
// --- Math Engine ---
function evaluateFormula(formula, time, sliderVal) {
if (!formula || formula.trim() === '') return sliderVal;
try {
const scope = {
t: time,
v: sliderVal,
x: Math.random(),
y: Math.random(),
pi: Math.PI
};
return math.evaluate(formula, scope);
} catch (err) {
return sliderVal; // Fallback bei Fehler
}
}
// --- 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;
setupCanvasForMedia(img.width, img.height);
state.texture = state.fxCanvas.texture(img);
loadingEl.classList.add('hidden');
renderFrame(0); // Init render
};