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/p5@1.9.0/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> |