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.
<!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