Task 3: Build second and third heartbeats with overflow branching

- Second heartbeat: R peak 60px, color shifts green→teal (#00C9A7), bg lightens to #0A0A0A
- Third heartbeat: R peak 100px, full teal (#00897B), bg lightens to #141414
- Overflow branching from third R peak apex: 7 SVG branch paths trace UI outlines
  - Branch 1: pill nav bar rounded rectangle at top center
  - Branches 2-3: hero section left/right edges
  - Branches 4-7: four vital sign card outlines
- Branches staggered by 50-150ms with cubic-bezier easing (800ms draw)
- Background transitions rapidly from near-black to white during branching
- All SVG lines fade out over 500ms, overlays removed from DOM, CV content revealed
- Glow filter dynamically matched to stroke color (green→teal)
- Total animation timing ~8.5s (within 8-9s guardrail)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 10:41:46 +00:00
parent 3de529ef50
commit 80ff51a1ee
+193 -17
View File
@@ -141,6 +141,15 @@
filter: drop-shadow(0 0 6px rgba(0, 255, 65, 0.7)); filter: drop-shadow(0 0 6px rgba(0, 255, 65, 0.7));
} }
.ecg-branch {
fill: none;
stroke: #00897B;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 0 3px rgba(0, 137, 123, 0.5));
}
/* ========================================= /* =========================================
FINAL CV CONTENT (hidden behind boot) FINAL CV CONTENT (hidden behind boot)
========================================= */ ========================================= */
@@ -238,6 +247,7 @@
function startECGPhase() { function startECGPhase() {
var overlay = document.getElementById('ecg-overlay'); var overlay = document.getElementById('ecg-overlay');
var bootScreen = document.getElementById('boot-screen');
var vw = window.innerWidth; var vw = window.innerWidth;
var vh = window.innerHeight; var vh = window.innerHeight;
var cy = Math.round(vh / 2); var cy = Math.round(vh / 2);
@@ -266,17 +276,187 @@
flatlinePath.style.strokeDashoffset = '0'; flatlinePath.style.strokeDashoffset = '0';
}); });
// --- FIRST HEARTBEAT (after flatline completes) --- // --- FIRST HEARTBEAT (R peak 40px, green) ---
setTimeout(function() { setTimeout(function() {
drawHeartbeat(svg, svgNS, vw, vh, cy, 40, '#00ff41', 600, function() { drawHeartbeat(svg, svgNS, vw, vh, cy, 40, '#00ff41', 600, function() {
// After first heartbeat, hold 300ms then signal done (Task 3 will continue) // Hold 300ms then second heartbeat
setTimeout(function() { setTimeout(function() {
finishECGPhase();
// --- SECOND HEARTBEAT (R peak 60px, transitioning color) ---
// Bg begins lightening slightly
bootScreen.style.transition = 'background 600ms ease';
bootScreen.style.background = '#0A0A0A';
drawHeartbeat(svg, svgNS, vw, vh, cy, 60, '#00C9A7', 600, function() {
// Hold 300ms then third heartbeat
setTimeout(function() {
// --- THIRD HEARTBEAT (R peak 100px, full teal) ---
bootScreen.style.transition = 'background 600ms ease';
bootScreen.style.background = '#141414';
drawHeartbeat(svg, svgNS, vw, vh, cy, 100, '#00897B', 600, function() {
// At the apex of the third beat — launch branching
startBranching(svg, svgNS, vw, vh, cy, overlay, bootScreen);
});
}, 300);
});
}, 300); }, 300);
}); });
}, 1050); }, 1050);
} }
/**
* Creates the overflow branching effect from the third heartbeat peak.
* Branch lines shoot outward and trace the outlines of UI elements.
*/
function startBranching(svg, svgNS, vw, vh, cy, overlay, bootScreen) {
var cx = Math.round(vw / 2);
var peakY = cy - 100; // Third beat R peak position
// Calculate UI element positions for branches to trace
// Pill nav: rounded rectangle at top center
var navW = Math.min(520, vw - 64);
var navH = 44;
var navX = Math.round((vw - navW) / 2);
var navY = 16;
var navR = 22; // border-radius
// Hero section: centered area
var heroW = Math.min(600, vw - 80);
var heroX = Math.round((vw - heroW) / 2);
var heroY = Math.round(vh * 0.2);
var heroH = Math.round(vh * 0.5);
// Card outlines (vital sign cards)
var cardW = 160;
var cardH = 80;
var cardGap = 16;
var totalCardsW = 4 * cardW + 3 * cardGap;
var cardsStartX = Math.round((vw - totalCardsW) / 2);
var cardsY = Math.round(vh * 0.65);
// Branch paths — each starts from the peak and traces outward
var branches = [];
// Branch 1: Shoots up to trace the pill nav bar outline
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + cx + ' ' + (navY + navH + 20) + ', ' + (navX + navR) + ' ' + (navY + navH) +
' L ' + (navX + navR) + ' ' + (navY + navH) +
' Q ' + navX + ' ' + (navY + navH) + ', ' + navX + ' ' + (navY + navH - navR) +
' L ' + navX + ' ' + (navY + navR) +
' Q ' + navX + ' ' + navY + ', ' + (navX + navR) + ' ' + navY +
' L ' + (navX + navW - navR) + ' ' + navY +
' Q ' + (navX + navW) + ' ' + navY + ', ' + (navX + navW) + ' ' + (navY + navR) +
' L ' + (navX + navW) + ' ' + (navY + navH - navR) +
' Q ' + (navX + navW) + ' ' + (navY + navH) + ', ' + (navX + navW - navR) + ' ' + (navY + navH) +
' L ' + (navX + navR) + ' ' + (navY + navH),
delay: 0
});
// Branch 2: Shoots left to trace the left edge of hero container
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (heroX + 40) + ' ' + (peakY - 30) + ', ' + heroX + ' ' + heroY +
' L ' + heroX + ' ' + (heroY + heroH),
delay: 80
});
// Branch 3: Shoots right to trace the right edge of hero container
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (heroX + heroW - 40) + ' ' + (peakY - 30) + ', ' + (heroX + heroW) + ' ' + heroY +
' L ' + (heroX + heroW) + ' ' + (heroY + heroH),
delay: 80
});
// Branch 4: Down-left to first card outline (trace rectangle, no Z back to peak)
var c1x = cardsStartX;
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (cx - 80) + ' ' + (cardsY - 40) + ', ' + c1x + ' ' + cardsY +
' L ' + (c1x + cardW) + ' ' + cardsY +
' L ' + (c1x + cardW) + ' ' + (cardsY + cardH) +
' L ' + c1x + ' ' + (cardsY + cardH) +
' L ' + c1x + ' ' + cardsY,
delay: 150
});
// Branch 5: Down to second card outline
var c2x = cardsStartX + cardW + cardGap;
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (c2x + cardW / 2) + ' ' + (cardsY - 20) + ', ' + c2x + ' ' + cardsY +
' L ' + (c2x + cardW) + ' ' + cardsY +
' L ' + (c2x + cardW) + ' ' + (cardsY + cardH) +
' L ' + c2x + ' ' + (cardsY + cardH) +
' L ' + c2x + ' ' + cardsY,
delay: 200
});
// Branch 6: Down-right to third card outline
var c3x = cardsStartX + 2 * (cardW + cardGap);
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (cx + 60) + ' ' + (cardsY - 30) + ', ' + c3x + ' ' + cardsY +
' L ' + (c3x + cardW) + ' ' + cardsY +
' L ' + (c3x + cardW) + ' ' + (cardsY + cardH) +
' L ' + c3x + ' ' + (cardsY + cardH) +
' L ' + c3x + ' ' + cardsY,
delay: 250
});
// Branch 7: Far right to fourth card outline
var c4x = cardsStartX + 3 * (cardW + cardGap);
branches.push({
d: 'M ' + cx + ' ' + peakY +
' Q ' + (cx + 120) + ' ' + (cardsY - 20) + ', ' + c4x + ' ' + cardsY +
' L ' + (c4x + cardW) + ' ' + cardsY +
' L ' + (c4x + cardW) + ' ' + (cardsY + cardH) +
' L ' + c4x + ' ' + (cardsY + cardH) +
' L ' + c4x + ' ' + cardsY,
delay: 300
});
// Animate each branch with staggered timing
var branchDuration = 800;
var maxDelay = 0;
branches.forEach(function(branch) {
if (branch.delay > maxDelay) maxDelay = branch.delay;
setTimeout(function() {
var path = document.createElementNS(svgNS, 'path');
path.setAttribute('d', branch.d);
path.setAttribute('class', 'ecg-branch');
svg.appendChild(path);
var len = path.getTotalLength();
path.style.strokeDasharray = len;
path.style.strokeDashoffset = len;
requestAnimationFrame(function() {
path.style.transition = 'stroke-dashoffset ' + branchDuration + 'ms cubic-bezier(0.25, 0.46, 0.45, 0.94)';
path.style.strokeDashoffset = '0';
});
}, branch.delay);
});
// Background rapidly transitions to white during branching
setTimeout(function() {
bootScreen.style.transition = 'background 800ms ease-out';
bootScreen.style.background = '#FFFFFF';
}, 200);
// After all branches finish drawing, fade out and reveal
var totalBranchTime = maxDelay + branchDuration + 100;
setTimeout(function() {
finishECGPhase(overlay, bootScreen);
}, totalBranchTime);
}
/** /**
* Draws a PQRST waveform centered horizontally on the screen. * Draws a PQRST waveform centered horizontally on the screen.
* @param {SVGElement} svg - The SVG container * @param {SVGElement} svg - The SVG container
@@ -353,6 +533,10 @@
beatPath.setAttribute('d', d); beatPath.setAttribute('d', d);
beatPath.setAttribute('class', 'ecg-heartbeat'); beatPath.setAttribute('class', 'ecg-heartbeat');
beatPath.style.stroke = color; beatPath.style.stroke = color;
// Match the glow filter to the stroke color
if (color !== '#00ff41') {
beatPath.style.filter = 'drop-shadow(0 0 6px rgba(0, 137, 123, 0.7))';
}
svg.appendChild(beatPath); svg.appendChild(beatPath);
var beatLen = beatPath.getTotalLength(); var beatLen = beatPath.getTotalLength();
@@ -372,25 +556,17 @@
} }
/** /**
* Temporary: finish ECG phase by hiding overlays and revealing CV. * Final cleanup: fade out all SVG lines and reveal the CV content.
* Task 3 will expand this with second/third heartbeats and branching.
*/ */
function finishECGPhase() { function finishECGPhase(overlay, bootScreen) {
var overlay = document.getElementById('ecg-overlay'); // Fade out all SVG lines
var bootScreen = document.getElementById('boot-screen');
// Fade out the ECG lines
overlay.style.transition = 'opacity 500ms ease'; overlay.style.transition = 'opacity 500ms ease';
overlay.style.opacity = '0'; overlay.style.opacity = '0';
// Transition boot screen background from black to white
bootScreen.style.transition = 'background 500ms ease';
bootScreen.style.background = '#FFFFFF';
setTimeout(function() { setTimeout(function() {
// Remove overlays // Remove overlays from DOM
overlay.style.display = 'none'; if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
bootScreen.style.display = 'none'; if (bootScreen.parentNode) bootScreen.parentNode.removeChild(bootScreen);
// Reveal CV content // Reveal CV content
var cvContent = document.getElementById('cv-content'); var cvContent = document.getElementById('cv-content');