Erster Upload von MX Linux

This commit is contained in:
2026-02-02 09:45:14 +01:00
commit a25d0becaf
109 changed files with 6801 additions and 0 deletions

174
public/apps/comic.html Normal file
View File

@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Comic Studio</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>
<script src="https://cdn.tailwindcss.com"></script> <style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
html {
width: 100%; height: 100%;
background-color: #facc15;
}
body {
/* Fixiertes Layout für maximale Stabilität */
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
margin: 0;
background-color: #facc15;
color: black;
overflow: hidden !important;
user-select: none;
touch-action: none;
font-family: sans-serif;
animation: fadeIn 0.4s ease-out;
transition: background-color 0.3s ease;
box-sizing: border-box;
/* OPTIMIERUNG: Näher an den Rand rutschen */
/* Oben: Notch + 10px (statt vorher 60px pauschal) */
padding-top: calc(10px + env(safe-area-inset-top));
/* Unten: Home-Balken + 20px */
padding-bottom: max(20px, env(safe-area-inset-bottom));
}
.font-comic { font-family: 'Comic Sans MS', 'Chalkboard SE', sans-serif; }
.no-scrollbar::-webkit-scrollbar { display: none; }
/* Sprechblasen */
.bubble-container { position: absolute; z-index: 20; display: flex; flex-direction: column; align-items: center; left: 50%; bottom: 25%; transform: translateX(-50%); }
.bubble-handle { background-color: #3b82f6; color: white; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; cursor: move; box-shadow: 0 4px 6px rgba(0,0,0,0.3); margin-bottom: -15px; z-index: 30; font-size: 24px; touch-action: none; border: 2px solid white; }
.bubble-text { background: white; border: 4px solid black; border-radius: 2rem; padding: 1rem 1.5rem; min-width: 160px; max-width: 280px; text-align: center; font-family: 'Comic Sans MS', sans-serif; font-size: 1.5rem; line-height: 1.2; box-shadow: 8px 8px 0px rgba(0,0,0,0.2); outline: none; cursor: text; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
@media print {
@page { size: landscape; margin: 0mm; }
body * { visibility: hidden; }
#print-container, #print-container * { visibility: visible; }
#print-container {
position: fixed; left: 0; top: 0; width: 100vw; height: 100vh;
display: flex !important; align-items: center; justify-content: center;
background: white; z-index: 99999; margin: 0; padding: 0;
}
#print-img { width: 100%; height: 100%; object-fit: contain; }
}
</style>
</head>
<body class="flex flex-col px-2 md:px-4"
onclick="if(window.resetIdleTimer) window.resetIdleTimer()"
ontouchstart="if(window.resetIdleTimer) window.resetIdleTimer()">
<div class="flex-none flex items-center justify-between gap-2 mb-1 bg-white/20 backdrop-blur-md p-1 rounded-3xl shadow-lg border-2 border-white/40 overflow-hidden relative z-50">
<div class="flex items-center gap-2 shrink-0 pl-1">
<button onclick="goHome()" class="bg-slate-900 text-white border-4 border-slate-700 hover:border-white px-3 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">
🏠 <span class="hidden lg:inline">MENÜ</span>
</button>
<h1 class="text-lg md:text-2xl font-black font-comic text-black tracking-widest drop-shadow-sm uppercase leading-none hidden md:block">
COMIC
</h1>
</div>
<div class="flex gap-1 overflow-x-auto no-scrollbar px-2 shrink-0 justify-center">
<button onclick="changeBg('#facc15')" class="w-10 h-10 rounded-xl bg-yellow-400 border-4 border-white hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
<button onclick="changeBg('#ef4444')" class="w-10 h-10 rounded-xl bg-red-500 border-4 border-white hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
<button onclick="changeBg('#3b82f6')" class="w-10 h-10 rounded-xl bg-blue-500 border-4 border-white hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
<button onclick="changeBg('#22c55e')" class="w-10 h-10 rounded-xl bg-green-500 border-4 border-white hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
<button onclick="changeBg('#a855f7')" class="w-10 h-10 rounded-xl bg-purple-500 border-4 border-white hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
<button onclick="changeBg('#ffffff')" class="w-10 h-10 rounded-xl bg-white border-4 border-gray-300 hover:scale-110 transition shadow-md ring-2 ring-transparent active:ring-black"></button>
</div>
<div class="flex items-center gap-2 shrink-0 pr-1">
<button onclick="switchCamera(); playSound('click')" class="w-12 h-12 bg-white hover:bg-gray-100 text-black rounded-full flex items-center justify-center text-xl shadow-lg border-4 border-slate-900 active:scale-95 transition" title="Kamera wechseln">
🔄
</button>
<div class="relative w-24 h-16 md:w-32 md:h-20 bg-black rounded-xl border-4 border-black shadow-xl overflow-hidden hidden sm:block">
<video id="webcam" autoplay playsinline muted class="w-full h-full object-cover opacity-90"></video>
<div class="absolute bottom-0 right-0 bg-red-600 text-white text-[8px] px-1 rounded-tl font-black animate-pulse pointer-events-none">LIVE</div>
</div>
<button onclick="toggleInfo(); playSound('click')" class="w-12 h-12 bg-slate-900 text-white rounded-full font-bold border-4 border-slate-700 hover:border-white shadow-xl flex items-center justify-center transition-transform active:scale-95 text-xl">
?
</button>
</div>
</div>
<div class="flex-1 flex flex-col md:flex-row gap-3 w-full max-w-[95rem] mx-auto mb-2 px-1 overflow-y-auto no-scrollbar z-10 relative min-h-0">
<div class="flex-1 bg-white border-[6px] border-black relative shadow-[4px_4px_0px_0px_rgba(0,0,0,0.8)] rounded-xl overflow-hidden group min-h-[150px]" id="panel-0">
<canvas class="w-full h-full object-cover bg-gray-100 pointer-events-none"></canvas>
<div class="absolute top-0 left-0 bg-black text-white px-3 py-0 text-2xl font-black font-comic rounded-br-lg pointer-events-none z-10">1</div>
<div class="bubble-container"><div class="bubble-handle"></div><div contenteditable="true" class="bubble-text" onfocus="clearText(this, 'Start...')" onblur="resetText(this, 'Start...')">Start...</div></div>
<button onclick="snap(0)" class="absolute bottom-2 right-2 bg-blue-600 hover:bg-blue-500 text-white w-16 h-16 rounded-full text-3xl shadow-xl border-4 border-white z-30 active:scale-90 transition flex items-center justify-center">📸</button>
</div>
<div class="flex-1 bg-white border-[6px] border-black relative shadow-[4px_4px_0px_0px_rgba(0,0,0,0.8)] rounded-xl overflow-hidden group min-h-[150px]" id="panel-1">
<canvas class="w-full h-full object-cover bg-gray-100 pointer-events-none"></canvas>
<div class="absolute top-0 left-0 bg-black text-white px-3 py-0 text-2xl font-black font-comic rounded-br-lg pointer-events-none z-10">2</div>
<div class="bubble-container"><div class="bubble-handle"></div><div contenteditable="true" class="bubble-text" onfocus="clearText(this, 'Dann...')" onblur="resetText(this, 'Dann...')">Dann...</div></div>
<button onclick="snap(1)" class="absolute bottom-2 right-2 bg-blue-600 hover:bg-blue-500 text-white w-16 h-16 rounded-full text-3xl shadow-xl border-4 border-white z-30 active:scale-90 transition flex items-center justify-center">📸</button>
</div>
<div class="flex-1 bg-white border-[6px] border-black relative shadow-[4px_4px_0px_0px_rgba(0,0,0,0.8)] rounded-xl overflow-hidden group min-h-[150px]" id="panel-2">
<canvas class="w-full h-full object-cover bg-gray-100 pointer-events-none"></canvas>
<div class="absolute top-0 left-0 bg-black text-white px-3 py-0 text-2xl font-black font-comic rounded-br-lg pointer-events-none z-10">3</div>
<div class="bubble-container"><div class="bubble-handle"></div><div contenteditable="true" class="bubble-text" onfocus="clearText(this, 'Ende!')" onblur="resetText(this, 'Ende!')">Ende!</div></div>
<button onclick="snap(2)" class="absolute bottom-2 right-2 bg-blue-600 hover:bg-blue-500 text-white w-16 h-16 rounded-full text-3xl shadow-xl border-4 border-white z-30 active:scale-90 transition flex items-center justify-center">📸</button>
</div>
</div>
<div class="text-center flex justify-center gap-4 px-4 z-20 relative shrink-0">
<button onclick="playSound('click'); downloadComic()" class="flex-1 max-w-xs bg-slate-900 text-white font-black font-comic text-xl md:text-2xl py-3 rounded-2xl border-b-[6px] border-slate-700 hover:border-slate-500 hover:translate-y-[-2px] active:border-b-0 active:translate-y-2 transition-all shadow-xl flex items-center justify-center gap-2">💾 SPEICHERN</button>
<button id="btn-print" onclick="playSound('click'); window.printComic()" class="flex-1 max-w-xs bg-white text-black font-black font-comic text-xl md:text-2xl py-3 rounded-2xl border-b-[6px] border-gray-300 hover:border-gray-400 hover:translate-y-[-2px] active:border-b-0 active:translate-y-2 transition-all shadow-xl flex items-center justify-center gap-2">🖨️ DRUCKEN</button>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onclick="toggleInfo()">
<div class="bg-slate-800 border-4 border-yellow-400 rounded-[2rem] max-w-3xl 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-yellow-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="grid grid-cols-1 md:grid-cols-3 gap-6 text-base">
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4 text-blue-400">📸</div><h3 class="text-white font-bold text-xl mb-2">Fotografieren</h3><p class="text-slate-300 leading-snug">Drücke auf den blauen Kamera-Button in jedem Bildrahmen.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4 text-white">💬</div><h3 class="text-white font-bold text-xl mb-2">Texten</h3><p class="text-slate-300 leading-snug">Tippe auf den Text. Du kannst die Blase am blauen Griff verschieben.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4 text-yellow-400">🎨</div><h3 class="text-white font-bold text-xl mb-2">Hintergrund</h3><p class="text-slate-300 leading-snug">Wähle oben eine Farbe für deinen Comic aus!</p></div>
</div>
<button onclick="toggleInfo(); playSound('click')" class="mt-8 w-full bg-yellow-500 hover:bg-yellow-400 text-black font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<div id="print-container" class="hidden"><img id="print-img" src=""></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.currentTime = 0; s.play().catch(e => console.warn(e)); } };
window.goHome = function() { playSound('click'); setTimeout(() => { window.location.href = '../index.html'; }, 300); };
const video = document.getElementById('webcam'); let currentFacingMode = 'user'; let currentStream; let idleTimer; let currentBgColor = '#facc15';
function resetIdleTimer() { clearTimeout(idleTimer); idleTimer = setTimeout(() => { window.location.href = '../index.html'; }, 120000); }
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, resetIdleTimer, {passive: true})); resetIdleTimer();
function toggleInfo() { const modal = document.getElementById('info-modal'); if (modal.classList.contains('hidden')) { modal.classList.remove('hidden'); modal.classList.add('flex'); } else { modal.classList.add('hidden'); modal.classList.remove('flex'); } }
async function startCamera() { try { if (currentStream) { 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; } catch (err) { alert("Kamera Fehler: " + err.message); } }
function switchCamera() { currentFacingMode = (currentFacingMode === 'user') ? 'environment' : 'user'; startCamera(); }
function snap(panelIndex) { playSound('shutter'); const panel = document.getElementById('panel-' + panelIndex); const canvas = panel.querySelector('canvas'); const ctx = canvas.getContext('2d'); canvas.width = 1280; canvas.height = 720; const vRatio = video.videoWidth / video.videoHeight; const cRatio = canvas.width / canvas.height; let sx, sy, sWidth, sHeight; if (vRatio > cRatio) { sHeight = video.videoHeight; sWidth = sHeight * cRatio; sx = (video.videoWidth - sWidth) / 2; sy = 0; } else { sWidth = video.videoWidth; sHeight = sWidth / cRatio; sx = 0; sy = (video.videoHeight - sHeight) / 2; } ctx.save(); if(currentFacingMode === 'user') { ctx.scale(-1, 1); ctx.drawImage(video, sx, sy, sWidth, sHeight, -canvas.width, 0, canvas.width, canvas.height); } else { ctx.drawImage(video, sx, sy, sWidth, sHeight, 0, 0, canvas.width, canvas.height); } ctx.restore(); panel.style.borderColor = '#3b82f6'; setTimeout(() => panel.style.borderColor = 'black', 200); }
function changeBg(color) { playSound('click'); currentBgColor = color; document.body.style.backgroundColor = color; }
function clearText(el, defaultText) { if (el.innerText.trim() === defaultText) el.innerText = ""; }
function resetText(el, defaultText) { if (el.innerText.trim() === "") el.innerText = defaultText; }
const handles = document.querySelectorAll('.bubble-handle'); let activeDrag = null; let startX, startY, initialLeft, initialTop;
handles.forEach(handle => { handle.addEventListener('mousedown', startDrag); handle.addEventListener('touchstart', startDrag, {passive: false}); });
function startDrag(e) { e.preventDefault(); activeDrag = e.target.parentElement; const clientX = e.clientX || e.touches[0].clientX; const clientY = e.clientY || e.touches[0].clientY; startX = clientX; startY = clientY; initialLeft = activeDrag.offsetLeft; initialTop = activeDrag.offsetTop; document.addEventListener('mousemove', doDrag); document.addEventListener('mouseup', stopDrag); document.addEventListener('touchmove', doDrag, {passive: false}); document.addEventListener('touchend', stopDrag); }
function doDrag(e) { if (!activeDrag) return; e.preventDefault(); const clientX = e.clientX || e.touches[0].clientX; const clientY = e.clientY || e.touches[0].clientY; const dx = clientX - startX; const dy = clientY - startY; activeDrag.style.left = (initialLeft + dx) + 'px'; activeDrag.style.top = (initialTop + dy) + 'px'; activeDrag.style.transform = 'none'; }
function stopDrag() { activeDrag = null; document.removeEventListener('mousemove', doDrag); document.removeEventListener('mouseup', stopDrag); document.removeEventListener('touchmove', doDrag); document.removeEventListener('touchend', stopDrag); }
function generateComicImage() { const refPanel = document.getElementById('panel-0'); const refRect = refPanel.getBoundingClientRect(); const screenRatio = refRect.width / refRect.height; const panelW = 1280; const panelH = panelW / screenRatio; const border = 60; const gap = 40; const titleSpace = 250; const master = document.createElement('canvas'); master.width = (panelW * 3) + (gap * 2) + (border * 2); master.height = panelH + (border * 2) + titleSpace; const ctx = master.getContext('2d'); ctx.fillStyle = currentBgColor; ctx.fillRect(0, 0, master.width, master.height); ctx.fillStyle = 'black'; ctx.font = '900 120px "Comic Sans MS", sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText("MEINE FOTO-STORY", master.width / 2, titleSpace / 2); [0, 1, 2].forEach(i => { const panelDiv = document.getElementById('panel-' + i); const srcCanvas = panelDiv.querySelector('canvas'); const textDiv = panelDiv.querySelector('.bubble-text'); const panelOffsetX = border + (i * (panelW + gap)); const panelOffsetY = titleSpace; ctx.fillStyle = '#eee'; ctx.fillRect(panelOffsetX, panelOffsetY, panelW, panelH); if (srcCanvas.width > 0) { const srcW = srcCanvas.width; const srcH = srcCanvas.height; const srcRatio = srcW / srcH; let sW, sH, sX, sY; if (srcRatio > screenRatio) { sH = srcH; sW = srcH * screenRatio; sX = (srcW - sW) / 2; sY = 0; } else { sW = srcW; sH = srcW / screenRatio; sX = 0; sY = (srcH - sH) / 2; } ctx.drawImage(srcCanvas, sX, sY, sW, sH, panelOffsetX, panelOffsetY, panelW, panelH); } ctx.lineWidth = 20; ctx.strokeStyle = 'black'; ctx.strokeRect(panelOffsetX, panelOffsetY, panelW, panelH); ctx.fillStyle = 'black'; ctx.fillRect(panelOffsetX, panelOffsetY, 120, 120); ctx.fillStyle = 'white'; ctx.font = 'bold 80px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(i + 1, panelOffsetX + 60, panelOffsetY + 65); const text = textDiv.innerText.trim(); if (text) { const domPanelRect = panelDiv.getBoundingClientRect(); const domBubbleRect = textDiv.getBoundingClientRect(); const scaleX = panelW / domPanelRect.width; const scaleY = panelH / domPanelRect.height; const relX = domBubbleRect.left - domPanelRect.left; const relY = domBubbleRect.top - domPanelRect.top; const bubbleX = panelOffsetX + (relX * scaleX); const bubbleY = panelOffsetY + (relY * scaleY); drawRoundedRectBubble(ctx, bubbleX, bubbleY, text); } }); return master.toDataURL('image/png'); }
function drawRoundedRectBubble(ctx, x, y, text) { ctx.font = 'bold 50px "Comic Sans MS", sans-serif'; const padding = 50; const lineHeight = 60; const maxWidth = 500; const words = text.split(' '); let line = ''; let lines = []; for(let n = 0; n < words.length; n++) { let testLine = line + words[n] + ' '; let metrics = ctx.measureText(testLine); if (metrics.width > maxWidth && n > 0) { lines.push(line); line = words[n] + ' '; } else { line = testLine; } } lines.push(line); let maxLineWidth = 0; lines.forEach(l => { const m = ctx.measureText(l); if(m.width > maxLineWidth) maxLineWidth = m.width; }); const boxW = maxLineWidth + (padding * 2); const boxH = (lines.length * lineHeight) + (padding * 2); ctx.fillStyle = 'rgba(0,0,0,0.2)'; roundRect(ctx, x + 15, y + 15, boxW, boxH, 40, true, false); ctx.fillStyle = 'white'; ctx.strokeStyle = 'black'; ctx.lineWidth = 10; roundRect(ctx, x, y, boxW, boxH, 40, true, true); ctx.fillStyle = 'black'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; let ty = y + padding + (lineHeight/2); const centerX = x + (boxW/2); lines.forEach(l => { ctx.fillText(l.trim(), centerX, ty); ty += lineHeight; }); }
function roundRect(ctx, x, y, w, h, r, fill, stroke) { ctx.beginPath(); ctx.moveTo(x+r, y); ctx.arcTo(x+w, y, x+w, y+h, r); ctx.arcTo(x+w, y+h, x, y+h, r); ctx.arcTo(x, y+h, x, y, r); ctx.arcTo(x, y, x+w, y, r); ctx.closePath(); if (fill) ctx.fill(); if (stroke) ctx.stroke(); }
function downloadComic() { playSound('success'); const dataUrl = generateComicImage(); const link = document.createElement('a'); link.download = 'mein-comic.png'; link.href = dataUrl; link.click(); }
window.printComic = async function() { const btn = document.getElementById('btn-print'); const oldText = btn ? btn.innerText : "🖨️ DRUCKEN"; if(btn) { btn.innerText = "⏳ Sende..."; btn.disabled = true; } const dataUrl = generateComicImage(); try { if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DirectPrinter) { await window.Capacitor.Plugins.DirectPrinter.printBase64({ data: dataUrl, name: 'Comic-Studio' }); if(btn) btn.innerText = "✅ Gedruckt!"; playSound('success'); } else { console.warn("DirectPrinter Plugin nicht gefunden, nutze Browser Print."); const printImg = document.getElementById('print-img'); printImg.src = dataUrl; printImg.onload = () => { setTimeout(() => { window.print(); }, 500); }; if(btn) btn.innerText = "✅ OK"; playSound('success'); } } catch (error) { console.error("Druckfehler:", error); alert("Drucken fehlgeschlagen: " + error.message); if(btn) btn.innerText = "❌ Fehler"; } setTimeout(() => { if(btn) { btn.innerText = oldText; btn.disabled = false; } }, 3000); }
window.onload = startCamera;
</script>
</body>
</html>

137
public/apps/gif.html Normal file
View File

@@ -0,0 +1,137 @@
<!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>

276
public/apps/magic.html Normal file
View File

@@ -0,0 +1,276 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Magic Selfie</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>
<script src="../js/camera_utils.js"></script>
<script src="../js/selfie_segmentation.js"></script>
<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
html {
width: 100%; height: 100%;
background-color: #0f172a;
}
body {
/* Fixiertes Layout */
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;
transition: background-color 0.5s;
animation: fadeIn 0.4s ease-out;
box-sizing: border-box;
/* Body Padding */
padding-top: calc(10px + env(safe-area-inset-top));
padding-bottom: max(20px, env(safe-area-inset-bottom));
}
.no-scrollbar::-webkit-scrollbar { display: none; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
@media print {
body * { visibility: hidden; }
#print-container, #print-container * { visibility: visible; }
#print-container { position: fixed; left: 0; top: 0; width: 100%; height: 100%; display: flex !important; align-items: center; justify-content: center; background: white; z-index: 99999; }
#print-img { max-width: 100%; max-height: 100%; object-fit: contain; }
}
</style>
</head>
<body class="flex flex-col md:flex-row bg-slate-900" onclick="window.resetIdleTimer()" ontouchstart="window.resetIdleTimer()">
<button onclick="goHome()" class="absolute left-6 z-[100] bg-slate-800 text-white border-4 border-slate-600 hover:border-white px-6 py-3 rounded-full text-xl font-black shadow-2xl flex items-center gap-3 transition-transform active:scale-95 no-underline" style="top: calc(20px + env(safe-area-inset-top));">
🏠 MENÜ
</button>
<div class="absolute w-full text-center pointer-events-none z-50 hidden md:block" style="top: calc(20px + env(safe-area-inset-top));">
<h1 class="text-3xl font-black text-white drop-shadow-[0_4px_4px_rgba(0,0,0,0.8)] tracking-widest uppercase">
✨ MAGIC SELFIE
</h1>
</div>
<button onclick="window.toggleInfo(); playSound('click')" class="absolute right-6 z-[100] w-14 h-14 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-2xl" style="top: calc(20px + env(safe-area-inset-top));">
?
</button>
<div class="relative flex-1 bg-black flex flex-col items-center justify-center overflow-hidden h-[60%] md:h-full w-full rounded-b-3xl md:rounded-r-3xl md:rounded-bl-none shadow-2xl z-10">
<div class="relative w-full h-full flex items-center justify-center p-2 md:p-8 pb-24">
<video id="ai-video" autoplay playsinline muted class="hidden"></video>
<canvas id="ai-canvas" class="max-w-full max-h-full object-contain shadow-2xl rounded-lg"></canvas>
<div id="preview-overlay" class="absolute inset-0 bg-slate-900 z-50 hidden flex-col items-center justify-center">
<h2 class="text-3xl font-bold text-white mb-6 animate-bounce">Dein magisches Foto!</h2>
<div class="relative max-w-[90%] max-h-[60%] border-8 border-white shadow-2xl rounded-xl overflow-hidden bg-black">
<img id="preview-img-display" class="w-full h-full object-contain" src="">
</div>
<div class="flex gap-6 mt-8">
<button onclick="window.closePreview(); playSound('click')" class="bg-yellow-500 hover:bg-yellow-400 text-black font-black py-4 px-8 rounded-2xl text-xl shadow-xl active:scale-95 transition border-b-8 border-yellow-700 active:border-b-0 active:translate-y-2 flex items-center gap-2">🔄 NOCHMAL</button>
<button id="btn-print" onclick="window.printFromPreview(this)" class="bg-white text-slate-900 font-black py-4 px-8 rounded-2xl text-xl shadow-xl hover:bg-gray-100 active:scale-95 transition border-b-8 border-gray-400 active:border-b-0 active:translate-y-2 flex items-center gap-2">🖨️ DRUCKEN</button>
<button onclick="window.saveFromPreview()" class="bg-violet-600 text-white font-black py-4 px-8 rounded-2xl text-xl shadow-xl hover:bg-violet-500 active:scale-95 transition border-b-8 border-violet-800 active:border-b-0 active:translate-y-2 flex items-center gap-2">💾 FOTO</button>
</div>
</div>
</div>
<div id="ai-loading" class="absolute inset-0 bg-black/95 flex flex-col items-center justify-center z-40 p-4 text-center">
<div class="text-6xl animate-spin mb-4">🔮</div>
<div class="text-2xl font-bold mb-4">Lade KI...</div>
<div id="ai-debug-box" class="text-xs text-left font-mono bg-black/80 border border-gray-700 p-4 rounded-xl mt-4 max-w-lg w-full h-32 overflow-y-auto opacity-70"><div>[Log] App gestartet...</div></div>
<div class="mt-8 flex gap-6">
<button onclick="location.reload()" class="border-2 border-white/30 px-6 py-3 rounded-full hover:bg-white/10">Neu laden</button>
<button onclick="window.drawFallback(); document.getElementById('ai-loading').style.display='none'; playSound('click')" class="bg-red-600 px-6 py-3 rounded-full font-bold hover:bg-red-500">Ohne KI starten</button>
</div>
</div>
<div id="main-controls" class="absolute bottom-6 flex gap-4 z-30 w-full justify-center px-4">
<button onclick="window.switchCamera(); playSound('click')" class="w-20 bg-slate-700 text-white font-black py-4 rounded-2xl text-2xl shadow-xl hover:scale-105 active:scale-95 transition border-b-8 border-slate-900 active:border-b-0 active:translate-y-2 flex items-center justify-center" title="Kamera wechseln">🔄</button>
<button onclick="window.capturePhoto()" class="flex-1 max-w-[200px] bg-violet-600 text-white font-black py-4 rounded-2xl text-lg md:text-xl shadow-xl hover:scale-105 active:scale-95 transition border-b-8 border-violet-800 active:border-b-0 active:translate-y-2 flex items-center justify-center gap-2">✨ FOTO</button>
</div>
</div>
<div class="h-[40%] md:h-full md:w-1/3 min-w-[280px] max-w-md bg-slate-800 p-4 border-t-4 md:border-t-0 md:border-l-4 border-slate-700 overflow-y-auto no-scrollbar z-20 flex flex-col">
<h3 class="text-white font-black text-xl mb-4 text-center uppercase tracking-wider sticky top-0 bg-slate-800 pb-2 z-10 pt-2">🌍 Reiseziel</h3>
<div class="grid grid-cols-2 gap-3 pb-4">
<div onclick="window.setBg('../assets/weltraum.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/weltraum.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-indigo-900/50 z-0"><span class="text-5xl">🚀</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">WELTALL</span></div>
<div onclick="window.setBg('../assets/paris.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/paris.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-blue-900/50 z-0"><span class="text-5xl">🇫🇷</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">PARIS</span></div>
<div onclick="window.setBg('../assets/dschungel.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/dschungel.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-green-900/50 z-0"><span class="text-5xl">🌴</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">DSCHUNGEL</span></div>
<div onclick="window.setBg('../assets/unterwasser.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/unterwasser.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-cyan-900/50 z-0"><span class="text-5xl">🐠</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">OZEAN</span></div>
<div onclick="window.setBg('../assets/wolken.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/wolken.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-sky-500/50 z-0"><span class="text-5xl">☁️</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">HIMMEL</span></div>
<div onclick="window.setBg('../assets/schloss.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/schloss.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-purple-900/50 z-0"><span class="text-5xl">🏰</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">SCHLOSS</span></div>
<div onclick="window.setBg('../assets/dino.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/dino.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-amber-900/50 z-0"><span class="text-5xl">🦖</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">DINO</span></div>
<div onclick="window.setBg('../assets/stadion.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/stadion.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-green-800/50 z-0"><span class="text-5xl"></span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">STADION</span></div>
</div>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onclick="window.toggleInfo()">
<div class="bg-slate-800 border-4 border-violet-500 rounded-[2rem] max-w-2xl w-full p-8 shadow-2xl relative" onclick="event.stopPropagation()">
<button onclick="window.toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white hover:text-violet-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 rechts einen <b>Hintergrund</b>.</p>
<p>2. Die KI schneidet dich automatisch aus.</p>
<p>3. Drücke <b>FOTO</b> für eine Vorschau.</p>
<p>4. Dann kannst du <b>Drucken</b> oder nochmal probieren.</p>
</div>
<button onclick="window.toggleInfo(); playSound('click')" class="mt-8 w-full bg-violet-600 hover:bg-violet-500 text-white font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<div id="print-container" class="hidden"><img id="print-img" src=""></div>
<script>
// --- AUDIO SYSTEM ---
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) {
const baseSound = _soundCache[id];
if (baseSound) {
const soundClone = baseSound.cloneNode();
soundClone.volume = 1.0;
soundClone.play().catch(e => console.warn("Sound-Problem:", e));
}
};
// --- HOME FUNKTION ---
window.goHome = function() {
playSound('click');
setTimeout(() => {
window.location.href = '../index.html';
}, 300);
};
let idleTimer;
function resetIdleTimer() {
clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
if (document.getElementById('preview-overlay').classList.contains('hidden')) { window.location.href = '../index.html'; }
}, 90000);
}
['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');
if (modal.classList.contains('hidden')) { modal.classList.remove('hidden'); modal.classList.add('flex'); } else { modal.classList.add('hidden'); modal.classList.remove('flex'); }
}
window.logAi = function(msg, color="white") {
const box = document.getElementById('ai-debug-box');
if(box) { box.innerHTML += `<div><span style="color:${color}">${msg}</span></div>`; box.scrollTop = box.scrollHeight; }
console.log(msg);
}
let seg = null; const video = document.getElementById('ai-video'); const canvas = document.getElementById('ai-canvas'); const ctx = canvas.getContext('2d'); const loadingEl = document.getElementById('ai-loading');
let isRunning = false; let bgImage = new Image(); bgImage.src = "../assets/weltraum.jpg"; let bgColor = null; let loadTimer = null; let currentFacingMode = 'user'; let currentStream;
async function checkFiles() {
const files = ['selfie_segmentation.binarypb', 'selfie_segmentation_solution_simd_wasm_bin.wasm'];
for(let f of files) { try { const r = await fetch('../models/' + f, {method: 'GET', cache: 'no-store'}); if(!r.ok) window.logAi(`WARNUNG: ${f} fehlt evtl.`, "orange"); else window.logAi(`OK: ${f}`, "green"); } catch(e) { window.logAi(`Check ignorieren: ${e.message}`, "gray"); } }
}
async function initCam() {
checkFiles();
try {
if(currentStream) currentStream.getTracks().forEach(t => t.stop());
window.logAi("Starte Kamera...");
const stream = await navigator.mediaDevices.getUserMedia({ video: { width: {ideal: 1280}, height: {ideal: 720}, facingMode: currentFacingMode } });
currentStream = stream; video.srcObject = stream; await video.play();
if(currentFacingMode === 'user') { video.style.transform = 'scaleX(-1)'; } else { video.style.transform = 'scaleX(1)'; }
loadTimer = setTimeout(() => { window.logAi("Timeout! Klicke 'Ohne KI starten'.", "yellow"); }, 30000);
window.logAi("Warte auf Videodaten...");
waitForVideo();
} catch(e) { window.logAi("Kamera-Fehler: " + e.message, "red"); alert("Kamera konnte nicht gestartet werden."); }
}
function waitForVideo() {
if (video.readyState >= 2 && video.videoWidth > 0) { window.logAi("Video läuft. Starte KI..."); setupAI(); } else { requestAnimationFrame(waitForVideo); }
}
window.switchCamera = function() { currentFacingMode = (currentFacingMode === 'user') ? 'environment' : 'user'; initCam(); }
function setupAI() {
if(seg) { loadingEl.style.display = 'none'; isRunning = true; processLoop(); return; }
window.logAi("Lade KI-Modell...", "cyan");
try {
seg = new SelfieSegmentation({locateFile: (file) => `../models/${file}`});
seg.setOptions({ modelSelection: 1, selfieMode: true });
seg.onResults(onResults);
isRunning = true; processLoop();
} catch(e) { window.logAi("KI Init Fehler: " + e, "red"); }
}
async function processLoop() { if(!isRunning) return; if(video.videoWidth > 0 && !video.paused) { try { await seg.send({image: video}); } catch(e) {} } requestAnimationFrame(processLoop); }
function onResults(results) {
if(loadingEl.style.display !== 'none') { clearTimeout(loadTimer); loadingEl.style.display = 'none'; window.logAi("KI läuft!", "green"); }
canvas.width = video.videoWidth; canvas.height = video.videoHeight;
ctx.save(); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(results.segmentationMask, 0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'source-in'; ctx.drawImage(results.image, 0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'destination-over';
if (bgColor) { ctx.fillStyle = bgColor; ctx.fillRect(0, 0, canvas.width, canvas.height); }
else if (bgImage.complete && bgImage.naturalWidth > 0) { ctx.drawImage(bgImage, 0, 0, canvas.width, canvas.height); }
else { ctx.fillStyle = '#111'; ctx.fillRect(0, 0, canvas.width, canvas.height); }
ctx.restore();
}
window.setBg = function(path, color = null) { if (color) { bgColor = color; } else { bgColor = null; bgImage.src = path; } }
window.capturePhoto = function() {
playSound('shutter');
flashEffect(); isRunning = false; const dataUrl = canvas.toDataURL('image/png');
document.getElementById('preview-img-display').src = dataUrl;
document.getElementById('preview-overlay').classList.remove('hidden'); document.getElementById('preview-overlay').style.display = 'flex';
document.getElementById('main-controls').classList.add('hidden');
}
window.closePreview = function() {
document.getElementById('preview-overlay').classList.add('hidden'); document.getElementById('preview-overlay').style.display = 'none';
document.getElementById('main-controls').classList.remove('hidden'); isRunning = true; processLoop();
}
window.saveFromPreview = function() {
playSound('success');
const dataUrl = document.getElementById('preview-img-display').src; const link = document.createElement('a'); link.download = 'magic-selfie.png'; link.href = dataUrl; link.click();
}
window.printFromPreview = async function(el) {
playSound('success');
const btn = el || document.getElementById('btn-print'); const oldText = btn ? btn.innerText : "🖨️ DRUCKEN";
if (btn) { btn.innerText = "⏳ Sende..."; btn.disabled = true; }
const dataUrl = document.getElementById('preview-img-display').src;
try {
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DirectPrinter) {
await window.Capacitor.Plugins.DirectPrinter.printBase64({ data: dataUrl, name: 'Magic-Selfie' });
if (btn) btn.innerText = "✅ Gedruckt!";
} else {
console.warn("DirectPrinter Plugin nicht gefunden, nutze Browser Print.");
document.getElementById('print-img').src = dataUrl; setTimeout(() => window.print(), 250);
if (btn) btn.innerText = "✅ OK";
}
} catch (error) { console.error("Druckfehler:", error); alert("Drucken fehlgeschlagen: " + error.message); if (btn) btn.innerText = "❌ Fehler"; }
setTimeout(() => { if (btn) { btn.innerText = oldText; btn.disabled = false; } }, 3000);
}
window.takePhoto = function() { window.capturePhoto(); }
function flashEffect() { canvas.style.filter = "brightness(10)"; setTimeout(() => canvas.style.filter = "none", 100); }
window.drawFallback = function() {
isRunning = false; loadingEl.style.display = 'none'; window.logAi("KI deaktiviert.");
function loop() {
canvas.width = video.videoWidth; canvas.height = video.videoHeight;
ctx.save(); if(currentFacingMode === 'user') ctx.scale(-1, 1);
ctx.drawImage(video, currentFacingMode === 'user' ? -canvas.width : 0, 0); ctx.restore();
ctx.fillStyle = "white"; ctx.font = "20px sans-serif"; ctx.fillText("Ohne KI", 20, 40);
requestAnimationFrame(loop);
} loop();
}
initCam();
</script>
</body>
</html>

258
public/apps/magic_old.html Normal file
View File

@@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Magic Booth</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="../js/tailwind.js"></script>
<script src="../js/camera_utils.js"></script>
<script src="../js/selfie_segmentation.js"></script>
<style>
/* HIER gehört das CSS hin (unsichtbar für den Nutzer) */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
body {
background-color: #facc15;
overflow: hidden;
user-select: none;
touch-action: none;
transition: background-color 0.5s;
animation: fadeIn 0.4s ease-out; /* Die Animation */
}
body { background-color: #0f172a; color: white; overflow: hidden; user-select: none; touch-action: none; }
.no-scrollbar::-webkit-scrollbar { display: none; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
@media print {
body * { visibility: hidden; }
#print-container, #print-container * { visibility: visible; }
#print-container { position: fixed; left: 0; top: 0; width: 100%; height: 100%; display: flex !important; align-items: center; justify-content: center; background: white; z-index: 99999; }
#print-img { max-width: 100%; max-height: 100%; object-fit: contain; }
}
</style>
</head>
<body class="h-screen w-screen flex flex-col md:flex-row p-0 m-0 bg-slate-900" onclick="window.resetIdleTimer()" ontouchstart="window.resetIdleTimer()">
<button onclick="goHome()" class="absolute top-6 left-6 z-[100] bg-slate-800 text-white border-4 border-slate-600 hover:border-white px-8 py-4 rounded-full text-2xl font-black shadow-2xl flex items-center gap-3 transition-transform active:scale-95 no-underline">
🏠 MENÜ
</button>
<button onclick="window.toggleInfo(); playSound('click')" class="absolute top-6 right-6 z-[100] w-14 h-14 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-2xl">
?
</button>
<div class="relative flex-1 bg-black flex flex-col items-center justify-center overflow-hidden h-[60%] md:h-full w-full">
<div class="relative w-full h-full flex items-center justify-center p-2 md:p-8 pb-24">
<video id="ai-video" autoplay playsinline muted class="hidden"></video>
<canvas id="ai-canvas" class="max-w-full max-h-full object-contain shadow-2xl rounded-lg"></canvas>
<div id="preview-overlay" class="absolute inset-0 bg-slate-900 z-50 hidden flex-col items-center justify-center">
<h2 class="text-3xl font-bold text-white mb-6 animate-bounce">Dein magisches Foto!</h2>
<div class="relative max-w-[90%] max-h-[60%] border-8 border-white shadow-2xl rounded-xl overflow-hidden bg-black">
<img id="preview-img-display" class="w-full h-full object-contain" src="">
</div>
<div class="flex gap-6 mt-8">
<button onclick="window.closePreview(); playSound('click')" class="bg-yellow-500 hover:bg-yellow-400 text-black font-black py-4 px-8 rounded-2xl text-xl shadow-xl active:scale-95 transition border-b-8 border-yellow-700 active:border-b-0 active:translate-y-2 flex items-center gap-2">🔄 NOCHMAL</button>
<button id="btn-print" onclick="window.printFromPreview(this)" class="bg-white text-slate-900 font-black py-4 px-8 rounded-2xl text-xl shadow-xl hover:bg-gray-100 active:scale-95 transition border-b-8 border-gray-400 active:border-b-0 active:translate-y-2 flex items-center gap-2">🖨️ DRUCKEN</button>
<button onclick="window.saveFromPreview()" class="bg-violet-600 text-white font-black py-4 px-8 rounded-2xl text-xl shadow-xl hover:bg-violet-500 active:scale-95 transition border-b-8 border-violet-800 active:border-b-0 active:translate-y-2 flex items-center gap-2">💾 FOTO</button>
</div>
</div>
</div>
<div id="ai-loading" class="absolute inset-0 bg-black/95 flex flex-col items-center justify-center z-40 p-4 text-center">
<div class="text-6xl animate-spin mb-4">🔮</div>
<div class="text-2xl font-bold mb-4">Lade KI...</div>
<div id="ai-debug-box" class="text-xs text-left font-mono bg-black/80 border border-gray-700 p-4 rounded-xl mt-4 max-w-lg w-full h-32 overflow-y-auto opacity-70"><div>[Log] App gestartet...</div></div>
<div class="mt-8 flex gap-6">
<button onclick="location.reload()" class="border-2 border-white/30 px-6 py-3 rounded-full hover:bg-white/10">Neu laden</button>
<button onclick="window.drawFallback(); document.getElementById('ai-loading').style.display='none'; playSound('click')" class="bg-red-600 px-6 py-3 rounded-full font-bold hover:bg-red-500">Ohne KI starten</button>
</div>
</div>
<div id="main-controls" class="absolute bottom-6 flex gap-4 z-30 w-full justify-center px-4">
<button onclick="window.switchCamera(); playSound('click')" class="w-20 bg-slate-700 text-white font-black py-4 rounded-2xl text-2xl shadow-xl hover:scale-105 active:scale-95 transition border-b-8 border-slate-900 active:border-b-0 active:translate-y-2 flex items-center justify-center" title="Kamera wechseln">🔄</button>
<button onclick="window.capturePhoto()" class="flex-1 max-w-[200px] bg-violet-600 text-white font-black py-4 rounded-2xl text-lg md:text-xl shadow-xl hover:scale-105 active:scale-95 transition border-b-8 border-violet-800 active:border-b-0 active:translate-y-2 flex items-center justify-center gap-2">✨ FOTO</button>
</div>
</div>
<div class="h-[40%] md:h-full md:w-1/3 min-w-[280px] max-w-md bg-slate-800 p-4 border-t-4 md:border-t-0 md:border-l-4 border-slate-700 overflow-y-auto no-scrollbar z-20 flex flex-col">
<h3 class="text-white font-black text-xl mb-4 text-center uppercase tracking-wider sticky top-0 bg-slate-800 pb-2 z-10 pt-2">🌍 Reiseziel</h3>
<div class="grid grid-cols-2 gap-3 pb-4">
<div onclick="window.setBg('../assets/weltraum.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/weltraum.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-indigo-900/50 z-0"><span class="text-5xl">🚀</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">WELTALL</span></div>
<div onclick="window.setBg('../assets/paris.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/paris.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-blue-900/50 z-0"><span class="text-5xl">🇫🇷</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">PARIS</span></div>
<div onclick="window.setBg('../assets/dschungel.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/dschungel.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-green-900/50 z-0"><span class="text-5xl">🌴</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">DSCHUNGEL</span></div>
<div onclick="window.setBg('../assets/unterwasser.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/unterwasser.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-cyan-900/50 z-0"><span class="text-5xl">🐠</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">OZEAN</span></div>
<div onclick="window.setBg('../assets/wolken.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/wolken.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-sky-500/50 z-0"><span class="text-5xl">☁️</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">HIMMEL</span></div>
<div onclick="window.setBg('../assets/schloss.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/schloss.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-purple-900/50 z-0"><span class="text-5xl">🏰</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">SCHLOSS</span></div>
<div onclick="window.setBg('../assets/dino.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/dino.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-amber-900/50 z-0"><span class="text-5xl">🦖</span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">DINO</span></div>
<div onclick="window.setBg('../assets/stadion.jpg'); playSound('click')" class="aspect-square bg-slate-700 rounded-2xl cursor-pointer border-4 border-transparent hover:border-violet-400 relative group overflow-hidden active:scale-95 transition shadow-lg"><img src="../assets/stadion.jpg" class="w-full h-full object-cover" onerror="this.style.display='none'"><div class="absolute inset-0 flex items-center justify-center bg-green-800/50 z-0"><span class="text-5xl"></span></div><span class="absolute bottom-2 left-3 text-xs font-black drop-shadow-md z-10 text-white uppercase tracking-wider">STADION</span></div>
</div>
</div>
<div id="info-modal" class="hidden fixed inset-0 z-[200] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onclick="window.toggleInfo()">
<div class="bg-slate-800 border-4 border-violet-500 rounded-[2rem] max-w-2xl w-full p-8 shadow-2xl relative" onclick="event.stopPropagation()">
<button onclick="window.toggleInfo(); playSound('click')" class="absolute top-6 right-6 text-white hover:text-violet-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 rechts einen <b>Hintergrund</b>.</p>
<p>2. Die KI schneidet dich automatisch aus.</p>
<p>3. Drücke <b>FOTO</b> für eine Vorschau.</p>
<p>4. Dann kannst du <b>Drucken</b> oder nochmal probieren.</p>
</div>
<button onclick="window.toggleInfo(); playSound('click')" class="mt-8 w-full bg-violet-600 hover:bg-violet-500 text-white font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<div id="print-container" class="hidden"><img id="print-img" src=""></div>
<script>
// --- ROBUSTES AUDIO SYSTEM (ANTI-ABSTURZ) ---
const _soundCache = {
'click': new Audio('../assets/sounds/click.mp3'),
'shutter': new Audio('../assets/sounds/shutter.mp3'),
'success': new Audio('../assets/sounds/success.mp3')
};
// Sounds vorladen
Object.values(_soundCache).forEach(s => s.load());
// Abspiel-Funktion
window.playSound = function(id) {
const baseSound = _soundCache[id];
if (baseSound) {
const soundClone = baseSound.cloneNode();
soundClone.volume = 1.0;
soundClone.play().catch(e => console.warn("Sound-Problem:", e));
}
};
// --- HOME FUNKTION (Diese fehlte wahrscheinlich!) ---
window.goHome = function() {
playSound('click');
// Kleine Verzögerung, damit der Sound noch zu hören ist
setTimeout(() => {
window.location.href = '../index.html';
}, 300);
};
let idleTimer;
function resetIdleTimer() {
clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
if (document.getElementById('preview-overlay').classList.contains('hidden')) { window.location.href = '../index.html'; }
}, 90000);
}
['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');
if (modal.classList.contains('hidden')) { modal.classList.remove('hidden'); modal.classList.add('flex'); } else { modal.classList.add('hidden'); modal.classList.remove('flex'); }
}
window.logAi = function(msg, color="white") {
const box = document.getElementById('ai-debug-box');
if(box) { box.innerHTML += `<div><span style="color:${color}">${msg}</span></div>`; box.scrollTop = box.scrollHeight; }
console.log(msg);
}
let seg = null; const video = document.getElementById('ai-video'); const canvas = document.getElementById('ai-canvas'); const ctx = canvas.getContext('2d'); const loadingEl = document.getElementById('ai-loading');
let isRunning = false; let bgImage = new Image(); bgImage.src = "../assets/weltraum.jpg"; let bgColor = null; let loadTimer = null; let currentFacingMode = 'user'; let currentStream;
async function checkFiles() {
const files = ['selfie_segmentation.binarypb', 'selfie_segmentation_solution_simd_wasm_bin.wasm'];
for(let f of files) { try { const r = await fetch('../models/' + f, {method: 'GET', cache: 'no-store'}); if(!r.ok) window.logAi(`WARNUNG: ${f} fehlt evtl.`, "orange"); else window.logAi(`OK: ${f}`, "green"); } catch(e) { window.logAi(`Check ignorieren: ${e.message}`, "gray"); } }
}
async function initCam() {
checkFiles();
try {
if(currentStream) currentStream.getTracks().forEach(t => t.stop());
window.logAi("Starte Kamera...");
const stream = await navigator.mediaDevices.getUserMedia({ video: { width: {ideal: 1280}, height: {ideal: 720}, facingMode: currentFacingMode } });
currentStream = stream; video.srcObject = stream; await video.play();
if(currentFacingMode === 'user') { video.style.transform = 'scaleX(-1)'; } else { video.style.transform = 'scaleX(1)'; }
loadTimer = setTimeout(() => { window.logAi("Timeout! Klicke 'Ohne KI starten'.", "yellow"); }, 30000);
window.logAi("Warte auf Videodaten...");
waitForVideo();
} catch(e) { window.logAi("Kamera-Fehler: " + e.message, "red"); alert("Kamera konnte nicht gestartet werden."); }
}
function waitForVideo() {
if (video.readyState >= 2 && video.videoWidth > 0) { window.logAi("Video läuft. Starte KI..."); setupAI(); } else { requestAnimationFrame(waitForVideo); }
}
window.switchCamera = function() { currentFacingMode = (currentFacingMode === 'user') ? 'environment' : 'user'; initCam(); }
function setupAI() {
if(seg) { loadingEl.style.display = 'none'; isRunning = true; processLoop(); return; }
window.logAi("Lade KI-Modell...", "cyan");
try {
seg = new SelfieSegmentation({locateFile: (file) => `../models/${file}`});
seg.setOptions({ modelSelection: 1, selfieMode: true });
seg.onResults(onResults);
isRunning = true; processLoop();
} catch(e) { window.logAi("KI Init Fehler: " + e, "red"); }
}
async function processLoop() { if(!isRunning) return; if(video.videoWidth > 0 && !video.paused) { try { await seg.send({image: video}); } catch(e) {} } requestAnimationFrame(processLoop); }
function onResults(results) {
if(loadingEl.style.display !== 'none') { clearTimeout(loadTimer); loadingEl.style.display = 'none'; window.logAi("KI läuft!", "green"); }
canvas.width = video.videoWidth; canvas.height = video.videoHeight;
ctx.save(); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(results.segmentationMask, 0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'source-in'; ctx.drawImage(results.image, 0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'destination-over';
if (bgColor) { ctx.fillStyle = bgColor; ctx.fillRect(0, 0, canvas.width, canvas.height); }
else if (bgImage.complete && bgImage.naturalWidth > 0) { ctx.drawImage(bgImage, 0, 0, canvas.width, canvas.height); }
else { ctx.fillStyle = '#111'; ctx.fillRect(0, 0, canvas.width, canvas.height); }
ctx.restore();
}
window.setBg = function(path, color = null) { if (color) { bgColor = color; } else { bgColor = null; bgImage.src = path; } }
window.capturePhoto = function() {
playSound('shutter'); // SOUND
flashEffect(); isRunning = false; const dataUrl = canvas.toDataURL('image/png');
document.getElementById('preview-img-display').src = dataUrl;
document.getElementById('preview-overlay').classList.remove('hidden'); document.getElementById('preview-overlay').style.display = 'flex';
document.getElementById('main-controls').classList.add('hidden');
}
window.closePreview = function() {
document.getElementById('preview-overlay').classList.add('hidden'); document.getElementById('preview-overlay').style.display = 'none';
document.getElementById('main-controls').classList.remove('hidden'); isRunning = true; processLoop();
}
window.saveFromPreview = function() {
playSound('success'); // SOUND
const dataUrl = document.getElementById('preview-img-display').src; const link = document.createElement('a'); link.download = 'magic-booth.png'; link.href = dataUrl; link.click();
}
window.printFromPreview = async function(el) {
playSound('success'); // SOUND
const btn = el || document.getElementById('btn-print'); const oldText = btn ? btn.innerText : "🖨️ DRUCKEN";
if (btn) { btn.innerText = "⏳ Sende..."; btn.disabled = true; }
const dataUrl = document.getElementById('preview-img-display').src;
try {
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DirectPrinter) {
await window.Capacitor.Plugins.DirectPrinter.printBase64({ data: dataUrl, name: 'Magic-Booth' });
if (btn) btn.innerText = "✅ Gedruckt!";
} else {
console.warn("DirectPrinter Plugin nicht gefunden, nutze Browser Print.");
document.getElementById('print-img').src = dataUrl; setTimeout(() => window.print(), 250);
if (btn) btn.innerText = "✅ OK";
}
} catch (error) { console.error("Druckfehler:", error); alert("Drucken fehlgeschlagen: " + error.message); if (btn) btn.innerText = "❌ Fehler"; }
setTimeout(() => { if (btn) { btn.innerText = oldText; btn.disabled = false; } }, 3000);
}
window.takePhoto = function() { window.capturePhoto(); }
function flashEffect() { canvas.style.filter = "brightness(10)"; setTimeout(() => canvas.style.filter = "none", 100); }
window.drawFallback = function() {
isRunning = false; loadingEl.style.display = 'none'; window.logAi("KI deaktiviert.");
function loop() {
canvas.width = video.videoWidth; canvas.height = video.videoHeight;
ctx.save(); if(currentFacingMode === 'user') ctx.scale(-1, 1);
ctx.drawImage(video, currentFacingMode === 'user' ? -canvas.width : 0, 0); ctx.restore();
ctx.fillStyle = "white"; ctx.font = "20px sans-serif"; ctx.fillText("Ohne KI", 20, 40);
requestAnimationFrame(loop);
} loop();
}
initCam();
</script>
</body>
</html>

154
public/apps/news.html Normal file
View File

@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>News Studio</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: manipulation;
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));
}
.no-scrollbar::-webkit-scrollbar { display: none; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
.animate-blink { animation: blink 1.5s infinite; }
@keyframes blink { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } }
.ticker-wrap { width: 100%; overflow: hidden; background-color: #fbbf24; color: black; font-weight: 900; white-space: nowrap; box-sizing: border-box; }
.ticker { display: inline-block; padding-left: 100%; animation: ticker 25s linear infinite; }
@keyframes ticker { 0% { transform: translate3d(0, 0, 0); } 100% { transform: translate3d(-100%, 0, 0); } }
.editable-input { background: transparent; border: none; color: inherit; width: 100%; text-align: center; outline: none; font-family: inherit; text-transform: uppercase; text-overflow: ellipsis; }
.editable-input:focus { background: rgba(255,255,255,0.1); border-radius: 4px; }
</style>
</head>
<body class="flex flex-col"
onclick="if(window.resetIdleTimer) window.resetIdleTimer()"
ontouchstart="if(window.resetIdleTimer) window.resetIdleTimer()">
<div class="flex-none flex justify-between items-center z-50 px-4 relative bg-slate-900 shadow-md 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-blue-400 font-sans tracking-tighter drop-shadow-[0_4px_4px_rgba(0,0,0,0.8)] uppercase italic transform -skew-x-6">
📰 NEWS
</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 relative w-full overflow-hidden bg-black shadow-inner">
<div class="absolute inset-0 z-0">
<video id="video" autoplay playsinline muted class="w-full h-full object-cover opacity-90"></video>
<canvas id="canvas" class="hidden"></canvas>
</div>
<div class="absolute inset-0 z-20 flex flex-col justify-between pointer-events-none pb-4">
<div class="w-full flex justify-end p-4 md:p-6">
<div class="flex items-center gap-2 bg-red-600/90 px-4 py-2 rounded-lg shadow-lg border-2 border-red-400 pointer-events-auto transform rotate-1 hover:rotate-0 transition-transform">
<div class="w-3 h-3 md:w-4 md:h-4 bg-white rounded-full animate-blink"></div>
<span class="font-black text-lg md:text-xl tracking-widest text-white">LIVE</span>
</div>
</div>
<div class="flex flex-col w-full pointer-events-auto items-center">
<div class="w-[90%] md:max-w-5xl bg-blue-800/95 border-l-8 border-blue-500 p-2 md:p-4 mb-2 shadow-2xl transform -skew-x-6 relative group hover:bg-blue-700/95 transition-colors">
<div class="text-blue-300 text-xs md:text-sm font-bold uppercase tracking-[0.2em] mb-1 ml-2 not-italic transform skew-x-6">Breaking News</div>
<input type="text" id="headline-input" value="SENSATION IM JUGENDZENTRUM!" class="editable-input text-2xl md:text-4xl font-black text-white drop-shadow-md transform skew-x-6 placeholder-blue-300">
</div>
<div class="self-start ml-4 md:ml-16 w-auto max-w-[80%] bg-red-600/95 text-white font-bold px-6 py-2 transform -skew-x-6 shadow-xl text-lg md:text-xl border-l-4 border-red-300 mb-4 hover:bg-red-500/95 transition-colors">
<input type="text" value="🔴 VOR ORT: MEDIENSTATION" class="editable-input w-full text-left transform skew-x-6">
</div>
<div class="ticker-wrap bg-yellow-400 py-2 md:py-3 border-y-4 border-black shadow-xl mb-4 md:mb-6">
<div class="ticker text-xl md:text-3xl text-black">
+++ WETTER: ES REGNET KONFETTI +++ SCHULE FÄLLT HEUTE AUS +++ EISCREME FÜR ALLE +++ KATZE WIRD BÜRGERMEISTER +++ EXTRA BLATT +++
</div>
</div>
<div class="flex justify-center gap-4 md:gap-6 pb-2 md:pb-4 w-full">
<button onclick="switchCamera(); playSound('click')" class="bg-slate-800/90 hover:bg-slate-700 text-white w-14 h-14 md:w-16 md:h-16 rounded-full border-2 border-slate-500 shadow-xl backdrop-blur flex items-center justify-center active:scale-95 transition">
<span class="text-2xl md:text-3xl">🔄</span>
</button>
<button onclick="capturePreview()" class="bg-white hover:bg-gray-200 text-black font-black h-14 md:h-16 px-8 md:px-10 rounded-full border-b-8 border-gray-400 active:border-b-0 active:translate-y-2 shadow-2xl text-lg md:text-xl flex items-center gap-3 transition active:scale-95">
<span>📸</span> FOTO
</button>
</div>
</div>
</div>
</div>
<div id="preview-modal" class="hidden fixed inset-0 z-[100] bg-black/90 flex flex-col items-center justify-center p-4 backdrop-blur-md">
<button onclick="goHome()" class="absolute top-6 left-6 z-50 bg-blue-900/90 backdrop-blur border-4 border-blue-500 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 text-white">
🏠 MENÜ
</button>
<h2 class="text-white text-3xl font-black mb-4">DEIN FOTO</h2>
<div class="relative w-full max-w-4xl h-auto border-8 border-white shadow-2xl rounded-lg overflow-hidden bg-black mb-8">
<img id="preview-img" src="" class="w-full h-auto object-contain">
</div>
<div class="flex gap-6">
<button onclick="closePreview(); playSound('click')" class="bg-red-600 hover:bg-red-500 text-white font-black py-4 px-8 rounded-2xl text-xl shadow-lg border-b-8 border-red-800 active:border-b-0 active:translate-y-2 transition flex items-center gap-2">
🗑️ LÖSCHEN
</button>
<button id="btn-print-confirm" onclick="confirmPrint()" class="bg-green-600 hover:bg-green-500 text-white font-black py-4 px-12 rounded-2xl text-xl shadow-lg border-b-8 border-green-800 active:border-b-0 active:translate-y-2 transition flex items-center gap-2">
🖨️ DRUCKEN
</button>
</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-blue-500 rounded-[2rem] max-w-lg 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-blue-400 text-4xl font-bold transition"></button>
<h2 class="text-3xl font-black text-white mb-6 border-b border-slate-600 pb-4">📰 NEWS STUDIO</h2>
<ul class="text-slate-300 space-y-3 mb-8 list-disc pl-6 text-lg">
<li>Tippe auf die <b>blaue Schlagzeile</b> und schreibe deinen Text.</li>
<li>Tippe auf den <b>roten Ort</b>.</li>
<li>Drücke auf <b>FOTO</b> für eine Vorschau.</li>
<li>Wenn es dir gefällt, drücke auf <b>DRUCKEN</b>.</li>
</ul>
<button onclick="toggleInfo(); playSound('click')" class="w-full bg-blue-600 hover:bg-blue-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(() => { window.location.href = '../index.html'; }, 120000); };
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, window.resetIdleTimer, {passive: true})); window.resetIdleTimer();
function toggleInfo() { const m = document.getElementById('info-modal'); m.classList.toggle('hidden'); m.classList.toggle('flex'); }
let video = document.getElementById('video'); let currentStream; let currentFacingMode = 'user';
async function initCam() { try { if (currentStream && currentStream.getTracks) 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.style.transform = (currentFacingMode === 'user') ? 'scaleX(-1)' : 'scaleX(1)'; } catch (err) { console.error("Kamera Fehler:", err); } }
function switchCamera() { currentFacingMode = (currentFacingMode === 'user') ? 'environment' : 'user'; initCam(); }
function capturePreview() { playSound('shutter'); const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); canvas.width = video.videoWidth || 1280; canvas.height = video.videoHeight || 720; 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(); const headline = document.getElementById('headline-input').value.toUpperCase(); ctx.fillStyle = "rgba(30, 64, 175, 0.95)"; const boxH = 140; const boxY = canvas.height - 220; const boxMargin = 60; const boxW = canvas.width - (boxMargin * 2); ctx.save(); ctx.translate(boxMargin, boxY); ctx.transform(1, 0, -0.1, 1, 0, 0); ctx.fillRect(0, 0, boxW, boxH); ctx.fillStyle = "#3b82f6"; ctx.fillRect(0, 0, 25, boxH); ctx.restore(); ctx.fillStyle = "white"; ctx.font = "900 55px sans-serif"; ctx.textAlign = "center"; ctx.fillText(headline, canvas.width / 2, boxY + 90, boxW - 100); ctx.fillStyle = "#93c5fd"; ctx.font = "bold 24px sans-serif"; ctx.textAlign = "left"; ctx.fillText("BREAKING NEWS", boxMargin + 40, boxY + 35); const redW = 550; const redY = boxY - 65; const redX = boxMargin + 40; ctx.save(); ctx.translate(redX, redY); ctx.transform(1, 0, -0.1, 1, 0, 0); ctx.fillStyle = "rgba(220, 38, 38, 0.95)"; ctx.fillRect(0, 0, redW, 60); ctx.fillStyle = "#fca5a5"; ctx.fillRect(0, 0, 10, 60); ctx.restore(); ctx.fillStyle = "white"; ctx.font = "bold 30px sans-serif"; ctx.textAlign = "left"; ctx.fillText("🔴 VOR ORT: MEDIENSTATION", redX + 30, redY + 40); ctx.fillStyle = "#fbbf24"; ctx.fillRect(0, canvas.height - 70, canvas.width, 70); ctx.fillStyle = "black"; ctx.font = "900 35px sans-serif"; ctx.textAlign = "left"; ctx.fillText("+++ WETTER: ES REGNET KONFETTI +++ SCHULE FÄLLT HEUTE AUS +++ EXTRA BLATT +++", 20, canvas.height - 22); ctx.fillStyle = "#dc2626"; ctx.fillRect(canvas.width - 160, 40, 120, 50); ctx.fillStyle = "white"; ctx.font = "900 30px sans-serif"; ctx.textAlign = "center"; ctx.fillText("LIVE", canvas.width - 100, 75); const dataUrl = canvas.toDataURL('image/png'); document.getElementById('preview-img').src = dataUrl; document.getElementById('preview-modal').classList.remove('hidden'); document.getElementById('preview-modal').classList.add('flex'); }
function closePreview() { document.getElementById('preview-modal').classList.add('hidden'); document.getElementById('preview-modal').classList.remove('flex'); }
async function confirmPrint() { const btn = document.getElementById('btn-print-confirm'); btn.innerText = "⏳ DRUCKE..."; const img = document.getElementById('preview-img'); const dataUrl = img.src; try { if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DirectPrinter) { await window.Capacitor.Plugins.DirectPrinter.printBase64({ data: dataUrl, name: 'News-Flash' }); } else { const link = document.createElement('a'); link.download = 'breaking-news.png'; link.href = dataUrl; link.click(); } playSound('success'); setTimeout(() => { closePreview(); btn.innerText = "🖨️ DRUCKEN"; }, 1000); } catch(e) { console.error(e); alert("Druck-Fehler: " + e.message); btn.innerText = "❌ FEHLER"; } }
window.onload = initCam;
</script>
</body>
</html>

118
public/apps/pixel.html Normal file
View File

@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Pixel Labor</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;
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));
}
.pixel-grid { display: grid; grid-template-columns: repeat(16, 1fr); grid-template-rows: repeat(16, 1fr); gap: 1px; background: #334155; border: 8px solid #10b981; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7); border-radius: 1rem; overflow: hidden; touch-action: none; width: 100%; height: 100%; aspect-ratio: 1 / 1; }
.cell { background-color: white; cursor: pointer; transition: background-color 0.05s; }
.no-scrollbar::-webkit-scrollbar { display: none; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
@media print { body * { visibility: hidden; } #print-container, #print-container * { visibility: visible; } #print-container { position: fixed; left: 0; top: 0; width: 100%; height: 100%; display: flex !important; align-items: center; justify-content: center; background: white; z-index: 99999; } #print-img { max-width: 90%; max-height: 90%; object-fit: contain; border: 4px solid black; image-rendering: pixelated; } }
</style>
</head>
<body class="flex flex-col p-4"
onclick="if(window.resetIdleTimer) window.resetIdleTimer()"
ontouchstart="if(window.resetIdleTimer) window.resetIdleTimer()">
<div class="flex-none flex justify-between items-center shrink-0 mb-2 px-2 relative z-50 h-16">
<button onclick="goHome()" class="bg-slate-800 text-white border-4 border-slate-600 hover:border-white px-6 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-emerald-400 font-mono tracking-wider drop-shadow-[0_4px_4px_rgba(0,0,0,0.8)] uppercase">
&lt;PIXEL&gt;
</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-col lg:flex-row items-center justify-center gap-4 min-h-0 w-full max-w-[95rem] mx-auto pb-1">
<div class="relative w-full max-w-[650px] flex-shrink-0 flex items-center justify-center p-1 min-h-0 h-full">
<div id="grid" class="pixel-grid max-h-full"></div>
</div>
<div class="bg-slate-800 p-3 lg:p-4 rounded-[1.5rem] border-4 border-slate-700 flex flex-row lg:flex-col gap-3 shadow-2xl w-full lg:w-auto h-auto lg:h-full justify-between overflow-x-auto lg:overflow-visible lg:overflow-y-auto no-scrollbar">
<div class="grid grid-cols-4 lg:grid-cols-2 gap-2 flex-shrink-0 justify-items-center" id="palette">
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-black border-4 border-slate-600 cursor-pointer active:scale-90 transition ring-4 ring-white shadow-lg" data-color="#000000" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-white border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#ffffff" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-red-500 border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#ef4444" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-blue-500 border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#3b82f6" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-green-500 border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#22c55e" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-yellow-400 border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#facc15" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-purple-500 border-4 border-slate-600 cursor-pointer active:scale-90 transition shadow-lg" data-color="#a855f7" onclick="setColor(this)"></div>
<div class="color-btn w-12 h-12 lg:w-14 lg:h-14 rounded-xl bg-slate-600 border-4 border-slate-500 cursor-pointer active:scale-90 transition flex items-center justify-center text-2xl shadow-lg" data-color="white" onclick="setColor(this)" title="Radierer">🧽</div>
</div>
<div class="h-1 w-full bg-slate-700 my-1 lg:block hidden rounded-full shrink-0"></div>
<div class="w-1 h-full bg-slate-700 mx-2 lg:hidden block rounded-full shrink-0"></div>
<div class="flex flex-col gap-2 w-full min-w-[160px] shrink-0">
<div class="flex gap-2">
<button onclick="downloadArt()" class="flex-1 bg-emerald-600 hover:bg-emerald-500 text-white py-3 px-2 rounded-xl font-black text-base shadow-lg active:translate-y-1 transition border-b-4 border-emerald-800 active:border-b-0 flex items-center justify-center gap-1">💾 SAVE</button>
<button id="btn-print" onclick="window.printArt(this)" class="flex-1 bg-white text-emerald-900 hover:bg-gray-200 py-3 px-2 rounded-xl font-black text-base shadow-lg active:translate-y-1 transition border-b-4 border-gray-300 active:border-b-0 flex items-center justify-center gap-1">🖨️ DRUCK</button>
</div>
<button onclick="clearGrid()" class="bg-slate-700 hover:bg-red-500/80 text-white py-3 px-4 rounded-xl font-bold transition text-base border-b-4 border-slate-900 active:border-b-0 active:translate-y-1 w-full flex items-center justify-center gap-2">🗑️ LÖSCHEN</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-emerald-500 rounded-[2rem] max-w-3xl 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-emerald-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="grid grid-cols-1 md:grid-cols-3 gap-6 text-base">
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">🎨</div><h3 class="text-emerald-400 font-bold text-xl mb-2">Malen</h3><p class="text-slate-300 leading-snug">Wähle rechts eine <b>Farbe</b> und tippe oder ziehe über die Quadrate.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">🧽</div><h3 class="text-white font-bold text-xl mb-2">Radieren</h3><p class="text-slate-300 leading-snug">Wähle den <b>Schwamm</b> oder drücke auf "LÖSCHEN", um von vorne zu beginnen.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">💾</div><h3 class="text-white font-bold text-xl mb-2">Fertig?</h3><p class="text-slate-300 leading-snug">Speichere dein Pixel-Kunstwerk oder drucke es direkt aus.</p></div>
</div>
<button onclick="toggleInfo(); playSound('click')" class="mt-8 w-full bg-emerald-600 hover:bg-emerald-500 text-white font-black py-4 rounded-xl text-xl transition">ALLES KLAR!</button>
</div>
</div>
<div id="print-container" class="hidden"><img id="print-img" src="" alt="Druckvorschau"></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 => console.warn(e)); } };
function playPaintSound() {}
window.goHome = function() { playSound('click'); setTimeout(() => { window.location.href = '../index.html'; }, 300); };
const grid = document.getElementById('grid'); let currentColor = '#000000'; let isDrawing = false; let idleTimer;
function resetIdleTimer() { clearTimeout(idleTimer); idleTimer = setTimeout(() => { window.location.href = '../index.html'; }, 90000); }
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, resetIdleTimer, {passive: true})); resetIdleTimer();
function toggleInfo() { const m = document.getElementById('info-modal'); m.classList.toggle('hidden'); m.classList.toggle('flex'); }
function initGrid() { grid.innerHTML = ''; for(let i=0; i<256; i++) { const cell = document.createElement('div'); cell.className = 'cell'; cell.addEventListener('mousedown', () => { isDrawing = true; cell.style.backgroundColor = currentColor; playPaintSound(); }); cell.addEventListener('mouseenter', () => { if(isDrawing) { cell.style.backgroundColor = currentColor; } }); cell.addEventListener('touchstart', (e) => { isDrawing = true; cell.style.backgroundColor = currentColor; playPaintSound(); }, {passive: false}); grid.appendChild(cell); } }
grid.addEventListener('touchmove', (e) => { e.preventDefault(); if(!isDrawing) return; const t = e.touches[0]; const el = document.elementFromPoint(t.clientX, t.clientY); if(el && el.classList.contains('cell')) { el.style.backgroundColor = currentColor; } }, {passive: false});
document.body.addEventListener('mouseup', () => isDrawing = false); document.body.addEventListener('touchend', () => isDrawing = false);
function setColor(btn) { playSound('click'); currentColor = btn.getAttribute('data-color'); document.querySelectorAll('.color-btn').forEach(b => b.classList.remove('ring-4', 'ring-white')); btn.classList.add('ring-4', 'ring-white'); }
function clearGrid() { playSound('click'); if(confirm("Wirklich alles löschen?")) { document.querySelectorAll('.cell').forEach(c => c.style.backgroundColor = 'white'); playSound('shutter'); } }
function generateCanvas() { const c = document.createElement('canvas'); const ctx = c.getContext('2d'); const s = 32; c.width = 16 * s; c.height = 16 * s; document.querySelectorAll('.cell').forEach((cell, i) => { const x = (i % 16) * s; const y = Math.floor(i / 16) * s; ctx.fillStyle = cell.style.backgroundColor || 'white'; ctx.fillRect(x, y, s, s); }); return c; }
function downloadArt() { playSound('success'); const c = generateCanvas(); const l = document.createElement('a'); l.download = 'mein-pixel-art.png'; l.href = c.toDataURL(); l.click(); }
window.printArt = async function(el) { playSound('click'); const btn = el || document.getElementById('btn-print'); let oldText = ""; if(btn) { oldText = btn.innerHTML; btn.innerText = "⏳ Sende..."; btn.disabled = true; } const c = generateCanvas(); const d = c.toDataURL(); try { if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.DirectPrinter) { await window.Capacitor.Plugins.DirectPrinter.printBase64({ data: d, name: 'Pixel-Art' }); if(btn) btn.innerText = "✅ Gedruckt!"; playSound('success'); } else { const img = document.getElementById('print-img'); img.src = d; setTimeout(() => { window.print(); }, 100); if(btn) btn.innerText = "✅ OK"; playSound('success'); } } catch (e) { alert("Fehler: " + e.message); if(btn) btn.innerText = "❌ Fehler"; } setTimeout(() => { if(btn) { btn.innerHTML = oldText || '🖨️ <span class="hidden xl:inline">DRUCK</span>'; btn.disabled = false; } }, 3000); }
window.onload = initGrid;
</script>
</body>
</html>

126
public/apps/rec.html Normal file
View File

@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Mikro Check</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;
user-select: none;
touch-action: manipulation;
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));
}
.no-scrollbar::-webkit-scrollbar { display: none; }
button:active { transform: scale(0.95); transition: transform 0.1s; }
.rec-active { animation: hardPulse 0.8s cubic-bezier(0.4, 0, 0.6, 1) infinite; background-color: #ef4444 !important; border-color: white !important; box-shadow: 0 0 30px rgba(239, 68, 68, 0.6) !important; }
@keyframes hardPulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.9; } }
.playing { box-shadow: 0 0 20px #22c55e; border-color: #86efac !important; }
.effect-active { ring: 4px solid white; transform: scale(1.1); box-shadow: 0 0 15px rgba(255,255,255,0.4); z-index: 10; }
</style>
</head>
<body class="flex flex-col px-4"
onclick="if(window.resetIdleTimer) window.resetIdleTimer()"
ontouchstart="if(window.resetIdleTimer) window.resetIdleTimer()">
<div class="flex justify-between items-center mb-4 z-50 flex-none h-16">
<button onclick="goHome()" class="bg-slate-800 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 text-white">
🏠 MENÜ
</button>
<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 text-xl">
?
</button>
</div>
<div class="flex-1 flex flex-col gap-4 relative w-full max-w-5xl mx-auto min-h-0">
<div class="relative w-full h-32 flex-none bg-slate-800 rounded-3xl border-4 border-slate-700 shadow-inner flex items-center justify-center overflow-hidden">
<canvas id="visualizer" class="w-full h-full"></canvas>
<div id="instruction-text" class="absolute text-slate-500 font-bold text-xl text-center px-4 pointer-events-none">
Mikrofon bereit... 🎤
</div>
</div>
<div class="flex-1 grid grid-cols-2 gap-4 min-h-0">
<div class="bg-slate-800 rounded-3xl border-4 border-slate-700 p-4 flex flex-col items-center justify-between shadow-lg relative overflow-hidden">
<div class="absolute top-0 left-0 bg-slate-700 px-4 py-1 rounded-br-xl font-black text-slate-400">SPUR 1</div>
<div id="timer-1" class="text-3xl md:text-4xl font-black font-mono text-slate-600 mt-2 transition-colors">00:00</div>
<div class="flex flex-col items-center gap-3 w-full">
<button id="btn-rec-1" onclick="toggleRecording(1)" class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-slate-700 border-8 border-slate-600 shadow-xl flex items-center justify-center transition-all hover:bg-slate-600 group">
<div id="icon-rec-1" class="w-6 h-6 md:w-8 md:h-8 bg-red-500 rounded-full shadow-lg group-hover:scale-110 transition-transform"></div>
</button>
<div id="fx-box-1" class="hidden flex gap-2 w-full justify-center mt-2">
<button onclick="setEffect(1, 0.6)" data-rate="0.6" class="fx-btn-1 w-12 h-12 rounded-xl bg-purple-600 text-2xl shadow-md border-b-4 border-purple-800 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Monster">🧟</button>
<button onclick="setEffect(1, 1)" data-rate="1" class="fx-btn-1 w-12 h-12 rounded-xl bg-blue-600 text-2xl shadow-md border-b-4 border-blue-800 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Normal">🙂</button>
<button onclick="setEffect(1, 1.7)" data-rate="1.7" class="fx-btn-1 w-12 h-12 rounded-xl bg-yellow-500 text-2xl shadow-md border-b-4 border-yellow-700 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Maus">🐭</button>
</div>
<button id="btn-play-1" onclick="playTrack(1)" class="hidden w-full bg-green-600 hover:bg-green-500 text-white font-black py-2 rounded-xl text-base shadow-lg border-b-4 border-green-800 active:border-b-0 active:translate-y-1 transition-all flex items-center justify-center gap-2"><span>▶️</span> ANHÖREN</button>
<button id="btn-del-1" onclick="deleteTrack(1)" class="hidden text-slate-500 hover:text-red-400 text-xs font-bold uppercase tracking-wider py-1">Löschen</button>
</div>
</div>
<div class="bg-slate-800 rounded-3xl border-4 border-slate-700 p-4 flex flex-col items-center justify-between shadow-lg relative overflow-hidden">
<div class="absolute top-0 left-0 bg-slate-700 px-4 py-1 rounded-br-xl font-black text-slate-400">SPUR 2</div>
<div id="timer-2" class="text-3xl md:text-4xl font-black font-mono text-slate-600 mt-2 transition-colors">00:00</div>
<div class="flex flex-col items-center gap-3 w-full">
<button id="btn-rec-2" onclick="toggleRecording(2)" class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-slate-700 border-8 border-slate-600 shadow-xl flex items-center justify-center transition-all hover:bg-slate-600 group">
<div id="icon-rec-2" class="w-6 h-6 md:w-8 md:h-8 bg-red-500 rounded-full shadow-lg group-hover:scale-110 transition-transform"></div>
</button>
<div id="fx-box-2" class="hidden flex gap-2 w-full justify-center mt-2">
<button onclick="setEffect(2, 0.6)" data-rate="0.6" class="fx-btn-2 w-12 h-12 rounded-xl bg-purple-600 text-2xl shadow-md border-b-4 border-purple-800 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Monster">🧟</button>
<button onclick="setEffect(2, 1)" data-rate="1" class="fx-btn-2 w-12 h-12 rounded-xl bg-blue-600 text-2xl shadow-md border-b-4 border-blue-800 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Normal">🙂</button>
<button onclick="setEffect(2, 1.7)" data-rate="1.7" class="fx-btn-2 w-12 h-12 rounded-xl bg-yellow-500 text-2xl shadow-md border-b-4 border-yellow-700 active:border-b-0 active:translate-y-1 transition flex items-center justify-center" title="Maus">🐭</button>
</div>
<button id="btn-play-2" onclick="playTrack(2)" class="hidden w-full bg-blue-600 hover:bg-blue-500 text-white font-black py-2 rounded-xl text-base shadow-lg border-b-4 border-blue-800 active:border-b-0 active:translate-y-1 transition-all flex items-center justify-center gap-2"><span>▶️</span> ANHÖREN</button>
<button id="btn-del-2" onclick="deleteTrack(2)" class="hidden text-slate-500 hover:text-red-400 text-xs font-bold uppercase tracking-wider py-1">Löschen</button>
</div>
</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-red-500 rounded-[2rem] max-w-lg 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-red-400 text-4xl font-bold transition"></button>
<h2 class="text-3xl font-black text-white mb-6 border-b border-slate-600 pb-4">🎙️ VOICE CHANGER</h2>
<ul class="text-slate-300 space-y-3 mb-8 list-disc pl-6 text-lg">
<li>Nimm links oder rechts etwas auf.</li>
<li>Drücke auf <b>Stopp</b>.</li>
<li>Jetzt erscheint Magie! ✨</li>
<li>Wähle: 🧟 <b>Monster</b> oder 🐭 <b>Maus</b>.</li>
<li>Hör es dir an!</li>
</ul>
<button onclick="toggleInfo(); playSound('click')" class="w-full bg-red-600 hover:bg-red-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); };
function toggleInfo() { const m = document.getElementById('info-modal'); m.classList.toggle('hidden'); m.classList.toggle('flex'); }
let micStream=null; let audioContext=null; let analyser=null; let dataArray=null; let gainNode=null;
const tracks = { 1: { isRecording: false, mediaRecorder: null, chunks: [], audio: new Audio(), hasData: false, timerInt: null, startTime: 0, playbackRate: 1, elementSource: null }, 2: { isRecording: false, mediaRecorder: null, chunks: [], audio: new Audio(), hasData: false, timerInt: null, startTime: 0, playbackRate: 1, elementSource: null } };
let idleTimer;
window.resetIdleTimer = function() { clearTimeout(idleTimer); const rec = tracks[1].isRecording || tracks[2].isRecording; if(!rec) { idleTimer = setTimeout(() => { window.location.href = '../index.html'; }, 120000); } };
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, window.resetIdleTimer, {passive: true})); window.resetIdleTimer();
async function initAudio() { try { if(!micStream) { micStream = await navigator.mediaDevices.getUserMedia({ audio: true }); audioContext = new (window.AudioContext || window.webkitAudioContext)(); gainNode = audioContext.createGain(); gainNode.gain.value = 1.5; gainNode.connect(audioContext.destination); analyser = audioContext.createAnalyser(); const s = audioContext.createMediaStreamSource(micStream); s.connect(analyser); analyser.fftSize = 256; dataArray = new Uint8Array(analyser.frequencyBinCount); drawVisualizer(); } if (audioContext && audioContext.state === 'suspended') await audioContext.resume(); } catch (e) { alert("Mikrofon Fehler: " + e.message); } }
function drawVisualizer() { const c = document.getElementById('visualizer'); if(!c) return; const ctx = c.getContext('2d'); c.width = c.offsetWidth; c.height = c.offsetHeight; function render() { requestAnimationFrame(render); if(!analyser) return; analyser.getByteFrequencyData(dataArray); ctx.fillStyle = '#1e293b'; ctx.fillRect(0, 0, c.width, c.height); const active = tracks[1].isRecording || tracks[2].isRecording; const alpha = active ? 1 : 0.3; let x = 0; const barW = (c.width / analyser.frequencyBinCount) * 2.5; for (let i = 0; i < analyser.frequencyBinCount; i++) { const h = dataArray[i] / 255 * c.height * 0.9; const r = active ? (h+50) : 50; const g = active ? 50 : (h+100); const b = active ? 50 : 200; ctx.fillStyle = `rgba(${r},${g},${b},${alpha})`; ctx.fillRect(x, c.height - h, barW, h); x += barW + 1; } } render(); }
async function toggleRecording(id) { await initAudio(); const t = tracks[id]; const btn = document.getElementById(`btn-rec-${id}`); const icon = document.getElementById(`icon-rec-${id}`); const playBtn = document.getElementById(`btn-play-${id}`); const delBtn = document.getElementById(`btn-del-${id}`); const fxBox = document.getElementById(`fx-box-${id}`); const timer = document.getElementById(`timer-${id}`); if (!t.isRecording) { playSound('shutter'); if(t.hasData) deleteTrack(id); t.mediaRecorder = new MediaRecorder(micStream); t.chunks = []; t.mediaRecorder.ondataavailable = e => t.chunks.push(e.data); t.mediaRecorder.onstop = () => { const b = new Blob(t.chunks, { type: 'audio/ogg; codecs=opus' }); t.audio.src = URL.createObjectURL(b); t.audio.preservesPitch = false; t.audio.mozPreservesPitch = false; t.audio.webkitPreservesPitch = false; t.hasData = true; playBtn.classList.remove('hidden'); delBtn.classList.remove('hidden'); fxBox.classList.remove('hidden'); fxBox.classList.add('flex'); setEffect(id, 1); playSound('success'); }; t.mediaRecorder.start(); t.isRecording = true; document.getElementById('instruction-text').classList.add('hidden'); btn.classList.add('rec-active'); icon.classList.replace('bg-red-500', 'bg-white'); timer.classList.replace('text-slate-600', 'text-white'); t.startTime = Date.now(); t.timerInt = setInterval(() => { const d = Math.floor((Date.now() - t.startTime) / 1000); timer.innerText = `${Math.floor(d/60).toString().padStart(2,'0')}:${(d%60).toString().padStart(2,'0')}`; }, 1000); } else { t.mediaRecorder.stop(); t.isRecording = false; clearInterval(t.timerInt); btn.classList.remove('rec-active'); icon.classList.replace('bg-white', 'bg-red-500'); timer.classList.replace('text-white', 'text-slate-600'); } }
function setEffect(id, rate) { playSound('click'); tracks[id].playbackRate = rate; document.querySelectorAll(`.fx-btn-${id}`).forEach(b => { b.classList.remove('effect-active', 'ring-4', 'ring-white'); if(b.getAttribute('data-rate') === rate.toString()) b.classList.add('effect-active', 'ring-4', 'ring-white'); }); }
function playTrack(id) { const t = tracks[id]; if(!t.hasData) return; playSound('click'); const btn = document.getElementById(`btn-play-${id}`); if (audioContext && !t.elementSource) { t.elementSource = audioContext.createMediaElementSource(t.audio); t.elementSource.connect(gainNode); } if (!t.audio.paused) { t.audio.pause(); t.audio.currentTime = 0; btn.classList.remove('playing'); btn.innerHTML = "<span>▶️</span> ANHÖREN"; } else { t.audio.playbackRate = t.playbackRate; t.audio.preservesPitch = false; if (audioContext.state === 'suspended') audioContext.resume(); t.audio.play(); btn.classList.add('playing'); btn.innerHTML = "<span>⏹</span> STOP"; t.audio.onended = () => { btn.classList.remove('playing'); btn.innerHTML = "<span>▶️</span> ANHÖREN"; }; } }
function deleteTrack(id) { playSound('click'); const t = tracks[id]; t.audio.pause(); t.chunks = []; t.hasData = false; document.getElementById(`btn-play-${id}`).classList.add('hidden'); document.getElementById(`btn-del-${id}`).classList.add('hidden'); document.getElementById(`fx-box-${id}`).classList.add('hidden'); document.getElementById(`fx-box-${id}`).classList.remove('flex'); document.getElementById(`timer-${id}`).innerText = "00:00"; }
</script>
</body>
</html>

161
public/apps/sound.html Normal file
View File

@@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Sound Labor</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 {
/* Fixiertes Layout */
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
margin: 0;
background-color: #0f172a; /* Dunkelblau */
color: white;
overflow: hidden !important;
user-select: none;
touch-action: manipulation;
font-family: sans-serif;
animation: fadeIn 0.4s ease-out;
box-sizing: border-box;
/* SAFE AREA PADDING */
padding-top: calc(10px + env(safe-area-inset-top));
padding-bottom: max(20px, env(safe-area-inset-bottom));
}
#pad-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 0.5rem; /* Etwas enger für mehr Platz */
width: 100%;
height: 100%;
}
.pad-item {
background-color: #1e293b;
border-radius: 1rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
border-bottom: 6px solid #0f172a;
position: relative;
user-select: none;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: all 0.1s;
}
.pad-item:active { border-bottom-width: 0; transform: translateY(4px); }
button:active, a:active { transform: scale(0.95); transition: transform 0.1s; }
.pulse-rec { animation: pulseRed 1s infinite; border-color: #ef4444 !important; box-shadow: 0 0 25px #ef4444; }
@keyframes pulseRed { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
.fx-active { ring: 4px solid white; transform: scale(1.1); box-shadow: 0 0 15px rgba(255,255,255,0.5); }
.no-scrollbar::-webkit-scrollbar { display: none; }
</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">
✨ SOUND
</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 flex-col xl:flex-row justify-center items-center gap-2 z-40 mb-2 shrink-0">
<div id="visualizer-box" class="hidden relative w-48 h-12 bg-slate-800 rounded-xl border-2 border-red-500 overflow-hidden shadow-[0_0_20px_rgba(239,68,68,0.5)]">
<canvas id="visualizer" class="w-full h-full"></canvas>
<div class="absolute inset-0 flex items-center justify-center text-red-500 font-black tracking-widest text-[10px] pointer-events-none animate-pulse">● REC</div>
</div>
<div class="flex gap-2 bg-slate-800/50 p-1 rounded-xl border border-slate-700 items-center">
<button onclick="setGlobalFx(0.6, this)" class="fx-btn w-10 h-10 rounded-lg bg-purple-600 text-xl shadow-md border-b-2 border-purple-800 active:border-b-0 active:translate-y-1 transition" title="Monster">🧟</button>
<button onclick="setGlobalFx(1.0, this)" class="fx-btn w-10 h-10 rounded-lg bg-blue-600 text-xl shadow-md border-b-2 border-blue-800 active:border-b-0 active:translate-y-1 transition fx-active ring-4 ring-white" title="Normal">🙂</button>
<button onclick="setGlobalFx(1.7, this)" class="fx-btn w-10 h-10 rounded-lg bg-yellow-500 text-xl shadow-md border-b-2 border-yellow-700 active:border-b-0 active:translate-y-1 transition" title="Maus">🐭</button>
</div>
<div class="flex gap-2 bg-slate-800 p-1 rounded-2xl border-2 border-slate-700 shadow-xl w-full xl:w-auto justify-center">
<button id="btn-rec" onclick="setMode('rec')" class="flex items-center justify-center gap-2 px-4 py-2 rounded-xl font-black text-base transition-all border-2 border-transparent bg-slate-700 hover:bg-slate-600 text-gray-300 active:scale-95">
<div class="w-3 h-3 rounded-full bg-red-500 shadow-sm"></div> REC
</button>
<button id="btn-loop" onclick="setMode('loop')" class="flex items-center justify-center gap-2 px-4 py-2 rounded-xl font-black text-base transition-all border-2 border-transparent bg-slate-700 hover:bg-slate-600 text-gray-300 active:scale-95">
🔁 LOOP
</button>
<button onclick="stopAll(); playSound('click')" class="px-4 py-2 rounded-xl font-black text-base bg-slate-700 hover:bg-red-500 hover:text-white transition-all text-gray-300 border-2 border-transparent active:scale-95">
⏹ STOP
</button>
</div>
</div>
<div id="status-bar" class="text-center text-slate-400 mb-1 font-mono text-sm uppercase tracking-widest font-bold transition-all duration-300 shrink-0">
Modus: SPIELEN
</div>
<div id="pad-container" class="flex-1 min-h-0 w-full max-w-5xl mx-auto z-10 pb-1"></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-rose-500 rounded-[2rem] max-w-3xl 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-rose-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="grid grid-cols-1 md:grid-cols-3 gap-6 text-base">
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">🧟</div><h3 class="text-purple-400 font-bold text-xl mb-2">Effekte</h3><p class="text-slate-300 leading-snug">Wähle <b>VOR</b> der Aufnahme oben Monster oder Maus.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">🎤</div><h3 class="text-rose-400 font-bold text-xl mb-2">Aufnehmen</h3><p class="text-slate-300 leading-snug">1. Tippe <b>REC</b>.<br>2. Wähle Pad.<br>3. Sprich.<br>4. Tippe Pad zum Stop.</p></div>
<div class="p-6 bg-slate-700/50 rounded-2xl border border-slate-600 flex flex-col items-center text-center"><div class="text-4xl mb-4">🔁</div><h3 class="text-blue-400 font-bold text-xl mb-2">Loopen</h3><p class="text-slate-300 leading-snug">Tippe <b>LOOP</b> und wähle Pads für Dauerschleife.</p></div>
</div>
<button onclick="toggleInfo(); playSound('click')" class="mt-8 w-full bg-rose-600 hover:bg-rose-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=>{}); } };
function goHome() { playSound('click'); setTimeout(() => { window.location.href = '../index.html'; }, 300); }
function toggleInfo() { const m = document.getElementById('info-modal'); m.classList.toggle('hidden'); m.classList.toggle('flex'); }
let idleTimer;
function resetIdleTimer() { clearTimeout(idleTimer); idleTimer = setTimeout(() => { if (typeof pads !== 'undefined' && Array.isArray(pads) && !pads.some(p => p.isRecording)) window.location.href = '../index.html'; }, 120000); }
['mousedown', 'touchstart', 'scroll', 'keydown', 'input'].forEach(evt => document.addEventListener(evt, resetIdleTimer, {passive: true})); resetIdleTimer();
let currentRecRate = 1.0;
function setGlobalFx(rate, btn) { playSound('click'); currentRecRate = rate; document.querySelectorAll('.fx-btn').forEach(b => b.classList.remove('fx-active', 'ring-4', 'ring-white')); btn.classList.add('fx-active', 'ring-4', 'ring-white'); }
let audioCtx; let pads = []; let currentMode = 'play'; let analyser, dataArray, visualizerFrame;
function init() { try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {} const container = document.getElementById('pad-container'); if(!container) return; container.innerHTML = ''; for(let i = 0; i < 12; i++) { pads.push({ buffer: null, loop: false, sourceNode: null, isRecording: false, mediaRecorder: null, chunks: [], playbackRate: 1.0 }); const el = document.createElement('div'); el.id = `pad-${i}`; el.className = `pad-item transition-all duration-100`; el.innerHTML = `<div class="text-4xl md:text-6xl font-black text-slate-700 transition-colors duration-300" id="label-${i}">${i+1}</div><div id="icon-loop-${i}" class="hidden absolute top-2 right-2 text-xl bg-blue-600 text-white rounded px-1 shadow-md">🔁</div><div id="icon-fx-${i}" class="absolute bottom-2 right-2 text-xl drop-shadow-md"></div><div id="icon-rec-${i}" class="hidden absolute top-3 left-3 w-4 h-4 bg-red-500 rounded-full animate-pulse shadow-[0_0_10px_red]"></div><div id="status-${i}" class="hidden absolute bottom-2 w-full text-center text-xs font-black uppercase tracking-widest text-emerald-400">BELEGT</div>`; el.onpointerdown = (e) => { e.preventDefault(); handlePadTouch(i); }; container.appendChild(el); } }
window.setMode = function(mode) { playSound('click'); if (currentMode === mode) currentMode = 'play'; else currentMode = mode; updateUI(); }
function updateUI() { const btnRec = document.getElementById('btn-rec'); const btnLoop = document.getElementById('btn-loop'); const status = document.getElementById('status-bar'); const inactive = "border-transparent bg-slate-700 text-gray-300 hover:bg-slate-600"; const activeRec = "border-red-500 text-red-400 bg-slate-900 shadow-[0_0_10px_rgba(239,68,68,0.2)]"; const activeLoop = "border-blue-500 text-blue-400 bg-slate-900 shadow-[0_0_10px_rgba(59,130,246,0.2)]"; const base = "flex items-center justify-center gap-2 px-4 py-2 rounded-xl font-black text-base transition-all border-2 active:scale-95 "; btnRec.className = base + (currentMode === 'rec' ? activeRec : inactive); btnLoop.className = base + (currentMode === 'loop' ? activeLoop : inactive); if (currentMode === 'rec') { status.innerText = "🔴 AUFNAHME"; status.className = "text-center mb-1 font-mono text-sm uppercase tracking-widest text-red-400 animate-pulse font-bold"; } else if (currentMode === 'loop') { status.innerText = "🔁 LOOP SETZEN"; status.className = "text-center mb-1 font-mono text-sm uppercase tracking-widest text-blue-400 font-bold"; } else { status.innerText = "Modus: SPIELEN"; status.className = "text-center text-slate-400 mb-1 font-mono text-sm uppercase tracking-widest font-bold"; } }
async function handlePadTouch(i) { resetIdleTimer(); if (audioCtx && audioCtx.state === 'suspended') await audioCtx.resume(); const pad = pads[i]; const el = document.getElementById(`pad-${i}`); if (currentMode === 'play') { if (!pad.buffer) return; if (pad.loop) { if (pad.sourceNode) { pad.sourceNode.stop(); pad.sourceNode = null; el.classList.remove('bg-blue-600', 'border-blue-800'); el.style.backgroundColor = "#1e293b"; } else { playMusicPad(i, true); el.style.backgroundColor = "#2563eb"; } } else { playMusicPad(i, false); el.style.backgroundColor = "#10b981"; setTimeout(() => el.style.backgroundColor = "#1e293b", 150); } } else if (currentMode === 'loop') { playSound('click'); if (!pad.buffer) return; pad.loop = !pad.loop; document.getElementById(`icon-loop-${i}`).classList.toggle('hidden', !pad.loop); el.style.transform = "scale(0.95)"; setTimeout(() => el.style.transform = "scale(1)", 100); } else if (currentMode === 'rec') { if (pad.isRecording) stopRecording(i); else startRecording(i); } }
function playMusicPad(i, loop) { const pad = pads[i]; if (pad.sourceNode) { try { pad.sourceNode.stop(); } catch(e){} pad.sourceNode = null; } const source = audioCtx.createBufferSource(); source.buffer = pad.buffer; source.loop = loop; source.playbackRate.value = pad.playbackRate; source.connect(audioCtx.destination); source.start(0); if (loop) pad.sourceNode = source; }
function startVisualizer(stream) { document.getElementById('visualizer-box').classList.remove('hidden'); const canvas = document.getElementById('visualizer'); const ctx = canvas.getContext('2d'); const source = audioCtx.createMediaStreamSource(stream); analyser = audioCtx.createAnalyser(); source.connect(analyser); analyser.fftSize = 256; dataArray = new Uint8Array(analyser.frequencyBinCount); function draw() { visualizerFrame = requestAnimationFrame(draw); analyser.getByteFrequencyData(dataArray); canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; ctx.fillStyle = '#1e293b'; ctx.fillRect(0, 0, canvas.width, canvas.height); const barWidth = (canvas.width / analyser.frequencyBinCount) * 2.5; let x = 0; for(let i = 0; i < analyser.frequencyBinCount; i++) { const barHeight = dataArray[i] / 255 * canvas.height; ctx.fillStyle = `rgb(${barHeight+50},50,50)`; ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight); x += barWidth + 1; } } draw(); }
function stopVisualizer() { document.getElementById('visualizer-box').classList.add('hidden'); cancelAnimationFrame(visualizerFrame); }
async function startRecording(i) { playSound('shutter'); const pad = pads[i]; const el = document.getElementById(`pad-${i}`); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); startVisualizer(stream); pad.mediaRecorder = new MediaRecorder(stream); pad.chunks = []; pad.mediaRecorder.ondataavailable = (e) => pad.chunks.push(e.data); pad.mediaRecorder.onstop = async () => { const blob = new Blob(pad.chunks, { 'type' : 'audio/webm; codecs=opus' }); const arrayBuffer = await blob.arrayBuffer(); audioCtx.decodeAudioData(arrayBuffer, (decodedBuffer) => { pad.buffer = decodedBuffer; pad.playbackRate = currentRecRate; updatePadVisuals(i, true); }, (e) => console.error(e)); stream.getTracks().forEach(track => track.stop()); stopVisualizer(); }; pad.mediaRecorder.start(); pad.isRecording = true; document.getElementById(`icon-rec-${i}`).classList.remove('hidden'); el.classList.add('pulse-rec'); } catch (err) { alert("Mikrofon Fehler: " + err.message); } }
function stopRecording(i) { playSound('success'); const pad = pads[i]; const el = document.getElementById(`pad-${i}`); if (pad.mediaRecorder && pad.mediaRecorder.state !== 'inactive') pad.mediaRecorder.stop(); pad.isRecording = false; document.getElementById(`icon-rec-${i}`).classList.add('hidden'); el.classList.remove('pulse-rec'); window.setMode('play'); }
function updatePadVisuals(i, hasSound) { const el = document.getElementById(`pad-${i}`); const label = document.getElementById(`label-${i}`); const status = document.getElementById(`status-${i}`); const iconFx = document.getElementById(`icon-fx-${i}`); const pad = pads[i]; if (hasSound) { el.style.borderColor = "#10b981"; label.classList.remove('text-slate-700'); label.classList.add('text-white'); status.classList.remove('hidden'); if(pad.playbackRate < 0.9) iconFx.innerText = "🧟"; else if(pad.playbackRate > 1.2) iconFx.innerText = "🐭"; else iconFx.innerText = ""; } }
window.stopAll = function() { if(pads && pads.length > 0) { pads.forEach((pad, i) => { if (pad.sourceNode) { try { pad.sourceNode.stop(); } catch(e){} pad.sourceNode = null; } const el = document.getElementById(`pad-${i}`); if(el) { el.style.backgroundColor = "#1e293b"; el.classList.remove('bg-blue-600'); } }); } }
init();
</script>
</body>
</html>