# Reference: Boot Sequence + ECG Animation > Covers the full pre-login flow: terminal boot → cursor transition → ECG heartbeat → name reveal → flatline. The flatline→login transition is covered in `ref-transition-login.md`. --- ## Current Architecture Two components manage the pre-login flow: - `src/components/BootSequence.tsx` → terminal text animation, ends with blinking cursor - `src/components/ECGAnimation.tsx` → canvas-based heartbeat + name tracing + flatline + bg transition - `App.tsx` phases: `boot → ecg → login → pmr` ## What Needs to Change ### 1. Boot Sequence — Clean Up for Easy Config **Problem:** Boot text lines are hardcoded as HTML strings with inline Tailwind classes. Adding/removing/reordering lines requires editing raw HTML. The `dangerouslySetInnerHTML` approach is fragile. **Fix:** Refactor to a clean config-driven structure: ```typescript // Example config structure — easy to customize const BOOT_CONFIG = { header: { text: 'CLINICAL TERMINAL v3.2.1', style: 'bright' }, lines: [ { type: 'status', text: 'Initialising pharmacist profile...' }, { type: 'separator' }, { type: 'field', label: 'SYSTEM', value: 'NHS Norfolk & Waveney ICB' }, { type: 'field', label: 'USER', value: 'Andy Charlwood' }, { type: 'field', label: 'ROLE', value: 'Deputy Head of Population Health & Data Analysis' }, { type: 'field', label: 'LOCATION', value: 'Norwich, UK' }, { type: 'separator' }, { type: 'status', text: 'Loading modules...' }, { type: 'module', name: 'pharmacist_core.sys' }, { type: 'module', name: 'population_health.mod' }, { type: 'module', name: 'data_analytics.eng' }, { type: 'separator' }, { type: 'ready', text: 'READY — Rendering CV..' }, ], timing: { lineDelay: 220, holdAfterComplete: 400, fadeOutDuration: 800 }, } ``` - Each line type maps to a React component (not raw HTML) - Colors remain: bright green `#00ff41`, dim green `#3a6b45`, cyan labels `#00e5ff` - Staggered reveal timing stays the same (220ms per line) - Font: Fira Code (this is the terminal phase, NOT the PMR — Fira Code is correct here) ### 2. Cursor → Dot Transition **Problem:** The boot sequence ends with a blinking green block cursor (`.animate-blink`). The ECG animation starts with a glowing dot that appears at the far left of the screen. There's a visual disconnect — the cursor and dot don't connect. **Fix:** The blinking cursor at the end of boot should smoothly transition INTO the ECG's glowing trace dot: - At end of boot, capture the cursor's screen position (x, y) - Pass this position to ECGAnimation via props - ECGAnimation starts with its glowing dot AT the cursor position - The cursor stops blinking and morphs: block cursor → circular glow (scale down width, increase glow, ~300ms) - The dot then begins moving rightward, drawing the flatline/heartbeat trace behind it - This means the ECG trace starts at the cursor position, NOT the far left edge ### 3. ECG Start Position **Problem:** Currently the ECG trace starts at x=0 (far left of viewport). The cursor ends somewhere in the middle-left of the screen. This means the dot "teleports" from cursor position to the left edge. **Fix:** The ECG animation should: - Accept a `startPosition: { x: number, y: number }` prop from the boot sequence - Begin the trace from that position - The first section of trace is a flat line from the cursor position rightward - Heartbeats begin after the first flat gap (same timing as now, just offset) - The viewport scroll logic already handles the "head" position — just shift the world-space origin ### 4. Text Reveal — Mask Technique **Problem:** The current ECGAnimation.tsx reveals letters by stroking them with progressive alpha (`letterProgress > 0.3` → fade in). This looks like letters fading in, not like the ECG line IS the letter shape. **Reference:** `ECGCombined.tsx` (Remotion version at project root) uses a superior technique: - The actual text characters are pre-rendered on the canvas (stroke-only, no fill) - A wipe mask follows the ECG trace head, revealing the text underneath - The ECG trace line IS the path that forms each letter shape (via the `getYAtX` function which returns letter Y values when in text region) - Connector lines between letters sit at the baseline - The neon glow filter applies to both the trace and revealed text **What to adopt from ECGCombined.tsx:** - The mask-based text reveal approach (the trace unveils pre-rendered text) - The connector lines between letters at baseline - The neon glow SVG filters (or canvas equivalents) - The text mask brush that follows the trace head **What to KEEP from current ECGAnimation.tsx:** - The character spacing (current `LETTER_W`, `LETTER_G`, `SPACE_W` values — preferred over ECGCombined.tsx spacing) - The heartbeat waveform shape (`generateHeartbeatPoints`) - The beat timing and amplitude escalation (0.3 → 0.55 → 0.85 → 1.0) - The canvas rendering approach (not SVG — canvas is correct for this performance-sensitive animation) - The viewport scrolling/camera logic - The flatline draw phase - The scanline and vignette effects - The background color transition to `#1E293B` ### 5. Text Rendering - The name is still "ANDREW CHARLWOOD" - Letters are stroke-only (no fill) in neon green `#00ff41` - Each letter shape is defined by the `ECG_LETTERS` point arrays (keep these) - The trace line passes through each letter's shape points, making the ECG waveform form recognizable letter shapes - Between letters, the trace returns to baseline with short connector segments - Neon glow effect on both trace and revealed text ## Transition to Login After the text is fully revealed and the flatline extends to the right edge, the flow continues as described in `ref-transition-login.md`: 1. Name holds with glow (300ms) 2. Glow fades, flatline extends right 3. Canvas fades to black (200ms) 4. Background transitions to dark blue-gray `#1E293B` (200ms) 5. Login card materializes ## prefers-reduced-motion With reduced motion enabled: - Boot text appears instantly (no stagger) - Cursor appears immediately - ECG animation is skipped entirely — transition straight from boot to login - Or: show the final frame (name fully revealed, flatline) as a static image for 1 second, then proceed to login ## Testing Checklist - [ ] Boot text renders correctly with all lines - [ ] Blinking cursor visible at end of boot - [ ] Cursor smoothly transitions to ECG dot (no teleport) - [ ] ECG trace starts from cursor position - [ ] Heartbeats render with increasing amplitude - [ ] Name letters revealed via mask technique (not alpha fade) - [ ] Connector lines between letters - [ ] Neon glow on trace and text - [ ] Flatline extends to right edge after name - [ ] Background transitions to `#1E293B` - [ ] Scanlines and vignette present - [ ] Reduced motion: instant/static - [ ] Mobile: scales correctly --- ## Design Guidance (from /frontend-design) > Pre-baked design direction. Do NOT invoke `/frontend-design` at runtime — this section contains the output. ### Aesthetic Direction: Authentic Clinical Terminal → Medical Monitor Realism This isn't "medical-themed" design — this IS medical equipment interfaces. Two distinct phases: 1. **Boot Terminal**: Authentic 1990s clinical system boot sequence (think legacy pharmacy dispensing systems, hospital terminal logins). CRT monitor aesthetic with phosphor green, scanlines, slight text glow. 2. **ECG Monitor**: Hospital bedside cardiac monitor realism. The heartbeat isn't decorative — it's a functioning waveform that becomes letterforms through technical precision. **Visual Signature**: The cursor-to-dot morphing transition. Most animations have discrete phases; this creates continuous material transformation — the blinking terminal cursor literally becomes the ECG trace point. It's the moment where "loading system" becomes "reading vital signs." ### Boot Sequence — Type-Safe Config ```typescript type BootLineType = 'header' | 'status' | 'separator' | 'field' | 'module' | 'ready' interface BootLine { type: BootLineType text: string label?: string value?: string style?: 'bright' | 'dim' | 'cyan' } interface BootConfig { header: string lines: BootLine[] timing: { lineDelay: number cursorBlinkInterval: number holdAfterComplete: number fadeOutDuration: number } colors: { bright: string dim: string cyan: string } } ``` Component architecture: - `` — renders individual line types with appropriate styling - `` — separate component for cursor with ref exposure for position capture - Config-driven rendering replaces hardcoded HTML Example config: ```typescript const BOOT_CONFIG: BootConfig = { header: 'CLINICAL TERMINAL v3.2.1', lines: [ { type: 'status', text: 'Initialising pharmacist profile...', style: 'dim' }, { type: 'separator', text: '---', style: 'dim' }, { type: 'field', label: 'SYSTEM', value: 'NHS Norfolk & Waveney ICB', style: 'cyan' }, { type: 'field', label: 'USER', value: 'Andy Charlwood', style: 'bright' }, // ... etc ], timing: { lineDelay: 220, cursorBlinkInterval: 530, holdAfterComplete: 400, fadeOutDuration: 800 }, colors: { bright: '#00ff41', dim: '#3a6b45', cyan: '#00e5ff' } } ``` Example BootLine component: ```typescript function BootLine({ line }: { line: BootLine }) { const colors = BOOT_CONFIG.colors const color = line.style ? colors[line.style] : colors.dim if (line.type === 'field') { return (
{line.label?.padEnd(9)} {line.value}
) } if (line.type === 'module') { return (
[OK] {' '} {line.text}
) } // ... other types } ``` ### Cursor → Dot Transition — Implementation ```typescript const [cursorPosition, setCursorPosition] = useState<{x: number, y: number} | null>(null) const cursorRef = useRef(null) useEffect(() => { if (cursorRef.current) { const rect = cursorRef.current.getBoundingClientRect() setCursorPosition({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }) } }, [/* trigger when boot completes */]) // Pass to ECG: ``` Morph animation: width 8px→0px, height 16px→6px, border-radius 0→50% (300ms ease-out). Simultaneously fade in ECG dot at same position. After morph complete, begin trace movement. ### Canvas Mask Reveal — Implementation ```javascript // Pre-render text to offscreen canvas const textCanvas = document.createElement('canvas') const textCtx = textCanvas.getContext('2d') textCtx.strokeStyle = lineColor textCtx.lineWidth = 1.5 textCtx.font = `bold ${fontSize}px Arial` textLayout.forEach(item => { textCtx.strokeText(item.char, item.centerX, baselineY) }) // During animation loop: ctx.save() // Create clipping path following trace head ctx.beginPath() ctx.rect(0, 0, headSX + 20, vh) // reveal up to head position + lead ctx.clip() // Draw pre-rendered text through clip ctx.drawImage(textCanvas, -viewOff, 0) ctx.restore() // Feathered leading edge: const gradient = ctx.createLinearGradient(headSX - 30, 0, headSX, 0) gradient.addColorStop(0, 'rgba(0,255,65,0)') gradient.addColorStop(1, 'rgba(0,255,65,1)') ``` ### Connector Lines Between Letters ```javascript const connectors: {startX: number, endX: number}[] = [] for (let i = 0; i < textLayout.length - 1; i++) { const curr = textLayout[i] const next = textLayout[i + 1] const endInset = CONNECTOR_INSETS[curr.char] || 0 const startInset = CONNECTOR_INSETS[next.char] || 0 connectors.push({ startX: curr.endX - endInset, endX: next.startX + startInset }) } // During draw: connectors.forEach(conn => { if (headWX > conn.startX) { const connectorEndX = Math.min(conn.endX, headWX) ctx.beginPath() ctx.moveTo(conn.startX - viewOff, baselineY) ctx.lineTo(connectorEndX - viewOff, baselineY) ctx.stroke() } }) ``` ### Visual Enhancement Details **CRT Scanlines** (boot phase): ```css .boot-scanlines { position: absolute; inset: 0; background: repeating-linear-gradient( 0deg, rgba(0, 0, 0, 0.15) 0px, transparent 1px, transparent 2px, rgba(0, 0, 0, 0.15) 3px ); pointer-events: none; animation: scanline-drift 8s linear infinite; } ``` **Phosphor Glow** (terminal text): ```css .terminal-text { text-shadow: 0 0 4px currentColor, 0 0 8px currentColor, 0 0 12px rgba(0, 255, 65, 0.3); } ``` **ECG Neon Glow** (canvas — multi-layer): - Primary trace: 2px solid line - Glow layer 1: 6px @ 25% opacity + shadowBlur 14px - Glow layer 2: Inner 3px core for sharpness - Text: Same multi-layer glow approach **Background Transition** (smooth RGB interpolation): ```javascript const bgProgress = (elapsed - flatlineStartTime) / BG_TRANSITION ctx.fillStyle = `rgb( ${Math.round(0 + (30 * bgProgress))}, ${Math.round(0 + (41 * bgProgress))}, ${Math.round(0 + (59 * bgProgress))} )` // black → #1E293B ctx.fillRect(0, 0, vw, vh) ```