/** * Global scroll progress, kept OUTSIDE React state so the R3F render loop can * read it every frame (via getScroll()) without triggering re-renders. Lenis * writes to it; the 3D scene and any rAF consumer read from it. */ export interface ScrollState { /** 0–1 progress across the entire page. */ progress: number /** Raw scroll offset in px. */ scrollY: number /** Total scrollable height in px. */ limit: number /** Viewport height in px. */ viewport: number } const state: ScrollState = { progress: 0, scrollY: 0, limit: 1, viewport: typeof window !== 'undefined' ? window.innerHeight : 1, } export function setScroll(scrollY: number, limit: number) { state.scrollY = scrollY state.limit = Math.max(1, limit) state.progress = Math.min(1, Math.max(0, scrollY / state.limit)) state.viewport = window.innerHeight } export function getScroll(): ScrollState { return state } /** * Progress (0–1) of a single section, given its index and the total number of * full-viewport sections. Lets the 3D scene know "how far through the hero / the * descent are we" independent of total page height. */ export function sectionProgress( sectionIndex: number, totalSections: number, ): number { const per = 1 / totalSections const start = sectionIndex * per const local = (state.progress - start) / per return Math.min(1, Math.max(0, local)) }