anycoder-8f7a0da9 / index.html
HI7RAI's picture
Upload folder using huggingface_hub
85f4eda verified
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Advanced Image-to-Video Morphing Studio</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary-bg: #0a0a0a;
--secondary-bg: #1a1a1a;
--accent-color: #00ff88;
--spot-color-1: #ff006e;
--spot-color-2: #00f5ff;
--text-primary: #ffffff;
--text-secondary: #888888;
--glass-bg: rgba(30, 30, 40, 0.4);
--glass-border: rgba(255, 255, 255, 0.1);
--control-height: 60px;
--panel-width: 320px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--primary-bg);
color: var(--text-primary);
overflow: hidden;
height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-columns: 1fr var(--panel-width);
grid-template-areas:
"header header"
"main controls"
"footer footer";
}
header {
grid-area: header;
background: var(--glass-bg);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 100;
}
header h1 {
font-size: clamp(1.2rem, 2vw, 1.8rem);
font-weight: 300;
letter-spacing: 2px;
background: linear-gradient(45deg, var(--accent-color), var(--spot-color-1));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
header a {
color: var(--accent-color);
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
}
header a:hover {
text-shadow: 0 0 10px var(--accent-color);
}
main {
grid-area: main;
perspective: 1200px;
position: relative;
overflow: hidden;
background: radial-gradient(circle at center, #111 0%, #000 100%);
}
.showroom-container {
position: absolute;
inset: 0;
display: grid;
place-items: center;
transform-style: preserve-3d;
transition: transform 0.1s ease-out;
}
#mainCanvas {
width: min(80vw, 80vh);
height: min(80vw, 80vh);
max-width: 600px;
max-height: 600px;
border: 1px solid var(--glass-border);
background: #000;
transform: rotateX(15deg) rotateY(-15deg);
box-shadow:
0 0 50px rgba(0, 255, 136, 0.2),
0 0 100px rgba(255, 0, 110, 0.1);
transition: transform 0.3s ease;
}
.canvas-locked-x {
transform: rotateX(15deg) !important;
}
.canvas-locked-y {
transform: rotateY(-15deg) !important;
}
.overlay-effects {
position: absolute;
inset: 0;
pointer-events: none;
mix-blend-mode: screen;
}
.pulse-effect {
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: radial-gradient(circle, var(--accent-color) 0%, transparent 70%);
transform: translate(-50%, -50%);
animation: pulse 0.5s ease-out;
}
@keyframes pulse {
to {
width: 200%;
height: 200%;
opacity: 0;
}
}
.controls-panel {
grid-area: controls;
background: var(--glass-bg);
backdrop-filter: blur(20px);
border-left: 1px solid var(--glass-border);
padding: 1rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1rem;
}
.control-section {
background: rgba(20, 20, 30, 0.6);
border-radius: 12px;
padding: 1rem;
border: 1px solid var(--glass-border);
}
.control-section h3 {
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--accent-color);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.file-input-wrapper {
position: relative;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
input[type="file"] {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-button {
background: linear-gradient(45deg, var(--accent-color), var(--spot-color-2));
border: none;
padding: 0.75rem 1rem;
border-radius: 8px;
color: var(--primary-bg);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.file-button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(0, 255, 136, 0.4);
}
.slider-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.slider-label {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-secondary);
}
input[type="range"] {
width: 100%;
height: 4px;
background: var(--secondary-bg);
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--accent-color);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 10px var(--accent-color);
}
.toggle-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.toggle-button {
background: var(--secondary-bg);
border: 1px solid var(--glass-border);
padding: 0.5rem;
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.8rem;
}
.toggle-button.active {
background: var(--accent-color);
color: var(--primary-bg);
border-color: var(--accent-color);
}
.action-button {
background: linear-gradient(45deg, var(--spot-color-1), var(--spot-color-2));
border: none;
padding: 1rem;
border-radius: 8px;
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
width: 100%;
}
.action-button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 25px rgba(255, 0, 110, 0.5);
}
.status-bar {
grid-area: footer;
background: var(--glass-bg);
backdrop-filter: blur(20px);
border-top: 1px solid var(--glass-border);
padding: 0.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
color: var(--text-secondary);
}
.image-thumbnails {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
gap: 0.5rem;
max-height: 150px;
overflow-y: auto;
}
.thumbnail {
aspect-ratio: 1;
background: var(--secondary-bg);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
border: 2px solid transparent;
}
.thumbnail:hover {
border-color: var(--accent-color);
transform: scale(1.05);
}
.thumbnail.active {
border-color: var(--spot-color-1);
}
@media (max-width: 768px) {
body {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto auto;
grid-template-areas:
"header"
"main"
"controls"
"footer";
}
.controls-panel {
max-height: 40vh;
}
#mainCanvas {
width: 90vw;
height: 90vw;
}
}
.loading-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.9);
display: none;
place-items: center;
z-index: 1000;
}
.loading-spinner {
width: 60px;
height: 60px;
border: 3px solid var(--glass-border);
border-top-color: var(--accent-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.audio-visualizer {
height: 40px;
background: var(--secondary-bg);
border-radius: 6px;
display: flex;
align-items: flex-end;
gap: 2px;
padding: 4px;
overflow: hidden;
}
.audio-bar {
flex: 1;
background: linear-gradient(to top, var(--accent-color), var(--spot-color-1));
border-radius: 2px;
transition: height 0.1s ease;
min-height: 2px;
}
</style>
</head>
<body>
<header>
<h1><i class="fas fa-magic"></i> Advanced Morphing Studio</h1>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">Built with anycoder</a>
</header>
<main>
<div class="showroom-container" id="showroom">
<canvas id="mainCanvas"></canvas>
<div class="overlay-effects" id="overlayEffects"></div>
</div>
</main>
<aside class="controls-panel">
<div class="control-section">
<h3><i class="fas fa-images"></i> Bilder</h3>
<div class="file-input-wrapper">
<button class="file-button" onclick="document.getElementById('folderInput').click()">
<i class="fas fa-folder-open"></i> Ordner laden
</button>
<input type="file" id="folderInput" webkitdirectory directory multiple accept="image/*">
<button class="file-button" onclick="document.getElementById('fileInput').click()">
<i class="fas fa-file-image"></i> Einzelbilder
</button>
<input type="file" id="fileInput" multiple accept="image/*">
</div>
<div class="image-thumbnails" id="thumbnails"></div>
</div>
<div class="control-section">
<h3><i class="fas fa-sliders-h"></i> Farbanalyse</h3>
<div class="slider-group">
<div class="slider-label">
<span>Kontrast Winkel</span>
<span id="contrastValue">50</span>
</div>
<input type="range" id="contrastAngle" min="0" max="100" value="50">
</div>
<div class="slider-group">
<div class="slider-label">
<span>Lichtverschiebung</span>
<span id="lightShiftValue">0</span>
</div>
<input type="range" id="lightShift" min="-100" max="100" value="0">
</div>
<div class="slider-group">
<div class="slider-label">
<span>Spot Farbe 1</span>
<span id="spotColor1Value">#ff006e</span>
</div>
<input type="color" id="spotColor1" value="#ff006e" style="width: 100%; height: 30px; border: none; border-radius: 6px;">
</div>
<div class="slider-group">
<div class="slider-label">
<span>Spot Farbe 2</span>
<span id="spotColor2Value">#00f5ff</span>
</div>
<input type="color" id="spotColor2" value="#00f5ff" style="width: 100%; height: 30px; border: none; border-radius: 6px;">
</div>
</div>
<div class="control-section">
<h3><i class="fas fa-cube"></i> 3D Effekte</h3>
<div class="toggle-group">
<button class="toggle-button active" id="toggle3D">3D Raum</button>
<button class="toggle-button" id="toggleDoubleExp">Double Exposure</button>
<button class="toggle-button" id="toggleMorph">Morphing</button>
<button class="toggle-button" id="togglePulse">Pulse</button>
</div>
<div class="slider-group">
<div class="slider-label">
<span>3D Tiefe</span>
<span id="depth3DValue">50</span>
</div>
<input type="range" id="depth3D" min="0" max="100" value="50">
</div>
<div class="slider-group">
<div class="slider-label">
<span>Morph Stärke</span>
<span id="morphStrengthValue">30</span>
</div>
<input type="range" id="morphStrength" min="0" max="100" value="30">
</div>
</div>
<div class="control-section">
<h3><i class="fas fa-music"></i> Audio Engine</h3>
<div class="file-input-wrapper">
<button class="file-button" onclick="document.getElementById('audioInput').click()">
<i class="fas fa-file-audio"></i> Audio importieren
</button>
<input type="file" id="audioInput" accept="audio/*">
</div>
<div class="audio-visualizer" id="audioVisualizer"></div>
<div class="slider-group">
<div class="slider-label">
<span>Takt (BPM)</span>
<span id="bpmValue">120</span>
</div>
<input type="range" id="bpm" min="60" max="180" value="120">
</div>
<div class="slider-group">
<div class="slider-label">
<span>Fazer Tiefe</span>
<span id="fazerValue">20</span>
</div>
<input type="range" id="fazerDepth" min="0" max="100" value="20">
</div>
<div class="slider-group">
<div class="slider-label">
<span>Flanger Rate</span>
<span id="flangerValue">0.5</span>
</div>
<input type="range" id="flangerRate" min="0" max="5" step="0.1" value="0.5">
</div>
<button class="action-button" id="generateAudioBtn">
<i class="fas fa-play"></i> Generiere Space Audio
</button>
</div>
<div class="control-section">
<h3><i class="fas fa-video"></i> Video Output</h3>
<div class="slider-group">
<div class="slider-label">
<span>Frame Rate</span>
<span id="fpsValue">30</span>
</div>
<input type="range" id="fps" min="15" max="60" value="30">
</div>
<div class="slider-group">
<div class="slider-label">
<span>Übergangszeit (s)</span>
<span id="transitionValue">2</span>
</div>
<input type="range" id="transitionTime" min="1" max="10" value="2">
</div>
<button class="action-button" id="renderBtn">
<i class="fas fa-film"></i> Rendere Video
</button>
<video id="outputVideo" style="width: 100%; margin-top: 1rem; border-radius: 8px; display: none;"></video>
</div>
</aside>
<footer class="status-bar">
<div id="statusText">Bereit - Lade Bilder um zu starten</div>
<div id="sensorStatus">Sensor: Inaktiv</div>
</footer>
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"></div>
</div>
<script>
// Global State
const state = {
images: [],
currentImageIndex: 0,
canvas: null,
ctx: null,
audioContext: null,
audioNodes: {},
isRendering: false,
lockedAxes: { x: false, y: false },
effects: {
contrastAngle: 50,
lightShift: 0,
spotColor1: '#ff006e',
spotColor2: '#00f5ff',
depth3D: 50,
morphStrength: 30,
doubleExposure: false,
pulse: false,
bpm: 120,
fazerDepth: 20,
flangerRate: 0.5,
fps: 30,
transitionTime: 2
}
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initializeCanvas();
initializeControls();
initializeAudio();
initializeSensors();
createAudioVisualizer();
});
function initializeCanvas() {
state.canvas = document.getElementById('mainCanvas');
state.ctx = state.canvas.getContext('2d');
state.canvas.width = 800;
state.canvas.height = 800;
}
function initializeControls() {
// File inputs
document.getElementById('folderInput').addEventListener('change', handleFolderSelect);
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
document.getElementById('audioInput').addEventListener('change', handleAudioImport);
// Sliders
const sliders = [
'contrastAngle', 'lightShift', 'depth3D', 'morphStrength',
'bpm', 'fazerDepth', 'flangerRate', 'fps', 'transitionTime'
];
sliders.forEach(id => {
const slider = document.getElementById(id);
const valueSpan = document.getElementById(id + 'Value');
slider.addEventListener('input', (e) => {
const value = e.target.value;
valueSpan.textContent = value;
state.effects[id] = parseFloat(value);
updateProcessing();
});
});
// Color pickers
document.getElementById('spotColor1').addEventListener('input', (e) => {
state.effects.spotColor1 = e.target.value;
document.getElementById('spotColor1Value').textContent = e.target.value;
});
document.getElementById('spotColor2').addEventListener('input', (e) => {
state.effects.spotColor2 = e.target.value;
document.getElementById('spotColor2Value').textContent = e.target.value;
});
// Toggle buttons
document.getElementById('toggle3D').addEventListener('click', toggleEffect);
document.getElementById('toggleDoubleExp').addEventListener('click', toggleEffect);
document.getElementById('toggleMorph').addEventListener('click', toggleEffect);
document.getElementById('togglePulse').addEventListener('click', toggleEffect);
// Action buttons
document.getElementById('generateAudioBtn').addEventListener('click', generateSpaceAudio);
document.getElementById('renderBtn').addEventListener('click', renderVideo);
}
function toggleEffect(e) {
e.target.classList.toggle('active');
const effectMap = {
'toggle3D': 'depth3D',
'toggleDoubleExp': 'doubleExposure',
'toggleMorph': 'morphing',
'togglePulse': 'pulse'
};
const effect = effectMap[e.target.id];
if (effect) state.effects[effect] = e.target.classList.contains('active');
updateProcessing();
}
async function handleFolderSelect(e) {
const files = Array.from(e.target.files).filter(f => f.type.startsWith('image/'));
await loadImages(files);
}
async function handleFileSelect(e) {
await loadImages(Array.from(e.target.files));
}
async function loadImages(files) {
showLoading(true);
state.images = [];
const thumbnails = document.getElementById('thumbnails');
thumbnails.innerHTML = '';
for (const file of files) {
const img = await createImageBitmap(file);
state.images.push(img);
const thumb = document.createElement('div');
thumb.className = 'thumbnail';
thumb.style.backgroundImage = `url(${URL.createObjectURL(file)})`;
thumb.style.backgroundSize = 'cover';
thumb.onclick = () => setActiveImage(state.images.length - 1);
thumbnails.appendChild(thumb);
}
if (state.images.length > 0) {
setActiveImage(0);
updateStatus(`${state.images.length} Bilder geladen`);
}
showLoading(false);
}
function setActiveImage(index) {
state.currentImageIndex = index;
document.querySelectorAll('.thumbnail').forEach((thumb, i) => {
thumb.classList.toggle('active', i === index);
});
updateProcessing();
}
function updateProcessing() {
if (state.images.length === 0) return;
const img = state.images[state.currentImageIndex];
const canvas = state.canvas;
const ctx = state.ctx;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Apply long exposure effect with previous image
if (state.currentImageIndex > 0 && state.effects.morphing) {
const prevImg = state.images[state.currentImageIndex - 1];
applyLongExposureMorph(ctx, prevImg, img, state.effects.morphStrength / 100);
} else {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
// Analyze and apply color grading
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
applyColorAnalysis(imageData);
ctx.putImageData(imageData, 0, 0);
// Apply double exposure if enabled
if (state.effects.doubleExposure && state.currentImageIndex < state.images.length - 1) {
const nextImg = state.images[state.currentImageIndex + 1];
applyDoubleExposure(ctx, nextImg, state.effects.depth3D / 100);
}
// Apply 3D displacement
if (state.effects.depth3D) {
apply3DDisplacement(ctx, state.effects.depth3D);
}
}
function applyLongExposureMorph(ctx, img1, img2, strength) {
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = ctx.canvas.width;
tempCanvas.height = ctx.canvas.height;
// Draw first image with fade
tempCtx.globalAlpha = 1 - strength * 0.5;
tempCtx.drawImage(img1, 0, 0);
// Draw second image with morph
tempCtx.globalAlpha = strength;
tempCtx.filter = `blur(${strength * 2}px)`;
tempCtx.drawImage(img2, 0, 0);
ctx.globalAlpha = 1;
ctx.filter = 'none';
ctx.drawImage(tempCanvas, 0, 0);
}
function applyColorAnalysis(imageData) {
const data = imageData.data;
const contrast = state.effects.contrastAngle / 50;
const lightShift = state.effects.lightShift;
// Extract spot colors
const spot1 = hexToRgb(state.effects.spotColor1);
const spot2 = hexToRgb(state.effects.spotColor2);
for (let i = 0; i < data.length; i += 4) {
// Convert to grayscale with custom weights
let gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
// Apply contrast curve
gray = 128 + (gray - 128) * contrast;
gray = Math.max(0, Math.min(255, gray + lightShift));
// Determine which spot color to apply based on pixel position
const pixelIndex = i / 4;
const x = (pixelIndex % imageData.width) / imageData.width;
const y = Math.floor(pixelIndex / imageData.width) / imageData.height;
const spotMix = (x + y) / 2;
const spotColor = spotMix > 0.5 ? spot1 : spot2;
const mixFactor = Math.abs(spotMix - 0.5) * 2;
// Mix grayscale with spot color
data[i] = gray * (1 - mixFactor * 0.3) + spotColor.r * mixFactor * 0.3;
data[i + 1] = gray * (1 - mixFactor * 0.3) + spotColor.g * mixFactor * 0.3;
data[i + 2] = gray * (1 - mixFactor * 0.3) + spotColor.b * mixFactor * 0.3;
}
}
function applyDoubleExposure(ctx, overlayImg, opacity) {
ctx.globalAlpha = opacity * 0.5;
ctx.globalCompositeOperation = 'screen';
ctx.drawImage(overlayImg, 0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
}
function apply3DDisplacement(ctx, depth) {
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const newData = ctx.createImageData(ctx.canvas.width, ctx.canvas.height);
const displacement = depth / 10;
for (let y = 0; y < ctx.canvas.height; y++) {
for (let x = 0; x < ctx.canvas.width; x++) {
const offsetX = Math.sin(y * 0.01) * displacement;
const offsetY = Math.cos(x * 0.01) * displacement;
const srcX = Math.max(0, Math.min(ctx.canvas.width - 1, x + offsetX));
const srcY = Math.max(0, Math.min(ctx.canvas.height - 1, y + offsetY));
const srcIndex = (Math.floor(srcY) * ctx.canvas.width + Math.floor(srcX)) * 4;
const destIndex = (y * ctx.canvas.width + x) * 4;
newData.data[destIndex] = imageData.data[srcIndex];
newData.data[destIndex + 1] = imageData.data[srcIndex + 1];
newData.data[destIndex + 2] = imageData.data[srcIndex + 2];
newData.data[destIndex + 3] = imageData.data[srcIndex + 3];
}
}
ctx.putImageData(newData, 0, 0);
}
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function initializeAudio() {
state.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
function createAudioVisualizer() {
const visualizer = document.getElementById('audioVisualizer');
for (let i = 0; i < 32; i++) {
const bar = document.createElement('div');
bar.className = 'audio-bar';
bar.style.height = '2px';
visualizer.appendChild(bar);
}
}
function generateSpaceAudio() {
if (!state.audioContext) return;
updateStatus('Generiere Space Audio...');
// Create drone
const oscillator = state.audioContext.createOscillator();
const gainNode = state.audioContext.createGain();
const filter = state.audioContext.createBiquadFilter();
oscillator.type = 'sawtooth';
oscillator.frequency.setValueAtTime(55, state.audioContext.currentTime); // A1
filter.type = 'lowpass';
filter.frequency.setValueAtTime(200, state.audioContext.currentTime);
filter.Q.setValueAtTime(10, state.audioContext.currentTime);
gainNode.gain.setValueAtTime(0.1, state.audioContext.currentTime);
// Create LFO for pulsating
const lfo = state.audioContext.createOscillator();
const lfoGain = state.audioContext.createGain();
lfo.frequency.setValueAtTime(state.effects.bpm / 60, state.audioContext.currentTime);
lfoGain.gain.setValueAtTime(0.05, state.audioContext.currentTime);
lfo.connect(lfoGain);
lfoGain.connect(gainNode.gain);
// Create stereo panner for binaural effect
const panner = state.audioContext.createStereoPanner();
panner.pan.setValueAtTime(-0.5, state.audioContext.currentTime);
// Create delay for space echo
const delay = state.audioContext.createDelay(1);
delay.delayTime.setValueAtTime(0.5, state.audioContext.currentTime);
const delayGain = state.audioContext.createGain();
delayGain.gain.setValueAtTime(0.3, state.audioContext.currentTime);
// Create flanger
const flanger = state.audioContext.createDelay(0.05);
flanger.delayTime.setValueAtTime(0.01, state.audioContext.currentTime);
const flangerLfo = state.audioContext.createOscillator();
flangerLfo.frequency.setValueAtTime(state.effects.flangerRate, state.audioContext.currentTime);
const flangerGain = state.audioContext.createGain();
flangerGain.gain.setValueAtTime(0.01, state.audioContext.currentTime);
flangerLfo.connect(flangerGain);
flangerGain.connect(flanger.delayTime);
// Connect nodes
oscillator.connect(filter);
filter.connect(gainNode);
gainNode.connect(delay);
gainNode.connect(panner);
delay.connect(delayGain);
delayGain.connect(panner);
panner.connect(state.audioContext.destination);
flangerLfo.start();
lfo.start();
oscillator.start();
// Store nodes for later manipulation
state.audioNodes = { oscillator, gainNode, filter, lfo, panner, flangerLfo };
// Generate random thunder/explosions
setInterval(() => {
if (Math.random() > 0.7) {
createThunderEffect();
}
}, 3000);
updateStatus('Space Audio aktiv');
}
function createThunderEffect() {
if (!state.audioContext) return;
const noise = state.audioContext.createBufferSource();
const buffer = state.audioContext.createBuffer(1, state.audioContext.sampleRate * 2, state.audioContext.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < buffer.length; i++) {
data[i] = Math.random() * 2 - 1;
}
noise.buffer = buffer;
const filter = state.audioContext.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(100, state.audioContext.currentTime);
filter.frequency.exponentialRampToValueAtTime(20, state.audioContext.currentTime + 2);
const gainNode = state.audioContext.createGain();
gainNode.gain.setValueAtTime(0.5, state.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, state.audioContext.currentTime + 2);
noise.connect(filter);
filter.connect(gainNode);
gainNode.connect(state.audioContext.destination);
noise.start();
noise.stop(state.audioContext.currentTime + 2);
// Trigger visual pulse
if (state.effects.pulse) {
createPulseEffect();
}
}
function createPulseEffect() {
const overlay = document.getElementById('overlayEffects');
const pulse = document.createElement('div');
pulse.className = 'pulse-effect';
overlay.appendChild(pulse);
setTimeout(() => pulse.remove(), 500);
}
function handleAudioImport(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (event) => {
const audioBuffer = await state.audioContext.decodeAudioData(event.target.result);
analyzeAudioBeat(audioBuffer);
};
reader.readAsArrayBuffer(file);
}
function analyzeAudioBeat(buffer) {
// Simple beat detection
const channelData = buffer.getChannelData(0);
const sampleRate = buffer.sampleRate;
const bpm = detectBPM(channelData, sampleRate);
state.effects.bpm = bpm;
document.getElementById('bpm').value = bpm;
document.getElementById('bpmValue').textContent = bpm;
updateStatus(`BPM erkannt: ${bpm}`);
}
function detectBPM(data, sampleRate) {
// Simplified beat detection - returns average energy peaks
const frameSize = Math.floor(sampleRate * 0.1);
let sum = 0;
let peaks = 0;
for (let i = 0; i < data.length; i += frameSize) {
let energy = 0;
for (let j = i; j < Math.min(i + frameSize, data.length); j++) {
energy += Math.abs(data[j]);
}
if (energy > 0.1) peaks++;
sum += energy;
}
return Math.max(60, Math.min(180, Math.floor(60 / (peaks / (data.length / sampleRate)))));
}
function initializeSensors() {
if (window.DeviceOrientationEvent) {
window.addEventListener('deviceorientation', handleOrientation);
document.getElementById('sensorStatus').textContent = 'Sensor: Aktiv';
} else {
// Fallback to mouse interaction
let mouseX = 0, mouseY = 0;
document.addEventListener('mousemove', (e) => {
mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
mouseY = (e.clientY / window.innerHeight - 0.5) * 2;
update3DView(mouseY * 30, mouseX * 30);
});
document.getElementById('sensorStatus').textContent = 'Sensor: Maus-Modus';
}
// Click to lock axes
document.getElementById('showroom').addEventListener('click', (e) => {
if (e.shiftKey) {
state.lockedAxes.x = !state.lockedAxes.x;
state.canvas.classList.toggle('canvas-locked-x', state.lockedAxes.x);
} else if (e.ctrlKey) {
state.lockedAxes.y = !state.lockedAxes.y;
state.canvas.classList.toggle('canvas-locked-y', state.lockedAxes.y);
}
});
}
function handleOrientation(event) {
const alpha = event.alpha || 0;
const beta = event.beta || 0;
const gamma = event.gamma || 0;
const rotX = state.lockedAxes.x ? 15 : beta * 0.5;
const rotY = state.lockedAxes.y ? -15 : gamma * 0.5;
update3DView(rotX, rotY);
// Trigger effects based on movement
if (Math.abs(beta) > 30 || Math.abs(gamma) > 30) {
createThunderEffect();
}
}
function update3DView(rotX, rotY) {
const showroom = document.getElementById('showroom');
if (!state.lockedAxes.x && !state.lockedAxes.y) {
showroom.style.transform = `rotateX(${rotX}deg) rotateY(${rotY}deg)`;
} else if (state.lockedAxes.x) {
showroom.style.transform = `rotateY(${rotY}deg)`;
} else if (state.lockedAxes.y) {
showroom.style.transform = `rotateX(${rotX}deg)`;
}
}
async function renderVideo() {
if (state.images.length < 2) {
updateStatus('Mindestens 2 Bilder für Video benötigt');
return;
}
showLoading(true);
updateStatus('Rendere Video...');
const fps = state.effects.fps;
const duration = state.effects.transitionTime;
const totalFrames = fps * duration * (state.images.length - 1);
const stream = state.canvas.captureStream(fps);
const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
const video = document.getElementById('outputVideo');
video.src = URL.createObjectURL(blob);
video.style.display = 'block';
showLoading(false);
updateStatus('Video fertig!');
};
recorder.start();
// Render frames
for (let i = 0; i < state.images.length - 1; i++) {
for (let frame = 0; frame < fps * duration; frame++) {
const progress = frame / (fps * duration);
// Interpolate between images
await interpolateFrames(state.images[i], state.images[i + 1], progress);
// Update audio visualizer
updateVisualizer();
await new Promise(r => setTimeout(r, 1000 / fps));
}
}
recorder.stop();
}
async function interpolateFrames(img1, img2, progress) {
const ctx = state.ctx;
const canvas = state.canvas;
// Clear with fade
ctx.globalAlpha = 0.1;
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1;
// Draw morphed image