Completed login screen transition, and started the spec work on design file info

This commit is contained in:
2026-02-11 22:15:29 +00:00
parent 1a1f1f1938
commit 192d629125
22 changed files with 2357 additions and 390 deletions
+359
View File
@@ -0,0 +1,359 @@
# 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:
- `<BootLine>` — renders individual line types with appropriate styling
- `<BootCursor>` — 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 (
<div className="font-mono text-sm leading-relaxed">
<span style={{ color: colors.cyan }}>{line.label?.padEnd(9)}</span>
<span style={{ color }}>{line.value}</span>
</div>
)
}
if (line.type === 'module') {
return (
<div className="font-mono text-sm leading-relaxed">
<span className="font-bold" style={{ color: colors.bright }}>[OK]</span>
{' '}
<span style={{ color: colors.dim }}>{line.text}</span>
</div>
)
}
// ... other types
}
```
### Cursor → Dot Transition — Implementation
```typescript
const [cursorPosition, setCursorPosition] = useState<{x: number, y: number} | null>(null)
const cursorRef = useRef<HTMLDivElement>(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:
<ECGAnimation startPosition={cursorPosition} onComplete={...} />
```
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)
```