PURPOSE
Shared intro helper used by every reward cinematic. Drives a two-pass reveal: Pass 1 during the announce state pops in a blank ? placeholder per card, staggered left-to-right; Pass 2 hands control to each cinematic’s unique reveal animation during the cinematic-specific states. This module owns the placeholder look (rounded-rect body, off-white stroke, centered question mark) and the stagger math; cinematics opt in by calling its helpers from their own draw hooks.
OWNS
- Per-card pop-in timing constants:
CARD_APPEAR_START,CARD_APPEAR_STAGGER,CARD_APPEAR_DURATION,HOLD_BEAT. - Exported lower-bound on
ANNOUNCE_TIME:INTRO_MIN_TIME. - Pop-in easing curve
easeOutBack(slight overshoot then settle). - Placeholder visual: dark rounded-rect with vertical gradient
#232836 → #0d1018,rgba(255,255,255,0.22)stroke at width 2, centered?glyph in Space Grotesk at 0.58 alpha. - Pop-in animation: scale
0.55 → 1.0througheaseOutBack, alpha0 → 1linear, gated by per-card progress. - Per-card progress calculation
placeholderProgress(cardIndex, cinCtx). - Active-state predicate
isIntroActive(cinCtx).
READS FROM
isSkipRewardAnimationsfrom../../core/config/_accessibility— short-circuits the intro entirely when accessibility skip is on (progress returns 1, active returns false, draw early-exits).CinematicContexttype from./registry— usesstateandstateTimerto drive the timeline.- The caller-supplied
CanvasRenderingContext2D, card rect (x, y, w, h),alpha, andscalepassed intodrawPlaceholderCard.
PUSHES TO
- The
CanvasRenderingContext2Dpassed in by the caller. Mutates only inside actx2d.save()/ctx2d.restore()pair — body fill, stroke, glyph fill, transform. - Returns numeric progress (
placeholderProgress) and a boolean (isIntroActive) for the caller’s render gating.
DOES NOT
- Does not subscribe to the reward state machine itself — it is invoked by per-family cinematic modules from their
drawCardOverrideand other hooks. - Does not own
ANNOUNCE_TIME; that constant lives inhud.tsand must be at leastINTRO_MIN_TIMEfor all three pop-ins plus the held beat to complete before the reveal pass starts. - Does not catch its own errors — caller propagation matches the registry contract.
- Does not mutate
CinematicContext. - Does not apply rarity tint, sound, particles, or text labels — neutral by design; the reveal pass is responsible for those.
- Does not render anything when
isSkipRewardAnimations()is true or when a card has not yet reached its pop-in start time.
Signals
- Returns
0fromplaceholderProgressuntil the card’s pop-in window opens. - Returns
1fromplaceholderProgressonce the pop-in finishes, or whenever the state is notannounce, so cinematics can unconditionally multiply by it during their own reveal. isIntroActivereturnstrueonly whilecinCtx.state === 'announce'and animations are not being skipped.drawPlaceholderCardshort-circuits without painting when progress is<= 0or when reward animations are skipped.
Entry points
INTRO_MIN_TIME— exported constant, consumed byhud.tsto validateANNOUNCE_TIME.placeholderProgress(cardIndex, cinCtx)— exported, returns the eased-input progress0..1for a given card.isIntroActive(cinCtx)— exported, used by cinematics to gate their own custom rendering duringannounce.drawPlaceholderCard(ctx2d, cardIndex, x, y, w, h, alpha, scale, cinCtx)— exported, called from each cinematic’sdrawCardOverridewhile the intro is active.
Pattern notes
- Pop-in start time for card
iisCARD_APPEAR_START + i * CARD_APPEAR_STAGGER, measured againstcinCtx.stateTimerwhile inannounce. - The drawn pop scale is
0.55 + easeOutBack(p) * 0.45, applied around the card center so the overshoot stays centered on the slot. - Alpha is the product of caller-supplied
alphaand per-card progressp, giving a linear fade-in alongside the eased scale. - Corner radius is
min(14, min(w, h) * 0.08)so the placeholder matches the card silhouette at any size. - Question-mark font size is
floor(h * 0.42), weight 700, family stack'Space Grotesk', system-ui, sans-serif. - The module is family-agnostic and stateless — all state flows through
CinematicContext, so any cinematic can compose the intro without coordination. ANNOUNCE_TIMEinhud.tsmust be>= INTRO_MIN_TIME; if shorter, the last placeholder is still mid-flight when the reveal pass begins. There is no runtime warning —INTRO_MIN_TIMEis the contract.- Cinematics that want to suppress the default card paint return
truefromdrawCardOverrideand calldrawPlaceholderCardinstead whileisIntroActiveis true.