From 3b75ec9b3df3831b7fe5702c68cf213f91633733 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Mon, 6 Jul 2026 22:38:33 -0400 Subject: [PATCH] Animate player display inactive transitions and death state Player display now tracks displayParticipants separately from raw encounter participants so active->inactive can animate out before removal, and inactive-> active can animate in. This preserves 1-list order, keeps DM display behavior unchanged, and avoids fading dying/stable participants. PlayerParticipantCard handles transitions: - active->inactive: scale/slide/fade out, then parent removes from display list - inactive->active: scale/slide/fade in - alive->dead: keep visible with dim/desaturate/red-rim death cue + skull pulse - dying/stable: full visible, no fade/removal DisplayView inactive characterization updated to wait for exit animation before asserting removal. TODO item removed. --- TODO.md | 12 -- src/App.js | 105 ++++++++++++++++-- .../DisplayView.characterization.test.js | 5 +- 3 files changed, 97 insertions(+), 25 deletions(-) diff --git a/TODO.md b/TODO.md index ea91014..0ce3028 100644 --- a/TODO.md +++ b/TODO.md @@ -11,18 +11,6 @@ not sure good way to do this -### FEAT: player display fade transitions for inactive state -- Inactive is DM-triggered via Mark Inactive. -- Player display should fade inactive participant out, then remove from display list. -- Reactivating should fade participant in. -- DM display keeps inactive participant visible. -- Dead state does not imply inactive/disabled. -- Dying/stable must not fade out or leave layout holes. -- Dead can keep skull/Dead label; create some good visual cues, no removal - but a cool transition to death would be nice. - - - - ### TEST GAP: current branch changes need focused coverage - Storage `where()` contract: firebase + server adapters should honor `[where('encounterPath','==',x), orderBy('ts','desc'), limit(n)]`. diff --git a/src/App.js b/src/App.js index f871e2d..a58a731 100644 --- a/src/App.js +++ b/src/App.js @@ -2554,6 +2554,61 @@ function AdminView({ userId }) { // DISPLAY VIEW COMPONENT (Player View) // ============================================================================ +// Player participant card: animate state transitions. +// mount/activate : animate in (opacity+scale+slide) +// deactivate : animate out then signal exit +// alive→dead : transition to death cue (dim+desaturate+skull pulse+red rim) +// conscious/dying/stable: full visible, no fade +function PlayerParticipantCard({ id, isActive, status, onExit, className, children, divRef }) { + // phase: 'hidden' (opacity-0, start state) -> 'visible' (opacity-100) + // disable: 'visible' -> 'exiting' (opacity-0, transition plays) -> onExit + const [phase, setPhase] = useState('hidden'); + const isDead = status === 'dead'; + + useEffect(() => { + if (!isActive) { + // ensure current 'visible' frame painted, THEN flip to exiting so + // browser sees opacity change and plays the fade-out. + const raf = requestAnimationFrame(() => { + setPhase('exiting'); + }); + const t = setTimeout(() => onExit(id), 1000); + return () => { cancelAnimationFrame(raf); clearTimeout(t); }; + } + // enable/reactivate: paint hidden frame first, THEN flip to visible + // so browser sees opacity change and plays the transition. + setPhase('hidden'); + const raf = requestAnimationFrame(() => { + requestAnimationFrame(() => setPhase('visible')); + }); + return () => cancelAnimationFrame(raf); + }, [isActive, id, onExit]); + + // animate: opacity + scale + slide. Visible motion, not just fade. + const fadeClass = phase === 'visible' + ? 'opacity-100 scale-100 translate-y-0' + : 'opacity-0 scale-75 translate-y-4'; + + // death transition: fires once when alive→dead. dim + desaturate + red rim. + const deathClass = isDead + ? 'brightness-50 saturate-0 border-2 border-red-900/70 shadow-red-900/50 shadow-2xl' + : ''; + + return ( +