feat: phase 3+4 timeline animation + education entities

- Add education entities (A-Levels, MPharm) to constellation data
- Add 'education' node type with dashed border styling
- Create useTimelineAnimation hook with rAF scheduler + state machine
  (IDLE → PLAYING → PAUSED → HOLDING → RESETTING → loop)
- Chronological reveal: entities oldest-first with skill stagger,
  link draw-on, reinforcement pulse for already-visible skills
- Year indicator overlay (monospace, top-left)
- Multiplicative opacity: animation visibility × highlight emphasis
- Highlight system respects visibleNodeIdsRef (unrevealed stay hidden)
- Interaction pause/resume wired to animation hook
- Play/pause button (bottom-right, larger touch target on mobile)
- prefers-reduced-motion: shows final state immediately, no animation
- Remove Phase 2 entry animation (replaced by timeline animation)
This commit is contained in:
2026-02-16 14:31:11 +00:00
parent 7d7628c8a7
commit 8b674ffe14
10 changed files with 675 additions and 171 deletions
+4 -4
View File
@@ -447,14 +447,14 @@ export function buildConstellationData(): {
constellationNodes: ConstellationNode[]
constellationLinks: ConstellationLink[]
} {
const roleSkillMappings: RoleSkillMapping[] = timelineCareerEntities.map((entity) => ({
const roleSkillMappings: RoleSkillMapping[] = timelineEntities.map((entity) => ({
roleId: entity.id,
skillIds: entity.skills,
}))
const roleNodes: ConstellationNode[] = timelineCareerEntities.map((entity) => ({
const roleNodes: ConstellationNode[] = timelineEntities.map((entity) => ({
id: entity.id,
type: 'role',
type: entity.kind === 'education' ? 'education' as const : 'role' as const,
label: entity.title,
shortLabel: entity.graphLabel,
organization: entity.organization,
@@ -471,7 +471,7 @@ export function buildConstellationData(): {
domain: skillDomainByCategory[skill.category],
}))
const constellationLinks: ConstellationLink[] = timelineCareerEntities.flatMap((entity) =>
const constellationLinks: ConstellationLink[] = timelineEntities.flatMap((entity) =>
entity.skills.map((skillId) => ({
source: entity.id,
target: skillId,