138 lines
17 KiB
HTML
138 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Loop Cam</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
|
|
<script src="../js/tailwind.js"></script>
|
|
|
|
<style>
|
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
html { width: 100%; height: 100%; background-color: #0f172a; }
|
|
body {
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0; margin: 0;
|
|
background-color: #0f172a;
|
|
color: white;
|
|
overflow: hidden !important;
|
|
user-select: none;
|
|
touch-action: none;
|
|
overscroll-behavior: none;
|
|
font-family: sans-serif;
|
|
animation: fadeIn 0.4s ease-out;
|
|
box-sizing: border-box;
|
|
padding-top: calc(10px + env(safe-area-inset-top));
|
|
padding-bottom: max(20px, env(safe-area-inset-bottom));
|
|
}
|
|
video, canvas { display: block; }
|
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
button:active { transform: scale(0.95); transition: transform 0.1s; }
|
|
</style>
|
|
</head>
|
|
<body class="flex flex-col p-2 md:p-4"
|
|
onclick="if(window.resetIdleTimer) window.resetIdleTimer()"
|
|
ontouchstart="if(window.resetIdleTimer) window.resetIdleTimer()">
|
|
|
|
<div class="flex-none flex justify-between items-center z-50 mb-2 relative h-16">
|
|
<button onclick="goHome()" class="bg-slate-800 text-white border-4 border-slate-600 hover:border-white px-4 py-2 rounded-full text-lg font-black shadow-xl flex items-center gap-2 transition-transform active:scale-95 no-underline focus:outline-none shrink-0 z-50">
|
|
🏠 MENÜ
|
|
</button>
|
|
<div class="absolute inset-0 flex items-center justify-center pointer-events-none z-0">
|
|
<h1 class="text-2xl md:text-4xl font-black text-white drop-shadow-[0_4px_4px_rgba(0,0,0,0.8)] tracking-widest uppercase">
|
|
✨ LOOP CAM
|
|
</h1>
|
|
</div>
|
|
<button onclick="toggleInfo(); playSound('click')" class="w-12 h-12 bg-slate-800 text-white rounded-full font-bold border-4 border-slate-600 hover:border-white shadow-xl flex items-center justify-center transition-transform active:scale-95 text-xl shrink-0 z-50">
|
|
?
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex-1 flex flex-row gap-2 md:gap-4 min-h-0 items-center justify-center w-full max-w-[95rem] mx-auto overflow-hidden pb-1">
|
|
<div class="w-20 md:w-24 flex flex-col gap-2 bg-gray-900/50 p-2 rounded-2xl border border-white/10 shadow-2xl shrink-0 h-full max-h-full overflow-hidden">
|
|
<div class="text-[8px] font-bold text-gray-500 uppercase tracking-widest text-center w-full py-1 shrink-0">Filter</div>
|
|
<div class="flex-1 overflow-y-auto no-scrollbar flex flex-col items-center justify-evenly w-full pb-1">
|
|
<button onclick="setFilter('')" class="w-16 h-12 rounded-xl bg-white text-black font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-transparent flex flex-col items-center justify-center"><span>⚪</span> NORM</button>
|
|
<button onclick="setFilter('grayscale(100%) contrast(1.2)')" class="w-16 h-12 rounded-xl bg-gray-600 text-white font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-white/20 flex flex-col items-center justify-center" style="filter: grayscale(100%)"><span>⚫</span> S/W</button>
|
|
<button onclick="setFilter('sepia(80%)')" class="w-16 h-12 rounded-xl bg-amber-700 text-white font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-white/20 flex flex-col items-center justify-center" style="filter: sepia(80%)"><span>📜</span> ALT</button>
|
|
<button onclick="setFilter('invert(100%)')" class="w-16 h-12 rounded-xl bg-blue-600 text-white font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-white/20 flex flex-col items-center justify-center" style="filter: invert(100%)"><span>💀</span> XRAY</button>
|
|
<button onclick="setFilter('saturate(250%)')" class="w-16 h-12 rounded-xl bg-pink-500 text-white font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-white/20 flex flex-col items-center justify-center" style="filter: saturate(250%)"><span>🎨</span> POP</button>
|
|
<button onclick="setFilter('hue-rotate(90deg)')" class="w-16 h-12 rounded-xl bg-emerald-500 text-white font-black text-[10px] hover:scale-105 active:scale-95 transition border-2 border-white/20 flex flex-col items-center justify-center" style="filter: hue-rotate(90deg)"><span>👽</span> ALIEN</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex-1 flex flex-col items-center h-full min-w-0 gap-2 md:gap-4 overflow-hidden">
|
|
<div id="video-container" class="relative flex-1 w-full bg-gray-900 rounded-[1.5rem] overflow-hidden border-[6px] border-gray-800 shadow-2xl group transition-colors duration-200 flex items-center justify-center min-h-0">
|
|
<video id="video" autoplay playsinline muted class="w-full h-full object-contain"></video>
|
|
<canvas id="canvas" class="w-full h-full object-contain hidden"></canvas>
|
|
<div id="countdown" class="absolute inset-0 flex items-center justify-center text-[8rem] font-black text-white hidden bg-black/50 backdrop-blur-sm z-30">3</div>
|
|
<div id="rec-indicator" class="absolute top-4 right-4 hidden items-center gap-2 z-30 bg-red-600 text-white px-3 py-1 rounded-full font-black text-sm animate-pulse shadow-lg"><div class="w-2 h-2 bg-white rounded-full"></div> REC</div>
|
|
<div id="saving-overlay" class="absolute inset-0 bg-black/80 z-50 hidden flex-col items-center justify-center text-white">
|
|
<div class="text-6xl animate-spin mb-4">💾</div>
|
|
<div class="text-2xl font-bold">Speichere...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="h-16 md:h-20 w-full flex-shrink-0 flex items-center justify-center relative">
|
|
<div id="start-overlay" class="absolute w-full flex justify-center items-center gap-4 z-20 pointer-events-none">
|
|
<button onclick="switchCamera(); playSound('click')" id="switch-btn" class="pointer-events-auto w-14 h-14 bg-slate-700 hover:bg-slate-600 text-white rounded-xl shadow-xl border-b-4 border-slate-900 active:border-b-0 active:translate-y-2 transition-all flex items-center justify-center shrink-0" title="Kamera wechseln"><span class="text-2xl">🔄</span></button>
|
|
<button onclick="playSound('click'); startCountdown()" class="pointer-events-auto bg-sky-500 hover:bg-sky-400 text-white text-2xl font-black py-3 px-8 rounded-xl shadow-2xl border-b-4 border-sky-700 active:border-b-0 active:translate-y-2 transition-all w-full max-w-xs flex items-center justify-center gap-2"><span>🎥</span> ACTION!</button>
|
|
</div>
|
|
|
|
<div id="action-buttons" class="hidden gap-2 md:gap-4 w-full justify-center z-20 pointer-events-none">
|
|
<button onclick="reset(); playSound('click')" class="pointer-events-auto bg-yellow-500 hover:bg-yellow-400 text-black text-xl font-black py-3 px-6 rounded-xl border-b-4 border-yellow-700 active:border-b-0 active:translate-y-2 transition-all shadow-xl flex items-center justify-center gap-2 flex-1 max-w-[150px]">🔄 NOCHMAL</button>
|
|
<button onclick="saveLoopVideo(); playSound('success')" class="pointer-events-auto bg-green-600 hover:bg-green-500 text-white text-xl font-black py-3 px-6 rounded-xl border-b-4 border-green-800 active:border-b-0 active:translate-y-2 transition-all shadow-xl flex items-center justify-center gap-2 flex-1 max-w-[150px]">💾 SPEICHERN</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="w-20 md:w-24 flex-shrink-0 flex flex-col justify-center gap-2 z-40 bg-gray-900/50 p-2 rounded-2xl border border-white/10 shadow-2xl h-full max-h-full overflow-hidden transition-opacity opacity-30 pointer-events-none" id="speed-controls">
|
|
<div class="text-center text-[8px] font-bold text-gray-400 mb-1 uppercase tracking-widest shrink-0">Tempo</div>
|
|
<div class="flex-1 flex flex-col justify-center gap-2">
|
|
<button id="btn-slow" onclick="setSpeed(150)" class="w-full h-16 bg-green-700 hover:bg-green-600 text-white rounded-lg shadow-lg border-b-2 border-green-900 transition active:scale-95 flex flex-col items-center justify-center gap-1 opacity-50"><span class="text-xl">🐢</span><span class="text-[8px] font-black uppercase">Langsam</span></button>
|
|
<button id="btn-norm" onclick="setSpeed(40)" class="w-full h-16 bg-blue-700 hover:bg-blue-600 text-white rounded-lg shadow-lg border-b-2 border-blue-900 transition active:scale-95 flex flex-col items-center justify-center gap-1 opacity-50"><span class="text-xl">🚶</span><span class="text-[8px] font-black uppercase">Mittel</span></button>
|
|
<button id="btn-turbo" onclick="setSpeed(10)" class="w-full h-16 bg-red-700 hover:bg-red-600 text-white rounded-lg shadow-lg border-b-2 border-red-900 transition active:scale-95 flex flex-col items-center justify-center gap-1 opacity-50"><span class="text-xl">🚀</span><span class="text-[8px] font-black uppercase">Turbo</span></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm items-center justify-center p-4" onclick="toggleInfo()">
|
|
<div class="bg-slate-800 border-4 border-sky-500 rounded-[2rem] max-w-2xl w-full p-8 shadow-2xl relative" onclick="event.stopPropagation()">
|
|
<button onclick="toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white hover:text-sky-400 text-4xl font-bold transition">✖</button>
|
|
<h2 class="text-4xl font-black text-white mb-8 border-b border-slate-600 pb-4 flex items-center gap-4"><span class="text-6xl">📹</span> ANLEITUNG</h2>
|
|
<div class="text-lg text-slate-300 space-y-4">
|
|
<p>1. Wähle links einen <b>Filter</b>.</p>
|
|
<p>2. Drücke unten auf <b>ACTION!</b>.</p>
|
|
<p>3. Nach der Aufnahme kannst du rechts das <b>Tempo</b> ändern.</p>
|
|
<p>4. Mit <b>🔄</b> wechselst du die Kamera.</p>
|
|
</div>
|
|
<button onclick="toggleInfo(); playSound('click')" class="mt-8 w-full bg-sky-600 hover:bg-sky-500 text-white font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const _soundCache = {'click': new Audio('../assets/sounds/click.mp3'), 'shutter': new Audio('../assets/sounds/shutter.mp3'), 'success': new Audio('../assets/sounds/success.mp3')};
|
|
Object.values(_soundCache).forEach(s => s.load());
|
|
window.playSound = function(id) { if(_soundCache[id]) { const s = _soundCache[id].cloneNode(); s.volume = 1.0; s.play().catch(e=>{}); } };
|
|
window.goHome = function() { playSound('click'); setTimeout(() => { window.location.href = '../index.html'; }, 300); };
|
|
let idleTimer;
|
|
window.resetIdleTimer = function() { clearTimeout(idleTimer); idleTimer = setTimeout(() => { if (document.getElementById('countdown').classList.contains('hidden') && document.getElementById('saving-overlay').classList.contains('hidden')) { window.location.href = '../index.html'; } else { window.resetIdleTimer(); } }, 120000); };
|
|
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, window.resetIdleTimer, {passive: true})); window.resetIdleTimer();
|
|
window.toggleInfo = function() { const modal = document.getElementById('info-modal'); modal.classList.toggle('hidden'); modal.classList.toggle('flex'); }
|
|
let video, canvas, ctx, videoContainer, recIndicator, startBtn, resultBtns, speedWrapper, switchBtn; let frames = []; let speed = 40; let loopInterval; let currentStream; let currentFacingMode = 'user';
|
|
window.initCam = async function() { video = document.getElementById('video'); canvas = document.getElementById('canvas'); ctx = canvas.getContext('2d'); videoContainer = document.getElementById('video-container'); recIndicator = document.getElementById('rec-indicator'); startBtn = document.getElementById('start-overlay'); resultBtns = document.getElementById('action-buttons'); speedWrapper = document.getElementById('speed-controls'); switchBtn = document.getElementById('switch-btn'); if(!video) return; try { if (currentStream && typeof currentStream.getTracks === 'function') currentStream.getTracks().forEach(track => track.stop()); const stream = await navigator.mediaDevices.getUserMedia({ video: { width: {ideal: 1280}, height: {ideal: 720}, facingMode: currentFacingMode } }); currentStream = stream; video.srcObject = stream; video.onloadedmetadata = () => { video.play(); }; video.style.transform = (currentFacingMode === 'user') ? 'scaleX(-1)' : 'scaleX(1)'; } catch (err) { alert("Kamera-Fehler: " + err.message); } }
|
|
window.switchCamera = function() { currentFacingMode = (currentFacingMode === 'user') ? 'environment' : 'user'; window.initCam(); }
|
|
window.setFilter = function(filter) { playSound('click'); if(video) video.style.filter = filter; if(canvas) canvas.style.filter = filter; }
|
|
window.startCountdown = function() { startBtn.classList.add('hidden'); switchBtn.classList.add('hidden'); const cd = document.getElementById('countdown'); cd.classList.remove('hidden'); cd.style.display = 'flex'; let count = 3; cd.innerText = count; const timer = setInterval(() => { count--; if (count > 0) { cd.innerText = count; } else { clearInterval(timer); cd.style.display = 'none'; playSound('shutter'); record(); } }, 800); }
|
|
function record() { frames = []; canvas.width = video.videoWidth; canvas.height = video.videoHeight; videoContainer.classList.replace('border-gray-800', 'border-red-600'); recIndicator.classList.remove('hidden'); recIndicator.style.display = 'flex'; let startTime = Date.now(); function captureLoop() { if (Date.now() - startTime < 2000) { ctx.save(); if (currentFacingMode === 'user') { ctx.translate(canvas.width, 0); ctx.scale(-1, 1); } ctx.drawImage(video, 0, 0, canvas.width, canvas.height); ctx.restore(); frames.push(ctx.getImageData(0, 0, canvas.width, canvas.height)); requestAnimationFrame(captureLoop); } else { play(); } } captureLoop(); }
|
|
function play() { videoContainer.classList.replace('border-red-600', 'border-gray-800'); recIndicator.classList.add('hidden'); recIndicator.style.display = ''; video.classList.add('hidden'); canvas.classList.remove('hidden'); resultBtns.classList.remove('hidden'); resultBtns.style.display = 'flex'; speedWrapper.classList.remove('opacity-30', 'pointer-events-none'); window.setSpeed(40); const loopFrames = [...frames, ...[...frames].reverse()]; startLoop(loopFrames, 0); }
|
|
function startLoop(loopFrames, i) { clearInterval(loopInterval); loopInterval = setInterval(() => { ctx.putImageData(loopFrames[i], 0, 0); i = (i + 1) % loopFrames.length; }, speed); }
|
|
window.setSpeed = function(ms) { playSound('click'); speed = ms; updateSpeedUI(); if (canvas && !canvas.classList.contains('hidden')) { const loopFrames = [...frames, ...[...frames].reverse()]; startLoop(loopFrames, 0); } }
|
|
function updateSpeedUI() { const btns = { 150: document.getElementById('btn-slow'), 40: document.getElementById('btn-norm'), 10: document.getElementById('btn-turbo') }; Object.values(btns).forEach(btn => { if(btn) { btn.classList.remove('opacity-100', 'scale-105', 'border-white'); btn.classList.add('opacity-50', 'border-transparent'); } }); const active = btns[speed]; if(active) { active.classList.remove('opacity-50', 'border-transparent'); active.classList.add('opacity-100', 'scale-105', 'border-white'); } }
|
|
window.saveLoopVideo = function() { document.getElementById('saving-overlay').classList.remove('hidden'); document.getElementById('saving-overlay').style.display = 'flex'; const stream = canvas.captureStream(30); 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 url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'mein-loop.webm'; a.click(); document.getElementById('saving-overlay').classList.add('hidden'); document.getElementById('saving-overlay').style.display = ''; }; recorder.start(); setTimeout(() => recorder.stop(), 3000); }
|
|
window.reset = function() { clearInterval(loopInterval); video.classList.remove('hidden'); canvas.classList.add('hidden'); startBtn.classList.remove('hidden'); resultBtns.classList.add('hidden'); resultBtns.style.display = 'none'; speedWrapper.classList.add('opacity-30', 'pointer-events-none'); switchBtn.classList.remove('hidden'); window.setSpeed(40); videoContainer.classList.replace('border-red-600', 'border-gray-800'); recIndicator.classList.add('hidden'); recIndicator.style.display = ''; }
|
|
window.addEventListener('beforeunload', () => { try { if(currentStream) currentStream.getTracks().forEach(track => track.stop()); } catch(e) {} });
|
|
window.onload = window.initCam;
|
|
</script>
|
|
</body>
|
|
</html>
|