AnimatedBar
PURPOSE
A single WAAPI-driven progress bar for the resolve phase. Animates a fill from a before state to an after state across one or more level boundaries, fires a one-shot onComplete when the visual is done, and supports a skip path that jumps straight to the final visual.
OWNS
- The
<div>track + fill + counter DOM nodes (refsfillRef,counterRef). - A
completedRefboolean guard ensuringonCompletefires exactly once per mount/effect run. - Inline style objects:
containerStyle,labelStyle,trackStyle,counterStyle, and thefillStyle(color)factory. - The decision of where the initial
scaleXsits on first render (derived fromdelta.before+getXpForLevel).
READS FROM
props.delta: ProgressDelta—before/afterXP+level pair, optionaldisplayLabel, and optionalthresholdsCrossedflag. Imported type from../data/reward-types.props.getXpForLevel(level)— caller-supplied curve lookup; used both for initial percent and for the finalskipjump. Multi-segment fills insidefillBaralso consume it via the options bag.props.color— CSS color string for the fill. Defaults to'#4fc3f7'.props.skip— boolean; when true, bypasses the WAAPI animation entirely.
PUSHES TO
- The fill DOM node’s
style.transform(viascaleX) on the skip path. - The counter DOM node’s
textContenton the skip path. fillBar(...)from../fx/juice— delegated all real animation work (multi-segment fills, head-flare, overshoot, counter roll, threshold pauses).props.onComplete()— exactly once, gated bycompletedRef.
DOES NOT
- Read or write any Zustand store. Source of truth is the
ProgressDeltaprop, never live state. - Mutate
deltaor any prop. - Manage its own lifecycle beyond a single
useEffectkeyed on[delta, skip]. - Handle taps/clicks directly. Skip semantics are controlled by the parent flipping
skiptotrue. - Decide pacing or sequencing between multiple bars. The parent (HomeResolveController) chains bars by waiting on
onComplete. - Read level curves itself. All curve evaluation is delegated to
getXpForLevel.
Signals
onComplete: () => void— fired exactly once per effect run when either the WAAPI animation resolves or the skip jump finishes. Guarded bycompletedRefso it cannot double-fire.- Threshold callback passed into
fillBaris a no-op (void id) whendelta.thresholdsCrossedis truthy —fillBarowns the pause-and-pop behavior; this component only opts in.
Entry points
export interface AnimatedBarProps— public prop shape.export function AnimatedBar(props)— the component itself; no default export.
Pattern notes
- Imperative ref + WAAPI, not React state. All animation lives on DOM nodes via
fillBar; React only renders the initial frame. This avoids per-frame re-renders during the fill. - Initial render matches
delta.beforeexactly. The first paint setsscaleX(initialPct)and counter text tobefore.xp / xpForLevel(before.level)so there is no flash before the effect runs. - Effect deps disable the exhaustive-deps lint intentionally on the
[delta, skip]dep set —getXpForLevelandonCompleteare treated as stable for the bar’s lifetime; the parent is expected to keep references stable across renders of the same bar. - One-shot completion guard.
completedRefsurvives StrictMode double-invocation and any race between the skip path and a resolvingfillBarpromise. - Defensive percent clamp.
Math.min(1, pct)on both initial and skip-final transforms toleratesafter.xp > xpForLevel(after.level)without overflowing the track visually. - No-op early return when
fillRef.currentis null still callsfinish()so a parent waiting ononCompletecannot stall if the node failed to mount.
EXTRACT-CANDIDATE
- The pattern “compute fill percent as
xp / xpForLevel(level)with a divide-by-zero guard andMath.min(1, ...)clamp” appears twice in this file and is the same arithmeticfillBarperforms internally. If a third caller appears, lift to a sharedxpToPct(xp, level, getXpForLevel)helper next toProgressDeltaindata/reward-typesor infx/juice. - The “fire
onCompleteexactly once across skip + async paths” idiom (ref-guardedfinish) is likely to recur in any resolve-phase animation component. Candidate for a tinyuseOnce(cb)hook inmetagame/hooks/if a second user shows up.