const LEG_DURATION = 6000; const HOLD_DURATION = 3800; const MOVE_DURATION = 1400; const LOOP_DURATION = LEG_DURATION * 6; const phaseTimes = Object.freeze({ queued: 0, moving: 4500, review: 5200 }); const clamp = (value) => Math.min(1, Math.max(0, value)); const ease = (value) => value * value * (3 - 2 * value); const pairs = Object.freeze( [ [0, 1], [1, 2], [0, 1], [1, 2], [0, 1], [1, 2], ].map(Object.freeze), ); const swap = (slots, pair) => slots.map((slot) => slot === pair[0] ? pair[1] : slot === pair[1] ? pair[0] : slot, ); const arrangements = Object.freeze( pairs.reduce( (poses, pair) => [...poses, Object.freeze(swap(poses.at(-1), pair))], [Object.freeze([0, 1, 2])], ), ); export function createSplashState(search = "", reduced = false) { const params = new URLSearchParams(search); const capture = params.get("capture") === "1"; const requested = params.get("phase"); const phase = Object.hasOwn(phaseTimes, requested) ? requested : "review"; return { elapsed: reduced ? phaseTimes.review : capture ? phaseTimes[phase] : 0, capture, locked: capture || reduced, }; } export function advanceSplash(state, milliseconds, active) { if ( state.locked || !active || !Number.isFinite(milliseconds) || milliseconds < 0 ) return { ...state }; return { ...state, elapsed: (state.elapsed + milliseconds) % LOOP_DURATION }; } export function setSplashReduced(state, reduced) { return { ...state, elapsed: reduced ? phaseTimes.review : state.elapsed, locked: state.capture || reduced, }; } export function describeSplash({ elapsed }) { const time = elapsed % LOOP_DURATION; const leg = Math.floor(time / LEG_DURATION); const local = time % LEG_DURATION; const progress = ease(clamp((local - HOLD_DURATION) / MOVE_DURATION)); const from = arrangements[leg]; const to = arrangements[leg + 1]; const phase = progress > 0 && progress < 1 ? "moving" : time < HOLD_DURATION ? "queued" : "review"; return { phase, cards: from.map((slot, index) => ({ from: slot, to: to[index], progress: slot === to[index] ? 0 : progress, slot: progress === 1 ? to[index] : slot, })), }; } export function splashPosition(progress, from, to, compact, direction) { // The passing offset returns to exactly zero at either anchor, including the loop seam. const passing = progress === 0 || progress === 1 ? 0 : Math.sin(Math.PI * progress) * 6; return { x: from.x + (to.x - from.x) * progress + (compact ? passing * direction : 0), y: from.y + (to.y - from.y) * progress - (compact ? 0 : passing * direction), }; } export function splashVisible(height, intersectionHeight, viewportHeight) { return ( height > 0 && intersectionHeight >= Math.min(height, viewportHeight) / 2 ); }