import type { Cue } from "./data"; /** * How one node looks at one instant. The animation functions below produce * these, and Transect.tsx only draws them, so adding a property here (a colour, * a stroke, a glow) is the only change the drawing needs to follow. */ export type NodeLook = { radius: number; opacity: number; }; /** * A node that is not being sung. Radius is in map units (the page's pixels), * so nodes grow with the camera's zoom. (The page itself uses 3 at 0.85.) */ export const RESTING: NodeLook = { radius: 2.5, opacity: 0.9 }; /** A node while its line is sung: the page's selected mark. */ export const LIT: NodeLook = { radius: 7, opacity: 1 }; const clamp01 = (v: number) => Math.min(1, Math.max(0, v)); /** Slightly slower lighting up and switching off. Relatively slower decay. */ const LIGHT_UP_S = 0.12; const SWITCH_OFF_S = 0.5; /** Blend two looks; p = 0 is `from`, p = 1 is `to`. */ export const mixLooks = (from: NodeLook, to: NodeLook, p: number): NodeLook => ({ radius: from.radius + (to.radius - from.radius) * p, opacity: from.opacity + (to.opacity - from.opacity) * p, }); /** * A node being switched on. * @param elapsedS seconds since its line started (0 at the lyric timestamp) * @param from the look it starts from */ export const lightUp = (elapsedS: number, from: NodeLook = RESTING): NodeLook => mixLooks(from, LIT, clamp01(elapsedS / LIGHT_UP_S)); /** * A node being switched off. * @param elapsedS seconds since its line ended (0 at the next lyric timestamp) * @param from the look it had when it was switched off */ export const switchOff = (elapsedS: number, from: NodeLook = LIT): NodeLook => mixLooks(from, RESTING, clamp01(elapsedS / SWITCH_OFF_S)); /** Longest a node holds fully lit, after lighting up, before it switches off. */ export const MAX_LIT_S = 0.9; /** * When a cue's nodes start switching off: when the line ends, or once they have * lit up and held for MAX_LIT_S, whichever comes first, so a long line does not * keep a node lit for too long. */ export const switchOffAt = (cue: Cue): number => Math.min(cue.stopS, cue.startS + LIGHT_UP_S + MAX_LIT_S); /** * The look of a node at time `t`, given the cue that lights it (or none). * Before the line: resting. Then lighting up until switchOffAt(). After that: * switching off, starting from however far the light-up got, so a short line * never jumps. */ export const nodeLookAt = (cue: Cue | undefined, t: number): NodeLook => { if (!cue || t < cue.startS) { return RESTING; } const offS = switchOffAt(cue); if (t < offS) { return lightUp(t - cue.startS); } return switchOff(t - offS, lightUp(offS - cue.startS)); }; /** Whether a look is anything other than resting (drawn on top if so). */ export const isAwake = (look: NodeLook): boolean => look.radius !== RESTING.radius || look.opacity !== RESTING.opacity;