81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
|
|
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;
|