80ff51a1ee
- 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>
581 lines
18 KiB
HTML
581 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Andy Charlwood — MPharm | CV</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Inter+Tight:wght@400;500;600&display=swap" rel="stylesheet">
|
|
<style>
|
|
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
:root {
|
|
--bg: #FFFFFF;
|
|
--text: #334155;
|
|
--heading: #0F172A;
|
|
--teal: #00897B;
|
|
--teal-light: rgba(0, 137, 123, 0.08);
|
|
--teal-medium: rgba(0, 137, 123, 0.15);
|
|
--coral: #FF6B6B;
|
|
--coral-light: rgba(255, 107, 107, 0.08);
|
|
--muted: #94A3B8;
|
|
--border: #E2E8F0;
|
|
--card-bg: #FFFFFF;
|
|
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
|
|
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
|
--shadow-lg: 0 8px 24px rgba(0,0,0,0.1);
|
|
--radius: 16px;
|
|
--font-primary: 'Plus Jakarta Sans', system-ui, sans-serif;
|
|
--font-secondary: 'Inter Tight', system-ui, sans-serif;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--font-primary);
|
|
font-size: 15px;
|
|
line-height: 1.7;
|
|
margin: 0;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
/* =========================================
|
|
BOOT SCREEN
|
|
========================================= */
|
|
|
|
#boot-screen {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: #000;
|
|
z-index: 1000;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
padding: 40px;
|
|
font-family: 'Fira Code', monospace;
|
|
font-size: 14px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#boot-lines {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
max-width: 640px;
|
|
}
|
|
|
|
.boot-line {
|
|
opacity: 0;
|
|
transform: translateY(8px);
|
|
transition: opacity 400ms ease-out, transform 400ms ease-out;
|
|
white-space: nowrap;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.boot-line.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.boot-line.fading {
|
|
opacity: 0;
|
|
transition: opacity 800ms ease;
|
|
}
|
|
|
|
.c-green { color: #00ff41; }
|
|
.c-green-bold { color: #00ff41; font-weight: 700; }
|
|
.c-cyan { color: #00e5ff; }
|
|
.c-dim { color: #3a6b45; }
|
|
.c-grey { color: #666; }
|
|
|
|
/* Blinking cursor */
|
|
#boot-cursor {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 16px;
|
|
background: #00ff41;
|
|
animation: blink 1s step-end infinite;
|
|
vertical-align: middle;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0; }
|
|
}
|
|
|
|
/* =========================================
|
|
ECG OVERLAY
|
|
========================================= */
|
|
|
|
#ecg-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 1001;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#ecg-overlay svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
|
|
.ecg-line {
|
|
fill: none;
|
|
stroke: #00ff41;
|
|
stroke-width: 2;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
filter: drop-shadow(0 0 4px rgba(0, 255, 65, 0.6));
|
|
}
|
|
|
|
.ecg-heartbeat {
|
|
fill: none;
|
|
stroke: #00ff41;
|
|
stroke-width: 2;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
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)
|
|
========================================= */
|
|
|
|
#cv-content {
|
|
opacity: 0;
|
|
}
|
|
|
|
#cv-content.revealed {
|
|
opacity: 1;
|
|
transition: opacity 600ms ease;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Boot Screen Overlay -->
|
|
<div id="boot-screen">
|
|
<div id="boot-lines"></div>
|
|
</div>
|
|
|
|
<!-- ECG Overlay (created dynamically but container lives here) -->
|
|
<div id="ecg-overlay"></div>
|
|
|
|
<!-- Final CV Content (hidden behind boot screen until transition) -->
|
|
<div id="cv-content">
|
|
<!-- CV sections will be built in subsequent tasks -->
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
/* =========================================
|
|
BOOT SEQUENCE
|
|
========================================= */
|
|
|
|
var bootLines = [
|
|
{ html: '<span class="c-green-bold">CLINICAL TERMINAL v3.2.1</span>', delay: 0 },
|
|
{ html: '<span class="c-dim">Initialising pharmacist profile...</span>', delay: 220 },
|
|
{ html: '<span class="c-dim">---</span>', delay: 220 },
|
|
{ html: '<span class="c-cyan">SYSTEM </span><span class="c-green">NHS Norfolk & Waveney ICB</span>', delay: 220 },
|
|
{ html: '<span class="c-cyan">USER </span><span class="c-green">Andy Charlwood</span>', delay: 220 },
|
|
{ html: '<span class="c-cyan">ROLE </span><span class="c-green">Deputy Head of Population Health & Data Analysis</span>', delay: 220 },
|
|
{ html: '<span class="c-cyan">LOCATION </span><span class="c-green">Norwich, UK</span>', delay: 220 },
|
|
{ html: '<span class="c-dim">---</span>', delay: 220 },
|
|
{ html: '<span class="c-dim">Loading modules...</span>', delay: 220 },
|
|
{ html: '<span class="c-green-bold">[OK]</span> <span class="c-dim">pharmacist_core.sys</span>', delay: 220 },
|
|
{ html: '<span class="c-green-bold">[OK]</span> <span class="c-dim">population_health.mod</span>', delay: 220 },
|
|
{ html: '<span class="c-green-bold">[OK]</span> <span class="c-dim">data_analytics.eng</span>', delay: 220 },
|
|
{ html: '<span class="c-dim">---</span>', delay: 220 },
|
|
{ html: '<span class="c-green-bold">> READY — Rendering CV...</span><span id="boot-cursor"></span>', delay: 220 }
|
|
];
|
|
|
|
var bootContainer = document.getElementById('boot-lines');
|
|
var totalBootTime = 0;
|
|
|
|
bootLines.forEach(function(line, index) {
|
|
totalBootTime += line.delay;
|
|
var el = document.createElement('div');
|
|
el.className = 'boot-line';
|
|
el.innerHTML = line.html;
|
|
bootContainer.appendChild(el);
|
|
|
|
setTimeout(function() {
|
|
el.classList.add('visible');
|
|
}, totalBootTime);
|
|
});
|
|
|
|
// After all lines are visible, wait 400ms then remove cursor and fade boot text
|
|
var bootEndTime = totalBootTime + 400;
|
|
|
|
setTimeout(function() {
|
|
// Remove cursor
|
|
var cursor = document.getElementById('boot-cursor');
|
|
if (cursor) {
|
|
cursor.remove();
|
|
}
|
|
|
|
// Fade out all boot lines
|
|
var allLines = bootContainer.querySelectorAll('.boot-line');
|
|
for (var i = 0; i < allLines.length; i++) {
|
|
allLines[i].classList.add('fading');
|
|
}
|
|
}, bootEndTime);
|
|
|
|
// After fade out (800ms), start the ECG phase
|
|
setTimeout(function() {
|
|
startECGPhase();
|
|
}, bootEndTime + 800);
|
|
|
|
/* =========================================
|
|
ECG PHASE: Flatline + First Heartbeat
|
|
========================================= */
|
|
|
|
function startECGPhase() {
|
|
var overlay = document.getElementById('ecg-overlay');
|
|
var bootScreen = document.getElementById('boot-screen');
|
|
var vw = window.innerWidth;
|
|
var vh = window.innerHeight;
|
|
var cy = Math.round(vh / 2);
|
|
|
|
// Create SVG
|
|
var svgNS = 'http://www.w3.org/2000/svg';
|
|
var svg = document.createElementNS(svgNS, 'svg');
|
|
svg.setAttribute('viewBox', '0 0 ' + vw + ' ' + vh);
|
|
svg.setAttribute('preserveAspectRatio', 'none');
|
|
overlay.appendChild(svg);
|
|
|
|
// --- FLATLINE ---
|
|
var flatlinePath = document.createElementNS(svgNS, 'path');
|
|
var flatlineD = 'M 0 ' + cy + ' L ' + vw + ' ' + cy;
|
|
flatlinePath.setAttribute('d', flatlineD);
|
|
flatlinePath.setAttribute('class', 'ecg-line');
|
|
svg.appendChild(flatlinePath);
|
|
|
|
var flatlineLen = flatlinePath.getTotalLength();
|
|
flatlinePath.style.strokeDasharray = flatlineLen;
|
|
flatlinePath.style.strokeDashoffset = flatlineLen;
|
|
|
|
// Animate flatline drawing left-to-right over 1000ms
|
|
requestAnimationFrame(function() {
|
|
flatlinePath.style.transition = 'stroke-dashoffset 1000ms linear';
|
|
flatlinePath.style.strokeDashoffset = '0';
|
|
});
|
|
|
|
// --- FIRST HEARTBEAT (R peak 40px, green) ---
|
|
setTimeout(function() {
|
|
drawHeartbeat(svg, svgNS, vw, vh, cy, 40, '#00ff41', 600, function() {
|
|
// Hold 300ms then second heartbeat
|
|
setTimeout(function() {
|
|
|
|
// --- 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);
|
|
});
|
|
}, 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.
|
|
* @param {SVGElement} svg - The SVG container
|
|
* @param {string} svgNS - SVG namespace
|
|
* @param {number} vw - Viewport width
|
|
* @param {number} vh - Viewport height
|
|
* @param {number} cy - Vertical center (baseline Y)
|
|
* @param {number} rHeight - Height of the R peak above baseline
|
|
* @param {string} color - Stroke color
|
|
* @param {number} duration - Animation duration in ms
|
|
* @param {function} onComplete - Callback when animation finishes
|
|
*/
|
|
function drawHeartbeat(svg, svgNS, vw, vh, cy, rHeight, color, duration, onComplete) {
|
|
// PQRST waveform shape relative to center point
|
|
// Total width of the waveform: ~160px
|
|
var waveWidth = 160;
|
|
var startX = Math.round((vw - waveWidth) / 2);
|
|
|
|
// Build the PQRST path
|
|
// P wave: gentle upward bump, ~8px above baseline, ~30px wide
|
|
// Flat: ~10px
|
|
// Q dip: sharp dip ~10px below, ~8px wide
|
|
// R spike: sharp peak rHeight above baseline, ~12px wide
|
|
// S dip: sharp dip ~15px below, ~8px wide
|
|
// Flat: ~10px
|
|
// T wave: gentle upward bump, ~12px above, ~35px wide
|
|
// Return to baseline
|
|
|
|
var x = startX;
|
|
var d = 'M ' + x + ' ' + cy;
|
|
|
|
// Lead-in flat segment
|
|
d += ' L ' + (x + 10) + ' ' + cy;
|
|
x += 10;
|
|
|
|
// P wave (cubic bezier for smooth bump)
|
|
d += ' C ' + (x + 8) + ' ' + cy + ', ' + (x + 10) + ' ' + (cy - 8) + ', ' + (x + 15) + ' ' + (cy - 8);
|
|
d += ' C ' + (x + 20) + ' ' + (cy - 8) + ', ' + (x + 22) + ' ' + cy + ', ' + (x + 30) + ' ' + cy;
|
|
x += 30;
|
|
|
|
// Flat segment before QRS
|
|
d += ' L ' + (x + 10) + ' ' + cy;
|
|
x += 10;
|
|
|
|
// Q dip
|
|
d += ' L ' + (x + 4) + ' ' + (cy + 10);
|
|
x += 4;
|
|
|
|
// R spike (sharp peak)
|
|
d += ' L ' + (x + 6) + ' ' + (cy - rHeight);
|
|
x += 6;
|
|
|
|
// S dip
|
|
d += ' L ' + (x + 6) + ' ' + (cy + 15);
|
|
x += 6;
|
|
|
|
// Return to baseline
|
|
d += ' L ' + (x + 4) + ' ' + cy;
|
|
x += 4;
|
|
|
|
// Flat segment before T wave
|
|
d += ' L ' + (x + 10) + ' ' + cy;
|
|
x += 10;
|
|
|
|
// T wave (cubic bezier for smooth bump)
|
|
d += ' C ' + (x + 8) + ' ' + cy + ', ' + (x + 12) + ' ' + (cy - 12) + ', ' + (x + 17) + ' ' + (cy - 12);
|
|
d += ' C ' + (x + 22) + ' ' + (cy - 12) + ', ' + (x + 27) + ' ' + cy + ', ' + (x + 35) + ' ' + cy;
|
|
x += 35;
|
|
|
|
// Trail-out flat segment
|
|
d += ' L ' + (x + 10) + ' ' + cy;
|
|
|
|
var beatPath = document.createElementNS(svgNS, 'path');
|
|
beatPath.setAttribute('d', d);
|
|
beatPath.setAttribute('class', 'ecg-heartbeat');
|
|
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);
|
|
|
|
var beatLen = beatPath.getTotalLength();
|
|
beatPath.style.strokeDasharray = beatLen;
|
|
beatPath.style.strokeDashoffset = beatLen;
|
|
|
|
// Animate the heartbeat drawing
|
|
requestAnimationFrame(function() {
|
|
beatPath.style.transition = 'stroke-dashoffset ' + duration + 'ms ease-in-out';
|
|
beatPath.style.strokeDashoffset = '0';
|
|
});
|
|
|
|
// Callback after animation
|
|
if (onComplete) {
|
|
setTimeout(onComplete, duration + 50);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Final cleanup: fade out all SVG lines and reveal the CV content.
|
|
*/
|
|
function finishECGPhase(overlay, bootScreen) {
|
|
// Fade out all SVG lines
|
|
overlay.style.transition = 'opacity 500ms ease';
|
|
overlay.style.opacity = '0';
|
|
|
|
setTimeout(function() {
|
|
// Remove overlays from DOM
|
|
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
if (bootScreen.parentNode) bootScreen.parentNode.removeChild(bootScreen);
|
|
|
|
// Reveal CV content
|
|
var cvContent = document.getElementById('cv-content');
|
|
cvContent.classList.add('revealed');
|
|
}, 500);
|
|
}
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|