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.0 through easeOutBack, alpha 0 → 1 linear, gated by per-card progress.
  • Per-card progress calculation placeholderProgress(cardIndex, cinCtx).
  • Active-state predicate isIntroActive(cinCtx).

READS FROM

  • isSkipRewardAnimations from ../../core/config/_accessibility — short-circuits the intro entirely when accessibility skip is on (progress returns 1, active returns false, draw early-exits).
  • CinematicContext type from ./registry — uses state and stateTimer to drive the timeline.
  • The caller-supplied CanvasRenderingContext2D, card rect (x, y, w, h), alpha, and scale passed into drawPlaceholderCard.

PUSHES TO

  • The CanvasRenderingContext2D passed in by the caller. Mutates only inside a ctx2d.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 drawCardOverride and other hooks.
  • Does not own ANNOUNCE_TIME; that constant lives in hud.ts and must be at least INTRO_MIN_TIME for 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 0 from placeholderProgress until the card’s pop-in window opens.
  • Returns 1 from placeholderProgress once the pop-in finishes, or whenever the state is not announce, so cinematics can unconditionally multiply by it during their own reveal.
  • isIntroActive returns true only while cinCtx.state === 'announce' and animations are not being skipped.
  • drawPlaceholderCard short-circuits without painting when progress is <= 0 or when reward animations are skipped.

Entry points

  • INTRO_MIN_TIME — exported constant, consumed by hud.ts to validate ANNOUNCE_TIME.
  • placeholderProgress(cardIndex, cinCtx) — exported, returns the eased-input progress 0..1 for a given card.
  • isIntroActive(cinCtx) — exported, used by cinematics to gate their own custom rendering during announce.
  • drawPlaceholderCard(ctx2d, cardIndex, x, y, w, h, alpha, scale, cinCtx) — exported, called from each cinematic’s drawCardOverride while the intro is active.

Pattern notes

  • Pop-in start time for card i is CARD_APPEAR_START + i * CARD_APPEAR_STAGGER, measured against cinCtx.stateTimer while in announce.
  • 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 alpha and per-card progress p, 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_TIME in hud.ts must 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_TIME is the contract.
  • Cinematics that want to suppress the default card paint return true from drawCardOverride and call drawPlaceholderCard instead while isIntroActive is true.