Lub Dub | Valves =link=
function getCycleIntervalMs() const bpm = parseInt(bpmSlider.value, 10); // one cardiac cycle = 60/BPM seconds → milliseconds return (60 / bpm) * 1000;
function activateDub() dubValveDiv.classList.add('active'); setTimeout(() => dubValveDiv.classList.remove('active'), 180);
input flex: 1; accent-color: #ff8c5a;
// helper: simple beep with decay (lub = lower freq, dub = higher + shorter) function playLub() if (audioCtx.state === 'suspended') audioCtx.resume(); const now = audioCtx.currentTime; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); osc.frequency.value = 85; // thud-like gain.gain.value = 0.45; gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.35); osc.start(); osc.stop(now + 0.35); // extra low harmonic for "lub" richness const osc2 = audioCtx.createOscillator(); const gain2 = audioCtx.createGain(); osc2.connect(gain2); gain2.connect(audioCtx.destination); osc2.frequency.value = 170; gain2.gain.value = 0.2; gain2.gain.exponentialRampToValueAtTime(0.0001, now + 0.28); osc2.start(); osc2.stop(now + 0.28);
.valve-icon font-size: 5rem; transition: transform 0.1s ease; filter: drop-shadow(0 4px 8px rgba(0,0,0,0.5)); lub dub valves
.valves-container display: flex; justify-content: space-between; gap: 2rem; flex-wrap: wrap; margin-bottom: 2rem;
.lub .valve-title color: #ff9f7c; .dub .valve-title color: #7cd4ff; function getCycleIntervalMs() const bpm = parseInt(bpmSlider
button background: #2c3e4e; border: none; font-size: 1.2rem; font-weight: bold; padding: 0.8rem 1.6rem; border-radius: 60px; color: white; cursor: pointer; transition: 0.1s linear; box-shadow: 0 4px 8px black; font-family: monospace;