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:
@@ -11,6 +11,8 @@ export function useConstellationInteraction(deps: {
|
||||
resolveGraphFallback: () => string | null
|
||||
resolveRoleFallback: () => string | null
|
||||
dimensionsTrigger: number
|
||||
pauseForInteraction?: () => void
|
||||
resumeAfterInteraction?: () => void
|
||||
}) {
|
||||
const [pinnedNodeId, setPinnedNodeId] = useState<string | null>(null)
|
||||
const pinnedNodeIdRef = useRef<string | null>(null)
|
||||
@@ -32,13 +34,15 @@ export function useConstellationInteraction(deps: {
|
||||
pinnedNodeIdRef.current = null
|
||||
deps.highlightGraphRef.current?.(null)
|
||||
deps.callbacksRef.current.onNodeHover?.(null)
|
||||
deps.resumeAfterInteraction?.()
|
||||
}
|
||||
})
|
||||
|
||||
nodeSelection.on('mouseenter.interaction', function(_event: MouseEvent, d: SimNode) {
|
||||
if (supportsCoarsePointer) return
|
||||
deps.pauseForInteraction?.()
|
||||
deps.highlightGraphRef.current?.(d.id)
|
||||
if (d.type === 'role') {
|
||||
if (d.type !== 'skill') {
|
||||
deps.callbacksRef.current.onNodeHover?.(d.id)
|
||||
}
|
||||
})
|
||||
@@ -47,6 +51,7 @@ export function useConstellationInteraction(deps: {
|
||||
if (supportsCoarsePointer) return
|
||||
deps.highlightGraphRef.current?.(deps.resolveGraphFallback())
|
||||
deps.callbacksRef.current.onNodeHover?.(deps.resolveRoleFallback())
|
||||
deps.resumeAfterInteraction?.()
|
||||
})
|
||||
|
||||
nodeSelection.on('click.interaction', function(_event: MouseEvent, d: SimNode) {
|
||||
@@ -56,15 +61,17 @@ export function useConstellationInteraction(deps: {
|
||||
pinnedNodeIdRef.current = null
|
||||
deps.highlightGraphRef.current?.(null)
|
||||
deps.callbacksRef.current.onNodeHover?.(null)
|
||||
deps.resumeAfterInteraction?.()
|
||||
} else {
|
||||
setPinnedNodeId(d.id)
|
||||
pinnedNodeIdRef.current = d.id
|
||||
deps.pauseForInteraction?.()
|
||||
deps.highlightGraphRef.current?.(d.id)
|
||||
deps.callbacksRef.current.onNodeHover?.(d.type === 'role' ? d.id : deps.resolveRoleFallback())
|
||||
deps.callbacksRef.current.onNodeHover?.(d.type !== 'skill' ? d.id : deps.resolveRoleFallback())
|
||||
}
|
||||
}
|
||||
|
||||
if (d.type === 'role') {
|
||||
if (d.type !== 'skill') {
|
||||
deps.callbacksRef.current.onRoleClick(d.id)
|
||||
} else {
|
||||
deps.callbacksRef.current.onSkillClick(d.id)
|
||||
@@ -72,7 +79,6 @@ export function useConstellationInteraction(deps: {
|
||||
})
|
||||
}, [deps])
|
||||
|
||||
// Re-bind events whenever selections change (triggered by simulation re-creation)
|
||||
useEffect(() => {
|
||||
bindEvents()
|
||||
}, [deps.dimensionsTrigger, bindEvents])
|
||||
|
||||
Reference in New Issue
Block a user