import { ACTIVATION_LINE_VH } from './section-controller'; /** * CARD GAPS: the open road between chapter cards. A chapter opens with its * title standing over the road, then one reading panel, then (route.css) a * stretch of open road before the next title: the section's bottom padding * plus the next section's top margin. Section-local progress runs 0 with a * chapter's heading on the activation line to 1 with the next heading there, * so the gap after a chapter's panel always sits at the END of its progress, * and it is the one stretch where a scene payoff can be watched rather than * read over. The payoffs (the district relight, the last yard lift, the relay * beams) key off CARD_GAP instead of their own tuned numbers, so this window * is the single source of truth. Pure and tiny: shared by the DOM * (maintenance steps) and the lazy scene chunk. */ /** Open road below a panel, in vh: the section's bottom padding plus the next section's top margin (route.css). */ export const CARD_GAP_OPEN_ROAD_VH = 20 + 48; /** The next chapter's title stack: its heading stands this far above its panel (about 197 px; vh at the 900 px design height). */ export const CARD_TITLE_VH = 22; /** The viewport band a payoff must have clear of panels, as fractions of the viewport height. */ export const CARD_CLEAR_BAND = { top: 0.3, bottom: 0.85 } as const; export interface GapWindow { /** The previous panel has cleared the band (section-local progress). */ from: number; /** The open road is centred in the viewport. */ peak: number; /** The next panel reaches the band. */ to: number; } /** * Where the open road after a chapter's panel fills the clear band, for a * chapter whose heading-to-heading span is `spanVh` viewport heights. Not * called per frame: payoffs use the static CARD_GAP below, which this * validates in tests. */ export function gapWindow(spanVh: number): GapWindow { const line = ACTIVATION_LINE_VH * 100; // Panel edges relative to the next heading, in vh: the previous panel's bottom and the next panel's top. const previousBottom = -CARD_GAP_OPEN_ROAD_VH; const nextTop = CARD_TITLE_VH; return { from: 1 + (previousBottom + line - CARD_CLEAR_BAND.top * 100) / spanVh, peak: 1 + ((previousBottom + nextTop) / 2 + line - 50) / spanVh, to: 1 + (nextTop + line - CARD_CLEAR_BAND.bottom * 100) / spanVh, }; } /** Chapter spans (vh) the static window must hold for: the shortest chapter on a tall screen to the tallest on a short one. */ export const CARD_GAP_SPANS_VH = { min: 140, max: 330 } as const; /** * The gap window every payoff keys off, in section-local progress. Valid for * all spans in CARD_GAP_SPANS_VH (tests/unit/card-gaps.test.ts proves it): * payoffs begin once the previous panel has cleared the band and complete * with the road centred, before the chapter's set dressing fades for the * boundary (`to`). */ export const CARD_GAP: GapWindow = Object.freeze({ from: 0.9, peak: 0.94, to: 0.96 });