Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
<title>ECG Simulator</title> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/p5.min.js"></script> | |
<style> | |
:root { | |
--bg: #0f0f12; | |
--primary: #00ff88; | |
--accent: #ff0040; | |
--text: #ffffff; | |
--grid: #1e1e2e; | |
--panel: rgba(30, 30, 46, 0.8); | |
--glass: rgba(255, 255, 255, 0.05); | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
background: var(--bg); | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
color: var(--text); | |
overflow-x: hidden; | |
min-height: 100vh; | |
display: grid; | |
place-items: center; | |
background-image: | |
radial-gradient(circle at 20% 30%, rgba(0, 255, 136, 0.03) 0%, transparent 50%), | |
radial-gradient(circle at 80% 70%, rgba(255, 0, 64, 0.03) 0%, transparent 50%); | |
} | |
main { | |
width: 100%; | |
max-width: 1200px; | |
padding: 2rem; | |
} | |
.ecg-container { | |
position: relative; | |
background: var(--panel); | |
border-radius: 1.5rem; | |
padding: 2rem; | |
box-shadow: | |
0 0 40px rgba(0, 255, 136, 0.1), | |
0 20px 60px rgba(0, 0, 0, 0.3); | |
backdrop-filter: blur(20px); | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
} | |
.header { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 1.5rem; | |
flex-wrap: wrap; | |
gap: 1rem; | |
} | |
h1 { | |
font-size: clamp(1.5rem, 4vw, 2.5rem); | |
font-weight: 700; | |
background: linear-gradient(135deg, var(--primary), var(--accent)); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
background-clip: text; | |
} | |
.controls { | |
display: flex; | |
align-items: center; | |
gap: 1rem; | |
flex-wrap: wrap; | |
} | |
.slider-container { | |
display: flex; | |
align-items: center; | |
gap: 0.75rem; | |
background: var(--glass); | |
padding: 0.75rem 1rem; | |
border-radius: 0.75rem; | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
} | |
label { | |
font-size: 0.875rem; | |
font-weight: 600; | |
color: rgba(255, 255, 255, 0.8); | |
} | |
.bpm-display { | |
font-size: 1.125rem; | |
font-weight: 700; | |
color: var(--primary); | |
min-width: 2.5rem; | |
text-align: center; | |
} | |
input[type="range"] { | |
width: 150px; | |
height: 4px; | |
background: var(--grid); | |
border-radius: 2px; | |
outline: none; | |
-webkit-appearance: none; | |
cursor: pointer; | |
} | |
input[type="range"]::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
width: 18px; | |
height: 18px; | |
background: var(--primary); | |
border-radius: 50%; | |
box-shadow: 0 0 10px rgba(0, 255, 136, 0.5); | |
transition: all 0.3s ease; | |
} | |
input[type="range"]::-webkit-slider-thumb:hover { | |
transform: scale(1.2); | |
box-shadow: 0 0 20px rgba(0, 255, 136, 0.8); | |
} | |
.stats { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); | |
gap: 1rem; | |
margin-top: 1.5rem; | |
} | |
.stat-card { | |
background: var(--glass); | |
padding: 1rem; | |
border-radius: 0.75rem; | |
text-align: center; | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
transition: transform 0.3s ease; | |
} | |
.stat-card:hover { | |
transform: translateY(-2px); | |
} | |
.stat-value { | |
font-size: 1.5rem; | |
font-weight: 700; | |
color: var(--primary); | |
} | |
.stat-label { | |
font-size: 0.75rem; | |
color: rgba(255, 255, 255, 0.6); | |
margin-top: 0.25rem; | |
} | |
canvas { | |
display: block; | |
border-radius: 0.5rem; | |
background: #000; | |
} | |
@media (max-width: 600px) { | |
main { | |
padding: 1rem; | |
} | |
.ecg-container { | |
padding: 1.5rem; | |
} | |
.header { | |
flex-direction: column; | |
align-items: stretch; | |
} | |
.controls { | |
justify-content: center; | |
} | |
.slider-container { | |
flex-direction: column; | |
gap: 0.5rem; | |
} | |
input[type="range"] { | |
width: 120px; | |
} | |
} | |
.pulse { | |
animation: pulse 1s ease-in-out; | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); } | |
50% { transform: scale(1.1); } | |
100% { transform: scale(1); } | |
} | |
</style> | |
</head> | |
<body> | |
<main> | |
<div class="ecg-container"> | |
<div class="header"> | |
<h1>ECG Simulator</h1> | |
<div class="controls"> | |
<div class="slider-container"> | |
<label for="bpmSlider">BPM</label> | |
<input type="range" id="bpmSlider" min="40" max="180" value="75" /> | |
<span class="bpm-display" id="bpmValue">75</span> | |
</div> | |
</div> | |
</div> | |
<div id="canvas-container"></div> | |
<div class="stats"> | |
<div class="stat-card"> | |
<div class="stat-value" id="heartRate">75</div> | |
<div class="stat-label">Heart Rate</div> | |
</div> | |
<div class="stat-card"> | |
<div class="stat-value" id="rhythm">Sinus</div> | |
<div class="stat-label">Rhythm</div> | |
</div> | |
<div class="stat-card"> | |
<div class="stat-value" id="status">Normal</div> | |
<div class="stat-label">Status</div> | |
</div> | |
</div> | |
</div> | |
</main> | |
<script> | |
let ecg = []; | |
let currentWaveform = []; | |
let waveIndex = 0; | |
let bpmSlider; | |
let interval = 1000; | |
let heartSize = 20; | |
let beatTimer = 0; | |
function setup() { | |
const canvas = createCanvas(windowWidth - 80, 300); | |
canvas.parent('canvas-container'); | |
bpmSlider = document.getElementById('bpmSlider'); | |
const bpmValue = document.getElementById('bpmValue'); | |
const heartRate = document.getElementById('heartRate'); | |
bpmSlider.addEventListener('input', (e) => { | |
bpmValue.textContent = e.target.value; | |
heartRate.textContent = e.target.value; | |
}); | |
generateWaveform(); | |
} | |
function draw() { | |
background(0); | |
drawGrid(); | |
let bpm = parseInt(bpmSlider.value); | |
interval = 60000 / bpm; | |
// Stream one waveform point per frame | |
if (waveIndex < currentWaveform.length) { | |
ecg.push(currentWaveform[waveIndex]); | |
waveIndex++; | |
} else { | |
generateWaveform(); | |
waveIndex = 0; | |
beatTimer = 10; // Trigger pulse | |
// Add pulse animation to heart icon | |
const heartIcon = document.querySelector('.heart-icon'); | |
if (heartIcon) { | |
heartIcon.classList.add('pulse'); | |
setTimeout(() => heartIcon.classList.remove('pulse'), 1000); | |
} | |
} | |
// Draw ECG | |
stroke(0, 255, 136); | |
strokeWeight(2); | |
noFill(); | |
beginShape(); | |
for (let i = 0; i < ecg.length; i++) { | |
vertex(i, height / 2 - ecg[i]); | |
} | |
endShape(); | |
// Green pulse dot at end | |
let dotY = height / 2 - ecg[ecg.length - 1]; | |
fill(0, 255, 136); | |
noStroke(); | |
ellipse(ecg.length - 1, dotY, 8); | |
// Heart icon | |
push(); | |
translate(width - 60, 50); | |
scale((heartSize + sin(frameCount * 0.3) * (beatTimer > 0 ? 6 : 0)) / 100); | |
fill(255, 0, 64); | |
noStroke(); | |
beginShape(); | |
vertex(0, -30); | |
bezierVertex(-25, -55, -55, -15, 0, 30); | |
bezierVertex(55, -15, 25, -55, 0, -30); | |
endShape(CLOSE); | |
pop(); | |
if (beatTimer > 0) beatTimer--; | |
// Scroll effect: remove leftmost point when full | |
if (ecg.length > width) ecg.shift(); | |
} | |
function generateWaveform() { | |
currentWaveform = []; | |
for (let i = 0; i < 10; i++) currentWaveform.push(0); // baseline | |
for (let i = 0; i < 10; i++) currentWaveform.push(sin(map(i, 0, 10, 0, PI)) * 10); // P wave | |
for (let i = 0; i < 5; i++) currentWaveform.push(-4); // dip | |
for (let i = 0; i < 2; i++) currentWaveform.push(-30); // Q | |
for (let i = 0; i < 2; i++) currentWaveform.push(60); // R | |
for (let i = 0; i < 4; i++) currentWaveform.push(-20); // S | |
for (let i = 0; i < 20; i++) currentWaveform.push(sin(map(i, 0, 20, 0, PI)) * 15); // T | |
for (let i = 0; i < 20; i++) currentWaveform.push(0); // flat after T | |
} | |
function drawGrid() { | |
stroke(30, 30, 46); | |
strokeWeight(1); | |
for (let x = 0; x < width; x += 20) { | |
line(x, 0, x, height); | |
} | |
for (let y = 0; y < height; y += 20) { | |
line(0, y, width, y); | |
} | |
} | |
function windowResized() { | |
resizeCanvas(windowWidth - 80, 300); | |
} | |
</script> | |
</body> | |
</html> |