Zurück zur Übersicht

A.R.C. OPTICAL BIO-SENSOR

Echtzeit-Herzfrequenzanalyse per Smartphone-Kamera Ein futuristisches rPPG-System zur experimentellen Erfassung von Herzschlagsignalen über die Kamera. Mikroskopische Farbveränderungen der Haut werden analysiert und in Echtzeit als BPM-Wert und Signalverlauf visualisiert. Hinweis: Nur zu Demonstrations- und Forschungszwecken – kein medizinisches Gerät.

Java
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>HDSpace A.R.C. TERMINAL v1.0.3 (Mobile)</title>
    <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@400;600&family=Share+Tech+Mono&display=swap" rel="stylesheet">
    <style>
        :root {
            --holo: #00f3ff;   /* Main Hologram Cyan */
            --dark: #00181f;   /* Deep Background */
            --alert: #ff3333;  /* Red Alert */
            --glass: rgba(0, 243, 255, 0.05);
            --text-main: 'Rajdhani', sans-serif;
            --text-code: 'Share Tech Mono', monospace;
        }

        * { box-sizing: border-box; }

        body {
            margin: 0;
            height: 100vh;
            width: 100vw;
            background-color: #000;
            color: var(--holo);
            font-family: var(--text-main);
            overflow: hidden; /* Desktop default: Kein Scrollen */
            display: flex;
            align-items: center;
            justify-content: center;
        }

        /* --- CRT START EFFEKT --- */
        body { animation: turnOn 2.0s cubic-bezier(0.1, 0.9, 0.2, 1); }
        @keyframes turnOn {
            0% { filter: brightness(0); opacity: 0; }
            20% { filter: brightness(2) contrast(2); opacity: 1; }
            50% { filter: brightness(0.9); }
            100% { filter: brightness(1); }
        }

        /* --- HINTERGRUND --- */
        .bg-grid {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            background-image: 
                linear-gradient(rgba(0, 243, 255, 0.03) 1px, transparent 1px),
                linear-gradient(90deg, rgba(0, 243, 255, 0.03) 1px, transparent 1px);
            background-size: 50px 50px;
            z-index: -2;
            transform: scale(1.1);
            opacity: 0.6;
        }
        
        /* --- HAUPT LAYOUT (GRID) --- */
        .hud-wrapper {
            position: relative;
            display: grid;
            grid-template-columns: 350px 1fr 350px;
            grid-template-rows: auto 1fr 120px;
            width: 98vw;
            height: 90vh;
            max-width: 1920px;
            gap: 20px;
            padding: 20px;
        }

        /* --- RESPONSIVE MOBILE LAYOUT --- */
        /* Sobald der Screen schmaler als 1200px ist (Tablets/Handys) */
        @media (max-width: 1200px) {
            body { 
                overflow-y: auto; /* Scrollen erlauben */
                height: auto; 
                display: block; 
            }
            .bg-grid { position: fixed; height: 100vh; }
            
            .hud-wrapper {
                display: flex; 
                flex-direction: column; 
                height: auto; 
                width: 100%;
                padding: 10px;
                padding-bottom: 60px; /* Platz für Copyright */
            }
            
            /* Panels werden flexibel */
            .side-panel { 
                order: 3; /* Nach unten schieben */
                min-height: auto;
            }
            /* Wichtige Panels nach oben */
            .side-panel.diagnostics { order: 2; margin-top: 20px; }
            
            .center-stage { 
                order: 1; 
                min-height: 350px; 
                margin: 20px 0;
            }
            
            .bottom-deck { order: 4; margin-top: 20px; }
            
            /* Reaktor schrumpfen für Handys */
            .reactor { width: 300px !important; height: 300px !important; }
            .video-mask { width: 240px !important; height: 240px !important; }
            .bpm-val { font-size: 4rem !important; }
            .sys-title { font-size: 1.5rem !important; letter-spacing: 2px !important; }
        }

        /* --- HEADER --- */
        .hud-header {
            grid-column: 1 / 4;
            display: flex; flex-direction: column; align-items: center;
            border-bottom: 1px solid rgba(0,243,255,0.3);
            padding-bottom: 15px;
            margin-bottom: 10px;
        }
        
        .sys-title { 
            font-family: 'Orbitron'; font-size: 2.2rem; letter-spacing: 4px; 
            text-shadow: 0 0 20px var(--holo); margin-bottom: 5px;
            text-transform: uppercase; text-align: center; line-height: 1.1;
        }
        .sys-subtitle {
            font-size: 0.9rem; letter-spacing: 3px; color: #fff; opacity: 0.7; margin-bottom: 15px; text-align: center;
        }

        .alert-banner {
            background: rgba(255, 51, 51, 0.1);
            border: 1px solid var(--alert);
            color: var(--alert);
            padding: 8px 20px;
            text-align: center;
            font-family: var(--text-code);
            font-size: 0.8rem;
            letter-spacing: 1px;
            box-shadow: 0 0 15px rgba(255, 51, 51, 0.1);
            animation: pulseAlert 4s infinite;
            max-width: 90%;
        }
        @keyframes pulseAlert { 50% { border-color: transparent; opacity: 0.6; } }

        /* --- SEITEN PANELS --- */
        .side-panel {
            background: rgba(0, 20, 30, 0.4);
            border: 1px solid rgba(0, 243, 255, 0.2);
            padding: 20px;
            display: flex; flex-direction: column; gap: 20px;
            font-family: var(--text-code);
        }
        
        .panel-title {
            color: #fff; border-bottom: 1px solid var(--holo); padding-bottom: 5px; margin-bottom: 10px;
            font-family: 'Orbitron'; letter-spacing: 2px;
        }

        .info-block { margin-bottom: 15px; }
        .text-de { color: var(--holo); display: block; margin-bottom: 3px; font-weight: bold; font-size: 1rem; }
        .text-en { color: #6688aa; display: block; font-size: 0.8em; font-style: italic; }

        /* --- ZENTRUM: REAKTOR --- */
        .center-stage {
            grid-column: 2 / 3; grid-row: 2 / 3;
            display: flex; justify-content: center; align-items: center;
            position: relative;
        }

        .reactor {
            position: relative;
            width: 450px; height: 450px; /* Desktop Größe */
            display: flex; justify-content: center; align-items: center;
        }

        .video-mask {
            width: 360px; height: 360px; /* Desktop Größe */
            border-radius: 50%;
            overflow: hidden;
            position: relative;
            border: 2px solid rgba(0, 243, 255, 0.2);
            z-index: 2;
            background: #000;
            transition: border-color 0.3s, box-shadow 0.3s;
        }
        .video-mask.error-state { border-color: var(--alert); box-shadow: 0 0 30px var(--alert); }
        .video-mask.beat-state { box-shadow: 0 0 40px var(--holo); border-color: #fff; }

        video {
            width: 100%; height: 100%; object-fit: cover;
            transform: scaleX(-1);
            filter: contrast(1.2) brightness(0.9) grayscale(0.5) sepia(0.2) hue-rotate(180deg);
            opacity: 0.8;
        }

        .hud-overlay {
            position: absolute; z-index: 10;
            text-align: center; pointer-events: none;
            text-shadow: 0 2px 10px #000; width: 100%;
        }
        .bpm-val { font-family: 'Orbitron'; font-size: 6rem; font-weight: 900; color: #fff; line-height: 0.9; }
        .bpm-lbl { font-size: 1.2rem; letter-spacing: 3px; font-weight: bold; }
        
        .status-box {
            margin-top: 15px; display: inline-block;
            background: rgba(0,0,0,0.8); border: 1px solid var(--holo); padding: 5px 15px;
        }
        .stat-de { display: block; font-size: 0.9rem; color: #fff; font-weight: bold; }
        .stat-en { display: block; font-size: 0.7rem; color: #aaa; }

        .ring-svg {
            position: absolute; top:0; left:0; width:100%; height:100%;
            pointer-events: none; z-index: 1;
        }
        .spin { transform-origin: 50% 50%; animation: rot 20s linear infinite; }
        @keyframes rot { 100% { transform: rotate(360deg); } }

        /* --- UNTEN --- */
        .bottom-deck {
            grid-column: 2 / 3; grid-row: 3 / 4;
            display: flex; flex-direction: column; justify-content: flex-end;
            gap: 15px;
        }

        .graph-box {
            width: 100%; height: 60px;
            background: rgba(0, 10, 15, 0.8);
            border: 1px solid var(--holo);
            position: relative;
        }
        canvas { width: 100%; height: 100%; }

        .btn-cali {
            background: transparent; color: var(--holo);
            border: 2px solid var(--holo);
            padding: 15px;
            cursor: pointer; transition: 0.3s;
            position: relative; overflow: hidden;
            text-align: center; width: 100%;
        }
        .btn-text-main { font-family: 'Orbitron'; font-size: 1.2rem; display: block; letter-spacing: 2px; }
        .btn-text-sub { font-family: 'Share Tech Mono'; font-size: 0.8rem; display: block; opacity: 0.7; }
        
        .btn-cali:hover { background: var(--holo); color: #000; box-shadow: 0 0 30px var(--holo); }
        
        /* --- COPYRIGHT --- */
        .copyright {
            position: fixed; bottom: 5px; width: 100%;
            text-align: center; font-family: var(--text-code);
            font-size: 0.7rem; color: var(--holo); opacity: 0.5;
            z-index: 100; pointer-events: none; letter-spacing: 2px;
            text-shadow: 0 0 5px #000;
        }
        /* Auf Mobile static, damit es nicht über den Content floatet */
        @media (max-width: 1200px) {
            .copyright { position: relative; padding: 20px 0; bottom: auto; }
        }

    </style>
</head>
<body>

    <div class="bg-grid"></div>

    <div class="hud-wrapper">
        
        <div class="hud-header">
            <div class="sys-title">A.R.C. OPTISCHER BIO-SENSOR</div>
            <div class="sys-subtitle">V1.0.3 // MOBILE READY // VASCULAR ANALYSIS</div>
            
            <div class="alert-banner">
                <div>ACHTUNG: KEIN MEDIZINPRODUKT // NUR ZUR DEMONSTRATION</div>
                <div style="font-size:0.8em; opacity:0.7; margin-top:2px;">WARNING: NOT A MEDICAL DEVICE // FOR DEMONSTRATION ONLY</div>
            </div>
        </div>

        <div class="side-panel">
            <div class="panel-title">SYSTEM INFO</div>
            
            <div class="info-block">
                <span class="text-de">> FUNKTIONSWEISE</span>
                <span class="text-en">> HOW IT WORKS</span>
                <p style="margin:5px 0; color:#ccc; line-height:1.4;">
                    Dieses System nutzt <strong>rPPG-Algorithmen</strong>. Es analysiert mikroskopische Farbveränderungen der Haut, die durch den Herzschlag (Hämoglobin-Reflexion) entstehen.
                    <br>
                    <span style="color:#6688aa; font-size:0.85em;">Uses <strong>rPPG algorithms</strong>. Analyzes microscopic skin color changes caused by heartbeat.</span>
                </p>
            </div>

            <div class="info-block">
                <span class="text-de">> ANLEITUNG</span>
                <span class="text-en">> INSTRUCTIONS</span>
                <ul style="margin:5px 0; padding-left:20px; color:#ccc;">
                    <li>Gutes Licht erforderlich<br><span style="color:#6688aa; font-size:0.8em;">Good lighting required</span></li>
                    <li>Ruhig sitzen<br><span style="color:#6688aa; font-size:0.8em;">Sit still</span></li>
                    <li>Kalibrieren klicken<br><span style="color:#6688aa; font-size:0.8em;">Click calibrate</span></li>
                </ul>
            </div>
        </div>

        <div class="center-stage">
            <div class="reactor">
                <svg class="ring-svg" viewBox="0 0 500 500">
                    <circle cx="250" cy="250" r="240" fill="none" stroke="#00f3ff" stroke-width="1" stroke-dasharray="10, 10" opacity="0.3" class="spin" />
                    <circle cx="250" cy="250" r="220" fill="none" stroke="#00f3ff" stroke-width="4" stroke-dasharray="100, 300" opacity="0.5" class="spin" style="animation-direction:reverse; animation-duration:15s;" />
                </svg>

                <div class="video-mask" id="videoContainer">
                    <video id="webcam" autoplay playsinline muted></video>
                </div>

                <div class="hud-overlay">
                    <div class="bpm-val" id="bpmDisplay">---</div>
                    <div class="bpm-lbl">BPM FLUX</div>
                    
                    <div class="status-box">
                        <span class="stat-de" id="statDe">WARTE AUF EINGABE</span>
                        <span class="stat-en" id="statEn">WAITING FOR INPUT</span>
                    </div>
                </div>
            </div>
        </div>

        <div class="side-panel diagnostics">
            <div class="panel-title">DIAGNOSTICS</div>

            <div class="info-block">
                <span class="text-de">STATUS: <span id="sysStatus" style="color:var(--holo)">BEREIT</span></span>
                <span class="text-en">STATUS: <span id="sysStatusEn" style="color:#6688aa">READY</span></span>
            </div>

            <div class="info-block">
                <span class="text-de">> SIGNAL-QUALITÄT</span>
                <span class="text-en">> SIGNAL QUALITY</span>
                <div style="background:#000; height:10px; border:1px solid #333; margin-top:5px;">
                    <div id="qualityBar" style="width:0%; height:100%; background:var(--holo); transition: width 0.2s;"></div>
                </div>
            </div>

            <div class="info-block" style="margin-top:auto;">
                <span class="text-de">> HILFE</span>
                <span class="text-en">> HELP</span>
                <p style="margin:5px 0; color:#ccc; font-size:0.85rem;">
                    <strong>0 BPM?</strong> Zu dunkel / Bewegung.<br>
                    <strong>Abbruch?</strong> Objekt verloren.<br>
                </p>
            </div>
            
            <div style="border-top:1px solid #333; paddingTop:10px; font-size:0.7rem; color:#666;">
                SESSION ID: <span id="sessId">XF-992</span><br>
                CORE: ONLINE // GHOST-FILTER: ACTIVE
            </div>
        </div>

        <div class="bottom-deck">
            <div class="graph-box">
                <canvas id="graphCanvas"></canvas>
            </div>
            
            <button class="btn-cali" onclick="startCalibration()">
                <span class="btn-text-main">[ SYSTEM KALIBRIEREN ]</span>
                <span class="btn-text-sub">CALIBRATE SYSTEM</span>
            </button>
        </div>

    </div>

    <div class="copyright">
        © 2026 CHRISTIAN SCHWARZER // HDSPACE.EU
    </div>

    <canvas id="procCanvas" width="50" height="50" style="display:none;"></canvas>

    <script>
        const video = document.getElementById('webcam');
        const procCanvas = document.getElementById('procCanvas');
        const ctx = procCanvas.getContext('2d', { willReadFrequently: true });
        const graphCanvas = document.getElementById('graphCanvas');
        const graphCtx = graphCanvas.getContext('2d');
        const bpmDisplay = document.getElementById('bpmDisplay');
        const statDe = document.getElementById('statDe');
        const statEn = document.getElementById('statEn');
        const sysStatus = document.getElementById('sysStatus');
        const sysStatusEn = document.getElementById('sysStatusEn');
        const videoContainer = document.getElementById('videoContainer');
        const qualityBar = document.getElementById('qualityBar');
        const btnMain = document.querySelector('.btn-text-main');
        const btnSub = document.querySelector('.btn-text-sub');

        document.getElementById('sessId').innerText = "XF-" + Math.floor(Math.random() * 9000 + 1000);

        let isCalibrated = false;
        let refColor = {r:0, g:0, b:0};
        let signalBuffer = new Array(300).fill(0);
        let bpmHistory = [];
        let lastPeakTime = 0;
        let trend = 0;
        let dispBPM = 0;
        let targetBPM = 0;

        navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: {ideal: 640} } })
            .then(stream => {
                video.srcObject = stream;
                video.onloadedmetadata = () => requestAnimationFrame(loop);
            }).catch(e => { 
                statDe.innerText = "KAMERA FEHLER"; 
                statEn.innerText = "CAMERA ERROR";
                statDe.style.color = "red";
            });

        function startCalibration() {
            btnMain.innerText = "SCANNE...";
            btnSub.innerText = "SCANNING...";
            statDe.innerText = "ERFASSE SPEKTRUM...";
            statEn.innerText = "ACQUIRING SPECTRUM...";
            statDe.style.color = "yellow";
            videoContainer.style.borderColor = "#fff";
            videoContainer.classList.remove("error-state");
            
            setTimeout(() => {
                videoContainer.style.borderColor = "rgba(0,243,255,0.2)";
                refColor = analyzeFrame(); 
                isCalibrated = true;
                bpmHistory = [];
                targetBPM = 0;
                
                statDe.innerText = "ZIEL ERFASST / AKTIV";
                statEn.innerText = "TARGET LOCKED / ACTIVE";
                statDe.style.color = "var(--holo)";
                btnMain.innerText = "[ NEU KALIBRIEREN ]";
                btnSub.innerText = "RE-CALIBRATE";
                sysStatus.innerText = "MESSUNG LÄUFT";
                sysStatusEn.innerText = "MEASURING ACTIVE";
            }, 1500);
        }

        function analyzeFrame() {
            ctx.drawImage(video, 0, 0, procCanvas.width, procCanvas.height);
            const cx = procCanvas.width / 2;
            const frame = ctx.getImageData(cx-10, cx-10, 20, 20);
            const d = frame.data;
            let r=0, g=0, b=0;
            for(let i=0; i<d.length; i+=4) { r+=d[i]; g+=d[i+1]; b+=d[i+2]; }
            let c = d.length/4;
            return { r: r/c, g: g/c, b: b/c };
        }

        function loop() {
            if(dispBPM < targetBPM) dispBPM++;
            if(dispBPM > targetBPM) dispBPM--;
            bpmDisplay.innerText = dispBPM > 0 ? dispBPM.toString().padStart(3, '0') : "---";

            const current = analyzeFrame();
            let isValid = false;
            let qualityScore = 0;

            if(isCalibrated) {
                const bright = (current.r + current.g + current.b) / 3;
                const diff = Math.abs(current.r - refColor.r) + Math.abs(current.g - refColor.g) + Math.abs(current.b - refColor.b);
                qualityScore = Math.max(0, 100 - diff);
                qualityBar.style.width = qualityScore + "%";
                
                if(bright < 20) {
                    statDe.innerText = "ZU DUNKEL"; statEn.innerText = "TOO DARK";
                    statDe.style.color = "var(--alert)";
                    videoContainer.classList.add("error-state");
                } else if (diff > 60) {
                    statDe.innerText = "SIGNAL VERLOREN"; statEn.innerText = "SIGNAL LOST";
                    statDe.style.color = "orange";
                    videoContainer.classList.add("error-state");
                } else {
                    statDe.innerText = "DATEN STABIL"; statEn.innerText = "DATA STABLE";
                    statDe.style.color = "var(--holo)";
                    videoContainer.classList.remove("error-state");
                    isValid = true;
                }
            }

            if(isValid) {
                let val = current.g;
                trend = (trend * 0.95) + (val * 0.05);
                let signal = (trend - val) * 15;
                updateGraph(signal);
                detectBeat(signal);
            } else {
                updateGraph(0);
                if(isCalibrated) targetBPM = 0;
            }
            requestAnimationFrame(loop);
        }

        function updateGraph(val) {
            signalBuffer.push(val);
            signalBuffer.shift();
            graphCtx.clearRect(0,0, graphCanvas.width, graphCanvas.height);
            graphCtx.lineWidth = 2;
            let grad = graphCtx.createLinearGradient(0,0,graphCanvas.width,0);
            grad.addColorStop(0, "rgba(0,243,255,0.2)");
            grad.addColorStop(0.5, "rgba(0,243,255,1)");
            grad.addColorStop(1, "rgba(0,243,255,0.2)");
            graphCtx.strokeStyle = isCalibrated ? grad : "rgba(0,243,255,0.1)";
            graphCtx.beginPath();
            let w = graphCanvas.width; let h = graphCanvas.height;
            let step = w / signalBuffer.length;
            for(let i=0; i<signalBuffer.length; i++){
                let y = (h/2) - (signalBuffer[i] * 3);
                if(i===0) graphCtx.moveTo(i*step, y); else graphCtx.lineTo(i*step, y);
            }
            graphCtx.stroke();
        }

        function detectBeat(signal) {
            let now = Date.now();
            if(signal > 2.5 && (now - lastPeakTime) > 300) {
                let delta = now - lastPeakTime;
                lastPeakTime = now;
                let bpm = 60000 / delta;
                if(bpm > 45 && bpm < 170) {
                    bpmHistory.push(bpm);
                    if(bpmHistory.length > 5) bpmHistory.shift();
                    targetBPM = Math.round(bpmHistory.reduce((a,b)=>a+b,0)/bpmHistory.length);
                    videoContainer.classList.add("beat-state");
                    setTimeout(() => videoContainer.classList.remove("beat-state"), 100);
                }
            }
        }
        
        // Canvas Resize Fix
        function resizeCanvas() {
            graphCanvas.width = document.querySelector('.graph-box').clientWidth;
            graphCanvas.height = document.querySelector('.graph-box').clientHeight;
        }
        window.addEventListener('resize', resizeCanvas);
        setTimeout(resizeCanvas, 500);

    </script>
</body>
</html>

// hinzugefügt am 12.09.2026

Zurück zur Übersicht