Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Pro Video & Frame Werkzeug</title> | |
| <!-- Icons importieren --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-color: #121212; | |
| --panel-color: #1e1e1e; | |
| --accent-color: #00e5ff; | |
| --accent-secondary: #ff0055; | |
| --text-color: #e0e0e0; | |
| --input-bg: #2c2c2c; | |
| --border-radius: 12px; | |
| --font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| outline: none; | |
| } | |
| body { | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| font-family: var(--font-family); | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* HEADER */ | |
| header { | |
| background: linear-gradient(90deg, #0f0f0f, #1a1a1a); | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| border-bottom: 1px solid #333; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.3); | |
| } | |
| h1 { | |
| font-size: 1.2rem; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| color: var(--accent-color); | |
| } | |
| .credits a { | |
| color: var(--text-color); | |
| text-decoration: none; | |
| font-size: 0.9rem; | |
| opacity: 0.7; | |
| transition: opacity 0.3s; | |
| } | |
| .credits a:hover { | |
| opacity: 1; | |
| color: var(--accent-color); | |
| } | |
| /* MAIN LAYOUT */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 1fr 350px; | |
| gap: 20px; | |
| padding: 20px; | |
| height: calc(100vh - 60px); | |
| } | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| height: auto; | |
| } | |
| } | |
| /* PREVIEW AREA (THE WORLD) */ | |
| .preview-container { | |
| background-color: #000; | |
| border-radius: var(--border-radius); | |
| position: relative; | |
| overflow: hidden; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| border: 1px solid #333; | |
| perspective: 1000px; /* Für 3D Effekte */ | |
| } | |
| #world { | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| transform-style: preserve-3d; | |
| transition: transform 0.1s ease-out; /* Smooth Gyro */ | |
| position: relative; | |
| } | |
| /* Das Grid der Bilder */ | |
| .image-grid { | |
| display: grid; | |
| gap: 10px; | |
| padding: 20px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 8px; | |
| transform-style: preserve-3d; | |
| transition: filter 0.2s; | |
| } | |
| .grid-item { | |
| width: 80px; | |
| height: 80px; | |
| background-color: #333; | |
| background-image: url('https://picsum.photos/seed/tech/100/100.jpg'); | |
| background-size: cover; | |
| border-radius: 4px; | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.5); | |
| transition: transform 0.3s; | |
| } | |
| .grid-item:hover { | |
| transform: scale(1.1) translateZ(20px); | |
| border: 2px solid var(--accent-color); | |
| } | |
| /* Video Overlay */ | |
| .video-overlay { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| z-index: 10; | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .video-element { | |
| width: 200px; | |
| height: 112px; | |
| background: #000; | |
| border: 1px solid var(--accent-color); | |
| display: none; /* Standardmäßig ausgeblendet, wird per JS gesteuert */ | |
| object-fit: cover; | |
| } | |
| .video-element.active { | |
| display: block; | |
| } | |
| /* CONTROL PANEL */ | |
| .controls { | |
| background-color: var(--panel-color); | |
| border-radius: var(--border-radius); | |
| padding: 20px; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| border: 1px solid #333; | |
| } | |
| .control-group { | |
| background: rgba(0,0,0,0.2); | |
| padding: 15px; | |
| border-radius: 8px; | |
| border-left: 3px solid var(--accent-color); | |
| } | |
| .control-group h3 { | |
| font-size: 0.9rem; | |
| margin-bottom: 15px; | |
| color: var(--accent-color); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 5px; | |
| font-size: 0.8rem; | |
| color: #aaa; | |
| } | |
| /* Inputs & Sliders */ | |
| input[type="range"] { | |
| width: 100%; | |
| margin-bottom: 10px; | |
| accent-color: var(--accent-color); | |
| } | |
| input[type="number"], input[type="text"] { | |
| width: 100%; | |
| background: var(--input-bg); | |
| border: 1px solid #444; | |
| color: white; | |
| padding: 8px; | |
| border-radius: 4px; | |
| margin-bottom: 10px; | |
| } | |
| input[type="file"] { | |
| font-size: 0.8rem; | |
| width: 100%; | |
| margin-bottom: 10px; | |
| } | |
| /* Buttons */ | |
| .btn { | |
| background: #333; | |
| color: white; | |
| border: none; | |
| padding: 8px 12px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 0.85rem; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 6px; | |
| width: 100%; | |
| margin-bottom: 5px; | |
| } | |
| .btn:hover { | |
| background: #444; | |
| } | |
| .btn-primary { | |
| background: var(--accent-color); | |
| color: #000; | |
| font-weight: bold; | |
| } | |
| .btn-primary:hover { | |
| background: #00b8cc; | |
| } | |
| .btn-danger { | |
| background: var(--accent-secondary); | |
| color: white; | |
| } | |
| .btn.locked { | |
| background: var(--accent-secondary); | |
| box-shadow: inset 0 0 5px rgba(0,0,0,0.5); | |
| } | |
| /* Toggle Switch für Szenen */ | |
| .scene-toggle { | |
| display: flex; | |
| background: #000; | |
| border-radius: 4px; | |
| padding: 2px; | |
| margin-bottom: 10px; | |
| } | |
| .scene-option { | |
| flex: 1; | |
| text-align: center; | |
| padding: 8px; | |
| cursor: pointer; | |
| font-size: 0.8rem; | |
| border-radius: 4px; | |
| } | |
| .scene-option.active { | |
| background: var(--accent-color); | |
| color: #000; | |
| font-weight: bold; | |
| } | |
| /* HUD Overlay */ | |
| .hud { | |
| position: absolute; | |
| bottom: 20px; | |
| right: 20px; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 10px; | |
| border-radius: 8px; | |
| font-family: monospace; | |
| font-size: 0.9rem; | |
| pointer-events: none; | |
| z-index: 100; | |
| border: 1px solid #555; | |
| } | |
| .hud-row { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 15px; | |
| margin-bottom: 2px; | |
| } | |
| /* Audio Beat Visualizer Placeholder */ | |
| .beat-indicator { | |
| width: 100%; | |
| height: 10px; | |
| background: #333; | |
| border-radius: 5px; | |
| overflow: hidden; | |
| margin-top: 5px; | |
| } | |
| .beat-bar { | |
| width: 0%; | |
| height: 100%; | |
| background: var(--accent-secondary); | |
| transition: width 0.1s linear; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1><i class="fas fa-cut"></i> VideoCutter & AlignTool</h1> | |
| <div class="credits"> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a> | |
| </div> | |
| </header> | |
| <main> | |
| <!-- LINKER BEREICH: VORSCHAU --> | |
| <div class="preview-container"> | |
| <div id="world"> | |
| <!-- Video Overlay (Szene 1 & 2) --> | |
| <div class="video-overlay"> | |
| <video id="videoScene1" class="video-element active" muted loop playsinline></video> | |
| <video id="videoScene2" class="video-element" muted loop playsinline></video> | |
| </div> | |
| <!-- Grid mit Einzelbildern --> | |
| <div class="image-grid" id="imageGrid"> | |
| <!-- Dynamisch generierte Bilder --> | |
| </div> | |
| </div> | |
| <!-- Gyro HUD --> | |
| <div class="hud"> | |
| <div class="hud-row"><span>Gyro X:</span> <span id="hudX">0°</span></div> | |
| <div class="hud-row"><span>Gyro Y:</span> <span id="hudY">0°</span></div> | |
| <div class="hud-row"><span>Status:</span> <span id="hudStatus">Bereit</span></div> | |
| </div> | |
| </div> | |
| <!-- RECHTER BEREICH: STEUERUNG --> | |
| <div class="controls"> | |
| <!-- 1. VIDEO CUTTER --> | |
| <div class="control-group"> | |
| <h3><i class="fas fa-film"></i> Video Cutter (2 Szenen)</h3> | |
| <div class="scene-toggle"> | |
| <div class="scene-option active" onclick="switchScene(1)">Szene 1</div> | |
| <div class="scene-option" onclick="switchScene(2)">Szene 2</div> | |
| </div> | |
| <label>Szene 1 Datei</label> | |
| <input type="file" accept="video/*" onchange="loadVideo(this, 1)"> | |
| <label>Szene 2 Datei</label> | |
| <input type="file" accept="video/*" onchange="loadVideo(this, 2)"> | |
| <button class="btn btn-primary" onclick="togglePlay()"> | |
| <i class="fas fa-play"></i> / <i class="fas fa-pause"></i> Wiedergabe | |
| </button> | |
| </div> | |
| <!-- 2. RASTER & MATHE --> | |
| <div class="control-group"> | |
| <h3><i class="fas fa-th"></i> Raster & Math (JS)</h3> | |
| <label>Raster Wert (Gap): <span id="rasterValDisplay">0.00</span></label> | |
| <input type="range" id="rasterInput" min="0" max="100" step="0.1"> | |
| <div style="font-size: 0.75rem; color: #888; margin-bottom: 10px;"> | |
| Mathematische Basis: <code>Math.pow(Math.PI, 3)</code> ≈ 31.006 | |
| </div> | |
| <label>Gesamt Bilder (Anzahl)</label> | |
| <input type="range" id="imgCountInput" min="1" max="50" value="12" oninput="updateGridCount()"> | |
| </div> | |
| <!-- 3. AUDIO & BPM --> | |
| <div class="control-group"> | |
| <h3><i class="fas fa-music"></i> Audio & BPM</h3> | |
| <label>Audio Datei</label> | |
| <input type="file" accept="audio/*" id="audioInput"> | |
| <label>BPM (Beats Per Minute)</label> | |
| <input type="number" id="bpmInput" value="120" min="60" max="200"> | |
| <div class="beat-indicator"> | |
| <div class="beat-bar" id="beatBar"></div> | |
| </div> | |
| </div> | |
| <!-- 4. LICHT & FARBE --> | |
| <div class="control-group"> | |
| <h3><i class="fas fa-adjust"></i> Licht Kontrast / Farbe</h3> | |
| <label>Helligkeit (Light)</label> | |
| <input type="range" id="brightnessInput" min="0" max="200" value="100" oninput="updateFilters()"> | |
| <label>Kontrast</label> | |
| <input type="range" id="contrastInput" min="0" max="200" value="100" oninput="updateFilters()"> | |
| <label>Sättigung (Color)</label> | |
| <input type="range" id="saturateInput" min="0" max="200" value="100" oninput="updateFilters()"> | |
| </div> | |
| <!-- 5. WINKEL / GYRO CONTROL --> | |
| <div class="control-group"> | |
| <h3><i class="fas fa-sync-alt"></i> Winkel Ausrichtung</h3> | |
| <button id="reqPermission" class="btn btn-primary"> | |
| <i class="fas fa-mobile-alt"></i> Gyro Berechtigung | |
| </button> | |
| <label>Geschwindigkeits-Multiplikator: <span id="multVal">1.0</span></label> | |
| <input type="range" id="speedMult" min="0.1" max="5.0" step="0.1" value="1.0"> | |
| <label>Manueller Offset (Touchpoint)</label> | |
| <input type="number" id="manualOffset" value="0"> | |
| <div style="display: flex; gap: 10px; margin-top: 10px;"> | |
| <button id="lockXBtn" class="btn" onclick="toggleLock('x')"> | |
| <i class="fas fa-unlock"></i> X-Achse | |
| </button> | |
| <button id="lockYBtn" class="btn" onclick="toggleLock('y')"> | |
| <i class="fas fa-unlock"></i> Y-Achse | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| // --- 1. INITIALISIERUNG & HELPER --- | |
| const world = document.getElementById('world'); | |
| const imageGrid = document.getElementById('imageGrid'); | |
| const rasterInput = document.getElementById('rasterInput'); | |
| const rasterValDisplay = document.getElementById('rasterValDisplay'); | |
| // Default Wert: Pi^3 | |
| const PI_CUBED = Math.pow(Math.PI, 3); // ~31.006 | |
| rasterInput.value = PI_CUBED; | |
| rasterValDisplay.innerText = PI_CUBED.toFixed(3); | |
| // Grid beim Start generieren | |
| updateGridCount(); | |
| // --- 2. VIDEO CUTTER LOGIK --- | |
| let currentScene = 1; | |
| const vid1 = document.getElementById('videoScene1'); | |
| const vid2 = document.getElementById('videoScene2'); | |
| let isPlaying = false; | |
| function loadVideo(input, sceneNum) { | |
| const file = input.files[0]; | |
| if (file) { | |
| const url = URL.createObjectURL(file); | |
| const vid = sceneNum === 1 ? vid1 : vid2; | |
| vid.src = url; | |
| vid.load(); | |
| // Automatisch abspielen wenn Szene aktiv | |
| if (currentScene === sceneNum && isPlaying) vid.play(); | |
| } | |
| } | |
| function switchScene(num) { | |
| // UI Update | |
| document.querySelectorAll('.scene-option').forEach((el, idx) => { | |
| el.classList.toggle('active', idx + 1 === num); | |
| }); | |
| // Video Update | |
| if (currentScene === num) return; // Nichts tun wenn schon aktiv | |
| // Altes pausieren | |
| const oldVid = currentScene === 1 ? vid1 : vid2; | |
| oldVid.classList.remove('active'); | |
| if(isPlaying) oldVid.pause(); | |
| currentScene = num; | |
| const newVid = currentScene === 1 ? vid1 : vid2; | |
| newVid.classList.add('active'); | |
| if(isPlaying) newVid.play(); | |
| } | |
| function togglePlay() { | |
| isPlaying = !isPlaying; | |
| const activeVid = currentScene === 1 ? vid1 : vid2; | |
| if (isPlaying) { | |
| activeVid.play(); | |
| } else { | |
| vid1.pause(); | |
| vid2.pause(); | |
| } | |
| } | |
| // --- 3. RASTER & GRID LOGIK (MATH) --- | |
| rasterInput.addEventListener('input', (e) => { | |
| const val = parseFloat(e.target.value); | |
| rasterValDisplay.innerText = val.toFixed(3); | |
| updateGridGap(val); | |
| }); | |
| function updateGridGap(val) { | |
| // Wir teilen den Wert durch 10, damit es gut in px passt | |
| imageGrid.style.gap = `${val / 10}px`; | |
| } | |
| // Initiale Anwendeung | |
| updateGridGap(PI_CUBED); | |
| function updateGridCount() { | |
| const count = document.getElementById('imgCountInput').value; | |
| imageGrid.innerHTML = ''; | |
| // Grid Spalten anpassen basierend auf Anzahl (quadratisch näherungsweise) | |
| const cols = Math.ceil(Math.sqrt(count)); | |
| imageGrid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; | |
| for(let i=0; i<count; i++) { | |
| const div = document.createElement('div'); | |
| div.className = 'grid-item'; | |
| // Zufälliger Seed für Bilder | |
| div.style.backgroundImage = `url('https://picsum.photos/seed/${i + 50}/100/100.jpg')`; | |
| imageGrid.appendChild(div); | |
| } | |
| } | |
| // --- 4. AUDIO & BPM LOGIK --- | |
| const audioInput = document.getElementById('audioInput'); | |
| const bpmInput = document.getElementById('bpmInput'); | |
| const beatBar = document.getElementById('beatBar'); | |
| let audioContext, audioSource, analyser; | |
| let beatInterval; | |
| audioInput.addEventListener('change', function() { | |
| const file = this.files[0]; | |
| if (file) { | |
| const url = URL.createObjectURL(file); | |
| const audio = new Audio(url); | |
| audio.loop = true; | |
| // Simpler Metronom Visualisierung basierend auf BPM | |
| startMetronome(); | |
| // Audio abspielen (optional, hier nur Logik-Setup) | |
| audio.play().catch(e => console.log("Auto-play blocked")); | |
| } | |
| }); | |
| bpmInput.addEventListener('input', () => { | |
| startMetronome(); | |
| }); | |
| function startMetronome() { | |
| if (beatInterval) clearInterval(beatInterval); | |
| const bpm = parseInt(bpmInput.value) || 120; | |
| const intervalMs = (60 / bpm) * 1000; | |
| beatInterval = setInterval(() => { | |
| // Beat Animation | |
| beatBar.style.width = '100%'; | |
| beatBar.style.opacity = '1'; | |
| setTimeout(() => { | |
| beatBar.style.width = '0%'; | |
| beatBar.style.opacity = '0.5'; | |
| }, 100); | |
| }, intervalMs); | |
| } | |
| // --- 5. LICHT & FARBE --- | |
| function updateFilters() { | |
| const b = document.getElementById('brightnessInput').value; | |
| const c = document.getElementById('contrastInput').value; | |
| const s = document.getElementById('saturateInput').value; | |
| // Anwenden auf das Grid (Bilder) | |
| imageGrid.style.filter = `brightness(${b}%) contrast(${c}%) saturate(${s}%)`; | |
| } | |
| // --- 6. GYROSKOP & WINKEL (USER CODE INTEGRATION) --- | |
| // Elemente referenzieren | |
| const hudX = document.getElementById('hudX'); | |
| const hudY = document.getElementById('hudY'); | |
| const speedInput = document.getElementById('speedMult'); | |
| const multValDisplay = document.getElementById('multVal'); | |
| const btnPerm = document.getElementById('reqPermission'); | |
| const offsetInput = document.getElementById('manualOffset'); | |
| const hudStatus = document.getElementById('hudStatus'); | |
| // Status Variablen | |
| let state = { | |
| currentX: 0, // Aktuelle Rotation X (finaler Wert für CSS) | |
| currentY: 0, // Aktuelle Rotation Y (finaler Wert für CSS) | |
| baseX: null, // Startwert beim Gyro-Start (Nullpunkt) | |
| baseY: null, | |
| lockedX: false, // Ist X fixiert? | |
| lockedY: false, // Ist Y fixiert? | |
| fixedValX: 0, // Der Wert, auf dem X fixiert wurde | |
| fixedValY: 0, // Der Wert, auf dem Y fixiert wurde | |
| multiplier: 1, // Geschwindigkeitsfaktor | |
| manualOffset: 0 // Touchpoint Offset (manuelle Korrektur) | |
| }; | |
| // --- KONFIGURATION UPDATES --- | |
| speedInput.addEventListener('input', (e) => { | |
| state.multiplier = parseFloat(e.target.value); | |
| multValDisplay.innerText = state.multiplier; | |
| }); | |
| offsetInput.addEventListener('input', (e) => { | |
| state.manualOffset = parseInt(e.target.value); | |
| updateView(); | |
| }); | |
| // --- LOCKING LOGIK (FIXIEREN) --- | |
| window.toggleLock = function(axis) { | |
| if (axis === 'x') { | |
| state.lockedX = !state.lockedX; | |
| const btn = document.getElementById('lockXBtn'); | |
| if (state.lockedX) { | |
| state.fixedValX = state.currentX; // Speichere aktuellen IST-Wert als Fixpunkt | |
| btn.classList.add('locked'); | |
| btn.innerHTML = `<i class="fas fa-lock"></i> X LOCKED`; | |
| } else { | |
| btn.classList.remove('locked'); | |
| btn.innerHTML = `<i class="fas fa-unlock"></i> X-Achse`; | |
| } | |
| } | |
| else if (axis === 'y') { | |
| state.lockedY = !state.lockedY; | |
| const btn = document.getElementById('lockYBtn'); | |
| if (state.lockedY) { | |
| state.fixedValY = state.currentY; // Speichere aktuellen IST-Wert | |
| btn.classList.add('locked'); | |
| btn.innerHTML = `<i class="fas fa-lock"></i> Y LOCKED`; | |
| } else { | |
| btn.classList.remove('locked'); | |
| btn.innerHTML = `<i class="fas fa-unlock"></i> Y-Achse`; | |
| } | |
| } | |
| } | |
| // --- GYROSKOP LOGIK --- | |
| function handleOrientation(event) { | |
| // Beta = X-Achse (vor/zurück neigen) [-180, 180] | |
| // Gamma = Y-Achse (links/rechts neigen) [-90, 90] | |
| const xRaw = event.beta; | |
| const yRaw = event.gamma; | |
| // Wenn noch kein Basiswert (Kalibrierung beim Start), setze ihn | |
| if (state.baseX === null) { | |
| state.baseX = xRaw; | |
| state.baseY = yRaw; | |
| hudStatus.innerText = "Aktiv"; | |
| hudStatus.style.color = "#00e5ff"; | |
| } | |
| // Differenz berechnen * Multiplier | |
| const deltaX = (xRaw - state.baseX) * state.multiplier; | |
| const deltaY = (yRaw - state.baseY) * state.multiplier; | |
| // Berechne potenzielle neue Zielwerte | |
| let newX = deltaX + state.manualOffset; | |
| let newY = deltaY; | |
| // --- ACHSEN FIXIERUNG PRÜFEN --- | |
| // X-Achse Logic | |
| if (!state.lockedX) { | |
| state.currentX = newX; | |
| } else { | |
| state.currentX = state.fixedValX; | |
| } | |
| // Y-Achse Logic | |
| if (!state.lockedY) { | |
| state.currentY = newY; | |
| } else { | |
| state.currentY = state.fixedValY; | |
| } | |
| updateView(); | |
| } | |
| // --- VISUALISIERUNG UPDATEN --- | |
| function updateView() { | |
| // Begrenzung der Winkel optional, um "Flip" zu vermeiden (z.B. -90 bis 90) | |
| // Hier lassen wir es frei für 360 Grad Feeling | |
| // CSS Transform anwenden | |
| // rotateX für Beta, rotateY für Gamma | |
| world.style.transform = `rotateX(${-state.currentX}deg) rotateY(${state.currentY}deg)`; | |
| // HUD Updaten | |
| hudX.innerText = Math.round(state.currentX) + "°"; | |
| hudY.innerText = Math.round(state.currentY) + "°"; | |
| } | |
| // --- PERMISSION REQUEST (iOS 13+ support) --- | |
| btnPerm.addEventListener('click', requestGyro); | |
| function requestGyro() { | |
| if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') { | |
| // iOS 13+ | |
| DeviceOrientationEvent.requestPermission() | |
| .then(response => { | |
| if (response === 'granted') { | |
| window.addEventListener('deviceorientation', handleOrientation); | |
| btnPerm.style.display = 'none'; // Button ausblenden nach Erfolg | |
| hudStatus.innerText = "Berechtigung erteilt"; | |
| } else { | |
| alert('Gyro permission denied'); | |
| hudStatus.innerText = "Verweigert"; | |
| hudStatus.style.color = "red"; | |
| } | |
| }) | |
| .catch(console.error); | |
| } else { | |
| // Android / ältere iOS / PC (DevTools Sensors) | |
| window.addEventListener('deviceorientation', handleOrientation); | |
| btnPerm.style.display = 'none'; | |
| hudStatus.innerText = "Lausche..."; | |
| // Für Debugging auf Desktop ohne Sensor: | |
| console.log("Gyro Event Listener added (Sensors required for movement)"); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |