philippurbschat.de/test/index.php
Philipp Urbschat f393beb7ff
feat: inkubator-standard & modul-autodiscovery
- Dynamische Modulerkennung via module.json in ModuleService.php
- Kaskadierender Env-Loader fuer zentrale API-Keys (GEMINI_API_KEY)
- Graceful Auth fuer alle Subprojekte (dinos, plants, storymachine, test)
- Wiederverwendbares Starter-Template in _template/
- test-Modul auf philippurbschat.de Dark-Design modernisiert
2026-09-13 00:23:16 +02:00

186 lines
No EOL
9.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once __DIR__ . '/bootstrap.php';
// Modul-Metadaten
$moduleDescriptor = file_exists(__DIR__ . '/module.json')
? json_decode(file_get_contents(__DIR__ . '/module.json'), true)
: [];
$moduleName = $moduleDescriptor['name'] ?? 'Test & Lab';
$moduleBadge = $moduleDescriptor['badge'] ?? 'Audio';
$moduleDesc = $moduleDescriptor['description'] ?? 'Modul-Showcase & Web Audio Synthesizer';
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($moduleName) ?> philippurbschat.de</title>
<link rel="stylesheet" href="/css/app.css">
<link rel="stylesheet" href="/css/style.css">
<!-- Standalone-Fallback -->
<script>
window.addEventListener('error', function(e) {
if (e.target && e.target.tagName === 'LINK' && e.target.href.includes('/css/app.css')) {
const cdn = document.createElement('script');
cdn.src = 'https://cdn.tailwindcss.com';
document.head.appendChild(cdn);
}
}, true);
</script>
</head>
<body class="bg-slate-950 text-slate-200 min-h-screen font-sans flex flex-col antialiased selection:bg-cyan-500 selection:text-white">
<!-- Top Navigation Bar -->
<header class="border-b border-slate-800/80 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50">
<div class="max-w-5xl mx-auto px-4 sm:px-6 py-3.5 flex items-center justify-between">
<div class="flex items-center gap-3">
<a href="/" class="text-xs font-mono text-slate-400 hover:text-cyan-400 transition-colors flex items-center gap-1.5 group">
<span class="group-hover:-translate-x-0.5 transition-transform">←</span> philippurbschat.de
</a>
<span class="text-slate-700">/</span>
<span class="text-xs font-mono text-slate-300 font-medium">test</span>
</div>
<div class="flex items-center gap-3">
<span class="text-[11px] font-mono px-2 py-0.5 rounded-full border border-cyan-500/30 bg-cyan-500/10 text-cyan-400 uppercase tracking-wider">
<?= htmlspecialchars($moduleBadge) ?>
</span>
<?php if ($isAdmin): ?>
<a href="/admin/users" class="text-[11px] font-mono px-2 py-0.5 rounded-full border border-fuchsia-500/30 bg-fuchsia-500/10 text-fuchsia-400 hover:bg-fuchsia-500/20 transition-colors uppercase tracking-wider">
Benutzer
</a>
<?php endif; ?>
<span class="text-[11px] font-mono text-slate-500 hidden sm:inline-block">
<?= htmlspecialchars($currentUser) ?>
</span>
</div>
</div>
</header>
<!-- Main Content -->
<main class="grow max-w-5xl mx-auto px-4 sm:px-6 py-10 w-full">
<!-- Header -->
<div class="mb-10">
<div class="inline-flex items-center gap-2 mb-3">
<span class="w-2 h-2 rounded-full bg-cyan-400 animate-pulse"></span>
<span class="text-xs font-mono uppercase tracking-[0.2em] text-cyan-400">Audio Lab</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold font-mono tracking-tight text-white mb-2">
<?= htmlspecialchars($moduleName) ?>
</h1>
<p class="text-slate-400 text-sm max-w-2xl">
<?= htmlspecialchars($moduleDesc) ?>
</p>
</div>
<!-- Sandbox & Synth Card -->
<div id="synth-container" class="bg-slate-900/40 border border-slate-800 rounded-2xl p-6 sm:p-8 backdrop-blur shadow-2xl relative overflow-hidden transition-transform duration-200">
<div class="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-cyan-500/40 to-transparent"></div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
<!-- Info Section -->
<div class="space-y-4">
<h2 class="text-xs font-mono uppercase tracking-widest text-slate-400">Web Audio API Test</h2>
<p class="text-sm text-slate-300 leading-relaxed">
Dieses Modul dient als Referenzimplementierung für das neue Modulsystem. Es demonstriert dynamische Web Audio Synthese (Oszillatoren, Rauschgeneratoren und Biquad-Filter) im Browser.
</p>
<div class="p-4 rounded-xl bg-slate-950/70 border border-slate-800/80 font-mono text-xs space-y-2">
<div class="text-slate-400">STATUS: <span id="audio-status" class="text-emerald-400">Bereit für Signaltest</span></div>
<div class="text-slate-500 text-[11px]">System: Graceful Auth &bull; Auto-Discovered</div>
</div>
</div>
<!-- Interactive Trigger -->
<div class="flex flex-col items-center justify-center p-8 rounded-xl bg-slate-950/60 border border-slate-800/60 text-center">
<button id="trigger-sound" class="w-32 h-32 rounded-full bg-gradient-to-br from-cyan-500/20 to-fuchsia-500/20 border-2 border-cyan-500/50 hover:border-cyan-400 hover:shadow-[0_0_30px_rgba(6,182,212,0.3)] transition-all duration-300 flex flex-col items-center justify-center gap-2 group cursor-pointer active:scale-95 select-none">
<svg class="w-8 h-8 text-cyan-400 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
</svg>
<span class="text-[10px] font-mono uppercase tracking-widest text-slate-300 group-hover:text-white">Signal Starten</span>
</button>
<p class="text-xs font-mono text-slate-500 mt-4">Klicken zum Erzeugen eines synthetischen Klanges</p>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="border-t border-slate-900 py-6 text-center text-xs font-mono text-slate-600">
<?= htmlspecialchars($moduleName) ?> &bull; Philipp Urbschat Lab
</footer>
<script>
const triggerBtn = document.getElementById('trigger-sound');
const synthContainer = document.getElementById('synth-container');
const statusMsg = document.getElementById('audio-status');
function playSynthesizerTone() {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
const duration = 0.4 + Math.random() * 0.4;
const mainGain = audioCtx.createGain();
const osc = audioCtx.createOscillator();
const oscGain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(55 + Math.random() * 25, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(25, audioCtx.currentTime + duration);
const lfo = audioCtx.createOscillator();
const lfoGain = audioCtx.createGain();
lfo.type = 'square';
lfo.frequency.setValueAtTime(12 + Math.random() * 6, audioCtx.currentTime);
lfoGain.gain.setValueAtTime(0.7, audioCtx.currentTime);
lfo.connect(lfoGain);
lfoGain.connect(oscGain.gain);
const bufferSize = Math.floor(audioCtx.sampleRate * duration);
const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const noiseData = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
noiseData[i] = Math.random() * 2 - 1;
}
const noise = audioCtx.createBufferSource();
noise.buffer = noiseBuffer;
const noiseFilter = audioCtx.createBiquadFilter();
noiseFilter.type = 'bandpass';
noiseFilter.frequency.setValueAtTime(380, audioCtx.currentTime);
const lowPass = audioCtx.createBiquadFilter();
lowPass.type = 'lowpass';
lowPass.frequency.setValueAtTime(420, audioCtx.currentTime);
lowPass.frequency.exponentialRampToValueAtTime(70, audioCtx.currentTime + duration);
mainGain.gain.setValueAtTime(0, audioCtx.currentTime);
mainGain.gain.linearRampToValueAtTime(0.4, audioCtx.currentTime + 0.05);
mainGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(oscGain);
oscGain.connect(lowPass);
noise.connect(noiseFilter);
noiseFilter.connect(lowPass);
lowPass.connect(mainGain);
mainGain.connect(audioCtx.destination);
osc.start();
lfo.start();
noise.start();
osc.stop(audioCtx.currentTime + duration);
lfo.stop(audioCtx.currentTime + duration);
noise.stop(audioCtx.currentTime + duration);
statusMsg.textContent = "Signal generiert (Synthese erfolgreich)";
}
triggerBtn.addEventListener('click', () => {
playSynthesizerTone();
synthContainer.classList.add('ring-1', 'ring-cyan-500/50');
setTimeout(() => synthContainer.classList.remove('ring-1', 'ring-cyan-500/50'), 300);
});
</script>
</body>
</html>