@anvpx/motion-scenes 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +39 -2
- package/dist/catalog.d.ts +17 -0
- package/dist/catalog.js +51 -0
- package/dist/custom.d.ts +86 -0
- package/dist/custom.js +170 -0
- package/dist/define-scene.d.ts +31 -0
- package/dist/define-scene.js +128 -0
- package/dist/html.d.ts +5 -0
- package/dist/html.js +14 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/position.d.ts +21 -0
- package/dist/position.js +16 -0
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +68 -0
- package/dist/runtime/easing.d.ts +14 -0
- package/dist/runtime/easing.js +45 -0
- package/dist/runtime/mount.d.ts +50 -0
- package/dist/runtime/mount.js +153 -0
- package/dist/runtime/timeline.d.ts +147 -0
- package/dist/runtime/timeline.js +199 -0
- package/dist/runtime-files.d.ts +2 -0
- package/dist/runtime-files.js +15 -0
- package/dist/scene-contract.d.ts +7 -0
- package/dist/scene-contract.js +104 -0
- package/dist/scene3d/post.d.ts +12 -0
- package/dist/scene3d/post.js +16 -0
- package/dist/scene3d/reflector.d.ts +9 -0
- package/dist/scene3d/reflector.js +17 -0
- package/dist/scene3d/textures.d.ts +9 -0
- package/dist/scene3d/textures.js +11 -0
- package/dist/scene3d/volumetric.d.ts +9 -0
- package/dist/scene3d/volumetric.js +16 -0
- package/dist/scenes/bug/index.d.ts +1 -0
- package/dist/scenes/bug/index.js +45 -0
- package/dist/scenes/bug/scene.d.ts +2 -0
- package/dist/scenes/bug/scene.js +6 -0
- package/dist/scenes/bug/style.css +30 -0
- package/dist/scenes/bullets/index.d.ts +1 -0
- package/dist/scenes/bullets/index.js +128 -0
- package/dist/scenes/bullets/scene.d.ts +2 -0
- package/dist/scenes/bullets/scene.js +6 -0
- package/dist/scenes/bullets/style.css +91 -0
- package/dist/scenes/callout/index.d.ts +9 -0
- package/dist/scenes/callout/index.js +95 -0
- package/dist/scenes/callout/scene.d.ts +2 -0
- package/dist/scenes/callout/scene.js +43 -0
- package/dist/scenes/callout/style.css +97 -0
- package/dist/scenes/caption/index.d.ts +1 -0
- package/dist/scenes/caption/index.js +72 -0
- package/dist/scenes/caption/scene.d.ts +11 -0
- package/dist/scenes/caption/scene.js +33 -0
- package/dist/scenes/caption/style.css +33 -0
- package/dist/scenes/chapter/index.d.ts +1 -0
- package/dist/scenes/chapter/index.js +29 -0
- package/dist/scenes/chapter/scene.d.ts +2 -0
- package/dist/scenes/chapter/scene.js +6 -0
- package/dist/scenes/chapter/style.css +25 -0
- package/dist/scenes/chart/index.d.ts +1 -0
- package/dist/scenes/chart/index.js +24 -0
- package/dist/scenes/chart/render.d.ts +4 -0
- package/dist/scenes/chart/render.js +257 -0
- package/dist/scenes/chart/scale.d.ts +11 -0
- package/dist/scenes/chart/scale.js +53 -0
- package/dist/scenes/chart/scene.d.ts +2 -0
- package/dist/scenes/chart/scene.js +73 -0
- package/dist/scenes/chart/schema.d.ts +36 -0
- package/dist/scenes/chart/schema.js +109 -0
- package/dist/scenes/chart/style.css +425 -0
- package/dist/scenes/code/index.d.ts +1 -0
- package/dist/scenes/code/index.js +109 -0
- package/dist/scenes/code/scene.d.ts +2 -0
- package/dist/scenes/code/scene.js +108 -0
- package/dist/scenes/code/style.css +155 -0
- package/dist/scenes/code/tokenize.d.ts +8 -0
- package/dist/scenes/code/tokenize.js +145 -0
- package/dist/scenes/compare/index.d.ts +1 -0
- package/dist/scenes/compare/index.js +128 -0
- package/dist/scenes/compare/scene.d.ts +2 -0
- package/dist/scenes/compare/scene.js +58 -0
- package/dist/scenes/compare/style.css +187 -0
- package/dist/scenes/countdown/index.d.ts +1 -0
- package/dist/scenes/countdown/index.js +46 -0
- package/dist/scenes/countdown/scene.d.ts +2 -0
- package/dist/scenes/countdown/scene.js +21 -0
- package/dist/scenes/countdown/style.css +32 -0
- package/dist/scenes/flow/index.d.ts +1 -0
- package/dist/scenes/flow/index.js +66 -0
- package/dist/scenes/flow/inspect.d.ts +26 -0
- package/dist/scenes/flow/inspect.js +25 -0
- package/dist/scenes/flow/layout.d.ts +83 -0
- package/dist/scenes/flow/layout.js +222 -0
- package/dist/scenes/flow/measure.d.ts +15 -0
- package/dist/scenes/flow/measure.js +33 -0
- package/dist/scenes/flow/plan.d.ts +42 -0
- package/dist/scenes/flow/plan.js +117 -0
- package/dist/scenes/flow/scene.d.ts +2 -0
- package/dist/scenes/flow/scene.js +6 -0
- package/dist/scenes/flow/schema.d.ts +49 -0
- package/dist/scenes/flow/schema.js +60 -0
- package/dist/scenes/flow/style.css +54 -0
- package/dist/scenes/hero/index.d.ts +3 -0
- package/dist/scenes/hero/index.js +77 -0
- package/dist/scenes/hero/scene.d.ts +2 -0
- package/dist/scenes/hero/scene.js +6 -0
- package/dist/scenes/hero/style.css +143 -0
- package/dist/scenes/icon-grid/index.d.ts +1 -0
- package/dist/scenes/icon-grid/index.js +69 -0
- package/dist/scenes/icon-grid/scene.d.ts +2 -0
- package/dist/scenes/icon-grid/scene.js +6 -0
- package/dist/scenes/icon-grid/style.css +74 -0
- package/dist/scenes/kinetic/index.d.ts +1 -0
- package/dist/scenes/kinetic/index.js +62 -0
- package/dist/scenes/kinetic/scene.d.ts +2 -0
- package/dist/scenes/kinetic/scene.js +98 -0
- package/dist/scenes/kinetic/style.css +94 -0
- package/dist/scenes/kpi-grid/index.d.ts +1 -0
- package/dist/scenes/kpi-grid/index.js +161 -0
- package/dist/scenes/kpi-grid/scene.d.ts +2 -0
- package/dist/scenes/kpi-grid/scene.js +6 -0
- package/dist/scenes/kpi-grid/style.css +141 -0
- package/dist/scenes/lower-third/index.d.ts +1 -0
- package/dist/scenes/lower-third/index.js +33 -0
- package/dist/scenes/lower-third/scene.d.ts +2 -0
- package/dist/scenes/lower-third/scene.js +6 -0
- package/dist/scenes/lower-third/style.css +45 -0
- package/dist/scenes/media-frame/index.d.ts +1 -0
- package/dist/scenes/media-frame/index.js +99 -0
- package/dist/scenes/media-frame/scene.d.ts +2 -0
- package/dist/scenes/media-frame/scene.js +85 -0
- package/dist/scenes/media-frame/style.css +275 -0
- package/dist/scenes/outro/index.d.ts +1 -0
- package/dist/scenes/outro/index.js +54 -0
- package/dist/scenes/outro/scene.d.ts +2 -0
- package/dist/scenes/outro/scene.js +6 -0
- package/dist/scenes/outro/style.css +92 -0
- package/dist/scenes/poster/index.d.ts +3 -0
- package/dist/scenes/poster/index.js +62 -0
- package/dist/scenes/poster/scene.d.ts +2 -0
- package/dist/scenes/poster/scene.js +6 -0
- package/dist/scenes/poster/style.css +102 -0
- package/dist/scenes/progress/index.d.ts +1 -0
- package/dist/scenes/progress/index.js +40 -0
- package/dist/scenes/progress/scene.d.ts +2 -0
- package/dist/scenes/progress/scene.js +17 -0
- package/dist/scenes/progress/style.css +31 -0
- package/dist/scenes/quote/index.d.ts +3 -0
- package/dist/scenes/quote/index.js +69 -0
- package/dist/scenes/quote/scene.d.ts +2 -0
- package/dist/scenes/quote/scene.js +6 -0
- package/dist/scenes/quote/style.css +86 -0
- package/dist/scenes/scene3d/index.d.ts +1 -0
- package/dist/scenes/scene3d/index.js +58 -0
- package/dist/scenes/scene3d/scene.d.ts +2 -0
- package/dist/scenes/scene3d/scene.js +413 -0
- package/dist/scenes/scene3d/style.css +43 -0
- package/dist/scenes/stat/index.d.ts +4 -0
- package/dist/scenes/stat/index.js +72 -0
- package/dist/scenes/stat/scene.d.ts +2 -0
- package/dist/scenes/stat/scene.js +6 -0
- package/dist/scenes/stat/style.css +95 -0
- package/dist/scenes/steps/index.d.ts +1 -0
- package/dist/scenes/steps/index.js +147 -0
- package/dist/scenes/steps/scene.d.ts +2 -0
- package/dist/scenes/steps/scene.js +6 -0
- package/dist/scenes/steps/style.css +190 -0
- package/dist/scenes/sticker/index.d.ts +1 -0
- package/dist/scenes/sticker/index.js +48 -0
- package/dist/scenes/sticker/scene.d.ts +2 -0
- package/dist/scenes/sticker/scene.js +57 -0
- package/dist/scenes/sticker/style.css +88 -0
- package/dist/scenes/table/index.d.ts +1 -0
- package/dist/scenes/table/index.js +131 -0
- package/dist/scenes/table/scene.d.ts +2 -0
- package/dist/scenes/table/scene.js +6 -0
- package/dist/scenes/table/style.css +102 -0
- package/dist/scenes/timeline/index.d.ts +1 -0
- package/dist/scenes/timeline/index.js +68 -0
- package/dist/scenes/timeline/scene.d.ts +2 -0
- package/dist/scenes/timeline/scene.js +6 -0
- package/dist/scenes/timeline/style.css +161 -0
- package/dist/scenes/title/index.d.ts +1 -0
- package/dist/scenes/title/index.js +52 -0
- package/dist/scenes/title/scene.d.ts +2 -0
- package/dist/scenes/title/scene.js +6 -0
- package/dist/scenes/title/style.css +80 -0
- package/dist/scenes/waveform/index.d.ts +1 -0
- package/dist/scenes/waveform/index.js +39 -0
- package/dist/scenes/waveform/scene.d.ts +2 -0
- package/dist/scenes/waveform/scene.js +35 -0
- package/dist/scenes/waveform/style.css +44 -0
- package/dist/shared/fit-text.d.ts +24 -0
- package/dist/shared/fit-text.js +82 -0
- package/dist/shared/format.d.ts +24 -0
- package/dist/shared/format.js +59 -0
- package/dist/shared/frame.d.ts +14 -0
- package/dist/shared/frame.js +21 -0
- package/dist/shared/icon-search.d.ts +9 -0
- package/dist/shared/icon-search.js +26 -0
- package/dist/shared/icon.d.ts +15 -0
- package/dist/shared/icon.js +28 -0
- package/dist/shared/index.d.ts +11 -0
- package/dist/shared/index.js +7 -0
- package/dist/shared/markup.d.ts +12 -0
- package/dist/shared/markup.js +43 -0
- package/dist/shared/measure.d.ts +7 -0
- package/dist/shared/measure.js +34 -0
- package/dist/shared/motion.d.ts +94 -0
- package/dist/shared/motion.js +117 -0
- package/dist/source-hash.d.ts +4 -0
- package/dist/source-hash.js +26 -0
- package/dist/test-fixtures.d.ts +9 -0
- package/dist/test-fixtures.js +22 -0
- package/dist/text-file.d.ts +1 -0
- package/dist/text-file.js +3 -0
- package/dist/words.d.ts +11 -0
- package/dist/words.js +93 -0
- package/package.json +36 -3
package/dist/registry.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { bugScene } from "./scenes/bug/index.js";
|
|
2
|
+
import { bulletsScene } from "./scenes/bullets/index.js";
|
|
3
|
+
import { calloutScene } from "./scenes/callout/index.js";
|
|
4
|
+
import { captionScene } from "./scenes/caption/index.js";
|
|
5
|
+
import { chapterScene } from "./scenes/chapter/index.js";
|
|
6
|
+
import { chartScene } from "./scenes/chart/index.js";
|
|
7
|
+
import { codeScene } from "./scenes/code/index.js";
|
|
8
|
+
import { compareScene } from "./scenes/compare/index.js";
|
|
9
|
+
import { countdownScene } from "./scenes/countdown/index.js";
|
|
10
|
+
import { flowScene } from "./scenes/flow/index.js";
|
|
11
|
+
import { heroScene } from "./scenes/hero/index.js";
|
|
12
|
+
import { iconGridScene } from "./scenes/icon-grid/index.js";
|
|
13
|
+
import { kineticScene } from "./scenes/kinetic/index.js";
|
|
14
|
+
import { kpiGridScene } from "./scenes/kpi-grid/index.js";
|
|
15
|
+
import { lowerThirdScene } from "./scenes/lower-third/index.js";
|
|
16
|
+
import { mediaFrameScene } from "./scenes/media-frame/index.js";
|
|
17
|
+
import { outroScene } from "./scenes/outro/index.js";
|
|
18
|
+
import { posterScene } from "./scenes/poster/index.js";
|
|
19
|
+
import { progressScene } from "./scenes/progress/index.js";
|
|
20
|
+
import { quoteScene } from "./scenes/quote/index.js";
|
|
21
|
+
import { scene3dScene } from "./scenes/scene3d/index.js";
|
|
22
|
+
import { statScene } from "./scenes/stat/index.js";
|
|
23
|
+
import { stepsScene } from "./scenes/steps/index.js";
|
|
24
|
+
import { stickerScene } from "./scenes/sticker/index.js";
|
|
25
|
+
import { tableScene } from "./scenes/table/index.js";
|
|
26
|
+
import { timelineScene } from "./scenes/timeline/index.js";
|
|
27
|
+
import { titleScene } from "./scenes/title/index.js";
|
|
28
|
+
import { waveformScene } from "./scenes/waveform/index.js";
|
|
29
|
+
/** The single list of scenes: adding a scene is one import and one entry here. */
|
|
30
|
+
const plugins = [
|
|
31
|
+
titleScene,
|
|
32
|
+
heroScene,
|
|
33
|
+
posterScene,
|
|
34
|
+
bulletsScene,
|
|
35
|
+
statScene,
|
|
36
|
+
flowScene,
|
|
37
|
+
outroScene,
|
|
38
|
+
lowerThirdScene,
|
|
39
|
+
bugScene,
|
|
40
|
+
progressScene,
|
|
41
|
+
countdownScene,
|
|
42
|
+
chapterScene,
|
|
43
|
+
captionScene,
|
|
44
|
+
compareScene,
|
|
45
|
+
calloutScene,
|
|
46
|
+
quoteScene,
|
|
47
|
+
iconGridScene,
|
|
48
|
+
stepsScene,
|
|
49
|
+
timelineScene,
|
|
50
|
+
mediaFrameScene,
|
|
51
|
+
kineticScene,
|
|
52
|
+
codeScene,
|
|
53
|
+
kpiGridScene,
|
|
54
|
+
chartScene,
|
|
55
|
+
tableScene,
|
|
56
|
+
scene3dScene,
|
|
57
|
+
stickerScene,
|
|
58
|
+
waveformScene,
|
|
59
|
+
];
|
|
60
|
+
export const sceneRegistry = new Map(plugins.map((plugin) => [plugin.type, plugin]));
|
|
61
|
+
export const sceneTypes = [...sceneRegistry.keys()];
|
|
62
|
+
export const getScene = (type) => {
|
|
63
|
+
const plugin = sceneRegistry.get(type);
|
|
64
|
+
if (!plugin) {
|
|
65
|
+
throw new Error(`unknown scene type "${type}"; use one of: ${sceneTypes.join("|")}`);
|
|
66
|
+
}
|
|
67
|
+
return plugin;
|
|
68
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type EaseFn = (t: number) => number;
|
|
2
|
+
/** @typedef {(t: number) => number} EaseFn */
|
|
3
|
+
/** Clamp to [0, 1]; non-finite input maps to 0.
|
|
4
|
+
* @param {number} t
|
|
5
|
+
* @returns {number}
|
|
6
|
+
*/
|
|
7
|
+
export declare const clamp01: (t: number) => number;
|
|
8
|
+
export declare const EASE_NAMES: string[];
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} name one of EASE_NAMES
|
|
11
|
+
* @param {number} t progress, clamped to [0, 1]
|
|
12
|
+
* @returns {number}
|
|
13
|
+
*/
|
|
14
|
+
export declare const ease: (name: string, t: number) => number;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Pure easing functions: no DOM, no clock, no state.
|
|
3
|
+
/** @typedef {(t: number) => number} EaseFn */
|
|
4
|
+
/** Clamp to [0, 1]; non-finite input maps to 0.
|
|
5
|
+
* @param {number} t
|
|
6
|
+
* @returns {number}
|
|
7
|
+
*/
|
|
8
|
+
export const clamp01 = (t) => (t > 0 ? Math.min(t, 1) : 0);
|
|
9
|
+
/** Normalised settle time of the closed-form spring response. */
|
|
10
|
+
const SPRING_SPAN = 1;
|
|
11
|
+
/**
|
|
12
|
+
* Closed-form underdamped spring step response, rescaled so f(1) = 1 exactly.
|
|
13
|
+
* @param {number} damping damping ratio, 0 < damping < 1
|
|
14
|
+
* @param {number} omega natural frequency over the normalised span
|
|
15
|
+
* @returns {EaseFn}
|
|
16
|
+
*/
|
|
17
|
+
const spring = (damping, omega) => {
|
|
18
|
+
const decay = damping * omega;
|
|
19
|
+
const damped = omega * Math.sqrt(1 - damping * damping);
|
|
20
|
+
/** @param {number} x */
|
|
21
|
+
const step = (x) => 1 - Math.exp(-decay * x) * (Math.cos(damped * x) + (decay / damped) * Math.sin(damped * x));
|
|
22
|
+
const settled = step(SPRING_SPAN);
|
|
23
|
+
return (t) => step(t * SPRING_SPAN) / settled;
|
|
24
|
+
};
|
|
25
|
+
/** @type {Readonly<Record<string, EaseFn>>} */
|
|
26
|
+
const EASINGS = {
|
|
27
|
+
linear: (t) => t,
|
|
28
|
+
in: (t) => t * t * t,
|
|
29
|
+
out: (t) => 1 - (1 - t) ** 3,
|
|
30
|
+
"in-out": (t) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2),
|
|
31
|
+
"spring-soft": spring(0.85, 9),
|
|
32
|
+
"spring-snappy": spring(0.6, 14),
|
|
33
|
+
};
|
|
34
|
+
export const EASE_NAMES = Object.keys(EASINGS);
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} name one of EASE_NAMES
|
|
37
|
+
* @param {number} t progress, clamped to [0, 1]
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
export const ease = (name, t) => {
|
|
41
|
+
const fn = EASINGS[name];
|
|
42
|
+
if (!fn)
|
|
43
|
+
throw new Error(`unknown ease "${name}"; use one of: ${EASE_NAMES.join("|")}`);
|
|
44
|
+
return fn(clamp01(t));
|
|
45
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type SceneConfig = {
|
|
2
|
+
ease?: string;
|
|
3
|
+
move?: string;
|
|
4
|
+
focus?: string;
|
|
5
|
+
};
|
|
6
|
+
export type RuntimeContext = {
|
|
7
|
+
fps?: number;
|
|
8
|
+
duration?: number;
|
|
9
|
+
};
|
|
10
|
+
export type Plan = import("./timeline.js").Plan;
|
|
11
|
+
export type Runtime = {
|
|
12
|
+
seek(t: number): void;
|
|
13
|
+
dispose(): void;
|
|
14
|
+
ready: Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export type CreateScene = (root: HTMLElement, config: SceneConfig, ctx: RuntimeContext) => Runtime;
|
|
17
|
+
export type Counter = {
|
|
18
|
+
el: HTMLElement;
|
|
19
|
+
text: string;
|
|
20
|
+
track: import("./timeline.js").Track;
|
|
21
|
+
};
|
|
22
|
+
export type Item = {
|
|
23
|
+
el: HTMLElement | SVGElement;
|
|
24
|
+
kind: string;
|
|
25
|
+
keys: string;
|
|
26
|
+
track: import("./timeline.js").Track;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Mounts every [data-anim] element of a scene fragment on one entrance timeline.
|
|
30
|
+
* @param {HTMLElement} root
|
|
31
|
+
* @param {SceneConfig} config
|
|
32
|
+
* @param {RuntimeContext} ctx
|
|
33
|
+
* @param {Plan} plan
|
|
34
|
+
* @returns {Runtime}
|
|
35
|
+
*/
|
|
36
|
+
export declare const mountTimeline: (root: HTMLElement, config: SceneConfig, ctx: RuntimeContext, plan: Plan) => Runtime;
|
|
37
|
+
/**
|
|
38
|
+
* An overlay: the entrance timeline plus an eased fade of the whole layer in and out of its
|
|
39
|
+
* time span. `hooks.update(t)` draws scene-specific state; `hooks.reset()` undoes it on dispose.
|
|
40
|
+
* @param {HTMLElement} root
|
|
41
|
+
* @param {SceneConfig} config
|
|
42
|
+
* @param {RuntimeContext} ctx
|
|
43
|
+
* @param {Plan} plan
|
|
44
|
+
* @param {{ update?: (t: number) => void, reset?: () => void }} [hooks]
|
|
45
|
+
* @returns {Runtime}
|
|
46
|
+
*/
|
|
47
|
+
export declare const mountOverlay: (root: HTMLElement, config: SceneConfig, ctx: RuntimeContext, plan: Plan, hooks?: {
|
|
48
|
+
update?: (t: number) => void;
|
|
49
|
+
reset?: () => void;
|
|
50
|
+
}) => Runtime;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Thin DOM layer: seek(t) writes styles as a pure function of t.
|
|
3
|
+
import { cameraScale, countScale, countText, countTrack, dimOpacity, driftOf, entranceState, entranceTrack, overlayEnvelope, sample, } from "./timeline.js";
|
|
4
|
+
/** @typedef {{ ease?: string, move?: string, focus?: string }} SceneConfig */
|
|
5
|
+
/** @typedef {{ fps?: number, duration?: number }} RuntimeContext */
|
|
6
|
+
/** @typedef {import("./timeline.js").Plan} Plan */
|
|
7
|
+
/** @typedef {{ seek(t: number): void, dispose(): void, ready: Promise<void> }} Runtime */
|
|
8
|
+
/** @typedef {(root: HTMLElement, config: SceneConfig, ctx: RuntimeContext) => Runtime} CreateScene */
|
|
9
|
+
/** @typedef {{ el: HTMLElement, text: string, track: import("./timeline.js").Track }} Counter */
|
|
10
|
+
/** @typedef {{ el: HTMLElement | SVGElement, kind: string, keys: string, track: import("./timeline.js").Track }} Item */
|
|
11
|
+
const DEFAULT_FPS = 30;
|
|
12
|
+
/**
|
|
13
|
+
* Config and ctx win over the data-* attributes the emitter put on the root.
|
|
14
|
+
* @param {HTMLElement} root
|
|
15
|
+
* @param {SceneConfig} config
|
|
16
|
+
* @param {RuntimeContext} ctx
|
|
17
|
+
*/
|
|
18
|
+
const readSettings = (root, config, ctx) => ({
|
|
19
|
+
ease: config.ease ?? root.dataset.ease ?? "out",
|
|
20
|
+
move: config.move ?? root.dataset.move ?? "static",
|
|
21
|
+
focus: config.focus ?? root.dataset.focus ?? "",
|
|
22
|
+
fps: ctx.fps ?? Number(root.dataset.fps ?? DEFAULT_FPS),
|
|
23
|
+
duration: ctx.duration ?? Number(root.dataset.duration ?? 0),
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* @param {HTMLElement} root
|
|
27
|
+
* @param {ReturnType<typeof readSettings>} settings
|
|
28
|
+
* @param {Plan} plan
|
|
29
|
+
* @returns {Item[]}
|
|
30
|
+
*/
|
|
31
|
+
const collectItems = (root, settings, plan) => Array.from(root.querySelectorAll("[data-anim]"), (node) => {
|
|
32
|
+
const el = /** @type {HTMLElement | SVGElement} */ (node);
|
|
33
|
+
const step = Number(el.dataset.step ?? 0);
|
|
34
|
+
return {
|
|
35
|
+
el,
|
|
36
|
+
kind: el.dataset.anim ?? "rise",
|
|
37
|
+
keys: el.dataset.focusKey ?? "",
|
|
38
|
+
track: entranceTrack(step, plan, settings.fps, settings.ease),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* @param {Item} item
|
|
43
|
+
* @param {number} t
|
|
44
|
+
* @param {string} focus
|
|
45
|
+
* @param {number} rise
|
|
46
|
+
*/
|
|
47
|
+
const applyItem = (item, t, focus, rise) => {
|
|
48
|
+
const state = entranceState(item.kind, sample(item.track, t), rise);
|
|
49
|
+
item.el.style.opacity = String(state.opacity * dimOpacity(focus, item.keys));
|
|
50
|
+
if (item.kind === "rise")
|
|
51
|
+
item.el.style.transform = `translateY(${state.y}px)`;
|
|
52
|
+
if (item.kind === "draw")
|
|
53
|
+
item.el.style.strokeDashoffset = String(state.dash);
|
|
54
|
+
if (item.kind === "grow")
|
|
55
|
+
item.el.style.transform = `scaleX(${state.scale})`;
|
|
56
|
+
};
|
|
57
|
+
/** @param {Item} item */
|
|
58
|
+
const clearItem = (item) => {
|
|
59
|
+
for (const name of ["opacity", "transform", "stroke-dashoffset"]) {
|
|
60
|
+
item.el.style.removeProperty(name);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Elements whose text counts up to the number they carry in data-count.
|
|
65
|
+
* @param {HTMLElement} root
|
|
66
|
+
* @param {ReturnType<typeof readSettings>} settings
|
|
67
|
+
* @param {Plan} plan
|
|
68
|
+
* @returns {Counter[]}
|
|
69
|
+
*/
|
|
70
|
+
const collectCounters = (root, settings, plan) => Array.from(root.querySelectorAll("[data-count]"), (node) => {
|
|
71
|
+
const el = /** @type {HTMLElement} */ (node);
|
|
72
|
+
const step = Number(el.dataset.step ?? 0);
|
|
73
|
+
return {
|
|
74
|
+
el,
|
|
75
|
+
text: el.dataset.count ?? "",
|
|
76
|
+
track: countTrack(step, plan, settings.fps, settings.ease, settings.duration),
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* @param {Counter} counter
|
|
81
|
+
* @param {number} t
|
|
82
|
+
*/
|
|
83
|
+
const applyCounter = (counter, t) => {
|
|
84
|
+
const progress = sample(counter.track, t);
|
|
85
|
+
counter.el.textContent = countText(counter.text, progress);
|
|
86
|
+
counter.el.style.transform = `scale(${countScale(progress)})`;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* @param {HTMLElement} root
|
|
90
|
+
* @returns {Promise<void>}
|
|
91
|
+
*/
|
|
92
|
+
const fontsReady = (root) => root.ownerDocument.fonts?.ready.then(() => undefined) ?? Promise.resolve();
|
|
93
|
+
/**
|
|
94
|
+
* Mounts every [data-anim] element of a scene fragment on one entrance timeline.
|
|
95
|
+
* @param {HTMLElement} root
|
|
96
|
+
* @param {SceneConfig} config
|
|
97
|
+
* @param {RuntimeContext} ctx
|
|
98
|
+
* @param {Plan} plan
|
|
99
|
+
* @returns {Runtime}
|
|
100
|
+
*/
|
|
101
|
+
export const mountTimeline = (root, config, ctx, plan) => {
|
|
102
|
+
const settings = readSettings(root, config, ctx);
|
|
103
|
+
const items = collectItems(root, settings, plan);
|
|
104
|
+
const counters = collectCounters(root, settings, plan);
|
|
105
|
+
/** @param {number} t */
|
|
106
|
+
const seek = (t) => {
|
|
107
|
+
for (const item of items)
|
|
108
|
+
applyItem(item, t, settings.focus, plan.rise);
|
|
109
|
+
for (const counter of counters)
|
|
110
|
+
applyCounter(counter, t);
|
|
111
|
+
root.style.setProperty("--drift", String(driftOf(t)));
|
|
112
|
+
root.style.transform = `scale(${cameraScale(settings.move, t, settings.duration)})`;
|
|
113
|
+
};
|
|
114
|
+
const dispose = () => {
|
|
115
|
+
for (const item of items)
|
|
116
|
+
clearItem(item);
|
|
117
|
+
for (const counter of counters) {
|
|
118
|
+
counter.el.textContent = counter.text;
|
|
119
|
+
counter.el.style.removeProperty("transform");
|
|
120
|
+
}
|
|
121
|
+
root.style.removeProperty("transform");
|
|
122
|
+
root.style.removeProperty("--drift");
|
|
123
|
+
};
|
|
124
|
+
seek(0);
|
|
125
|
+
return { seek, dispose, ready: fontsReady(root) };
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* An overlay: the entrance timeline plus an eased fade of the whole layer in and out of its
|
|
129
|
+
* time span. `hooks.update(t)` draws scene-specific state; `hooks.reset()` undoes it on dispose.
|
|
130
|
+
* @param {HTMLElement} root
|
|
131
|
+
* @param {SceneConfig} config
|
|
132
|
+
* @param {RuntimeContext} ctx
|
|
133
|
+
* @param {Plan} plan
|
|
134
|
+
* @param {{ update?: (t: number) => void, reset?: () => void }} [hooks]
|
|
135
|
+
* @returns {Runtime}
|
|
136
|
+
*/
|
|
137
|
+
export const mountOverlay = (root, config, ctx, plan, hooks = {}) => {
|
|
138
|
+
const settings = readSettings(root, config, ctx);
|
|
139
|
+
const inner = mountTimeline(root, config, ctx, plan);
|
|
140
|
+
/** @param {number} t */
|
|
141
|
+
const seek = (t) => {
|
|
142
|
+
inner.seek(t);
|
|
143
|
+
root.style.opacity = String(overlayEnvelope(t, settings.duration, settings.ease));
|
|
144
|
+
hooks.update?.(t);
|
|
145
|
+
};
|
|
146
|
+
const dispose = () => {
|
|
147
|
+
inner.dispose();
|
|
148
|
+
root.style.removeProperty("opacity");
|
|
149
|
+
hooks.reset?.();
|
|
150
|
+
};
|
|
151
|
+
seek(0);
|
|
152
|
+
return { seek, dispose, ready: inner.ready };
|
|
153
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export type Track = {
|
|
2
|
+
from: number;
|
|
3
|
+
to: number;
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
ease: string;
|
|
7
|
+
};
|
|
8
|
+
export type Plan = {
|
|
9
|
+
baseDelay: number;
|
|
10
|
+
staggerFrames: number;
|
|
11
|
+
rise: number;
|
|
12
|
+
};
|
|
13
|
+
export type EntranceState = {
|
|
14
|
+
opacity: number;
|
|
15
|
+
y: number;
|
|
16
|
+
dash: number;
|
|
17
|
+
scale: number;
|
|
18
|
+
};
|
|
19
|
+
/** @typedef {{ from: number, to: number, start: number, end: number, ease: string }} Track */
|
|
20
|
+
/** @typedef {{ baseDelay: number, staggerFrames: number, rise: number }} Plan */
|
|
21
|
+
/** @typedef {{ opacity: number, y: number, dash: number, scale: number }} EntranceState */
|
|
22
|
+
export declare const ENTRANCE_SECONDS = 0.6;
|
|
23
|
+
export declare const MIN_STAGGER_FRAMES = 3;
|
|
24
|
+
export declare const MAX_STAGGER_FRAMES = 6;
|
|
25
|
+
export declare const COUNT_SECONDS = 1.4;
|
|
26
|
+
/** Share of the scene a count-up may take, and its ceiling in seconds. */
|
|
27
|
+
export declare const COUNT_SHARE = 0.25;
|
|
28
|
+
export declare const MAX_COUNT_SECONDS = 3;
|
|
29
|
+
/** Opacity of a "rise" element at progress 0, so no scene opens on a blank frame. */
|
|
30
|
+
export declare const RISE_FLOOR = 0.45;
|
|
31
|
+
/** Size of a counting number at progress 0, relative to its final size. */
|
|
32
|
+
export declare const COUNT_SCALE_FROM = 0.7;
|
|
33
|
+
/** Fade-in and fade-out length of an overlay window, in seconds. */
|
|
34
|
+
export declare const ENVELOPE_SECONDS = 0.4;
|
|
35
|
+
/** Extra size of a countdown number at the start of each step. */
|
|
36
|
+
export declare const PULSE_SCALE = 0.25;
|
|
37
|
+
/**
|
|
38
|
+
* Value of a single-segment track at time t (seconds).
|
|
39
|
+
* @param {Track} track
|
|
40
|
+
* @param {number} t
|
|
41
|
+
* @returns {number}
|
|
42
|
+
*/
|
|
43
|
+
export declare const sample: (track: Track, t: number) => number;
|
|
44
|
+
/**
|
|
45
|
+
* Stagger between consecutive steps, in seconds, from a frame count.
|
|
46
|
+
* @param {number} frames 3-6
|
|
47
|
+
* @param {number} fps
|
|
48
|
+
* @returns {number}
|
|
49
|
+
*/
|
|
50
|
+
export declare const staggerSeconds: (frames: number, fps: number) => number;
|
|
51
|
+
/**
|
|
52
|
+
* Progress track (0 -> 1) of the entrance of step `step`.
|
|
53
|
+
* @param {number} step
|
|
54
|
+
* @param {Plan} plan
|
|
55
|
+
* @param {number} fps
|
|
56
|
+
* @param {string} easing
|
|
57
|
+
* @returns {Track}
|
|
58
|
+
*/
|
|
59
|
+
export declare const entranceTrack: (step: number, plan: Plan, fps: number, easing: string) => Track;
|
|
60
|
+
/**
|
|
61
|
+
* Visual state of an element kind at entrance progress p.
|
|
62
|
+
* @param {string} kind rise | fade | draw | grow
|
|
63
|
+
* @param {number} p progress; may overshoot for springs
|
|
64
|
+
* @param {number} rise rise distance in px
|
|
65
|
+
* @returns {EntranceState}
|
|
66
|
+
*/
|
|
67
|
+
export declare const entranceState: (kind: string, p: number, rise: number) => EntranceState;
|
|
68
|
+
/**
|
|
69
|
+
* Count-up length: COUNT_SECONDS, stretched to a quarter of a long scene (max MAX_COUNT_SECONDS).
|
|
70
|
+
* @param {number} duration scene seconds; 0 or unknown keeps COUNT_SECONDS
|
|
71
|
+
* @returns {number}
|
|
72
|
+
*/
|
|
73
|
+
export declare const countSeconds: (duration: number) => number;
|
|
74
|
+
/**
|
|
75
|
+
* Track of a count-up that starts with the entrance of `step`.
|
|
76
|
+
* @param {number} step
|
|
77
|
+
* @param {Plan} plan
|
|
78
|
+
* @param {number} fps
|
|
79
|
+
* @param {string} easing
|
|
80
|
+
* @param {number} [duration] scene seconds
|
|
81
|
+
* @returns {Track}
|
|
82
|
+
*/
|
|
83
|
+
export declare const countTrack: (step: number, plan: Plan, fps: number, easing: string, duration?: number) => Track;
|
|
84
|
+
/**
|
|
85
|
+
* Scale of a counting number at count progress p: grows from COUNT_SCALE_FROM to 1.
|
|
86
|
+
* @param {number} p
|
|
87
|
+
* @returns {number}
|
|
88
|
+
*/
|
|
89
|
+
export declare const countScale: (p: number) => number;
|
|
90
|
+
/**
|
|
91
|
+
* The first number of `text` scaled to progress p, keeping prefix, suffix, decimals and
|
|
92
|
+
* thousands separators; p >= 1 and text without one leading number return `text` verbatim.
|
|
93
|
+
* @param {string} text
|
|
94
|
+
* @param {number} p
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
export declare const countText: (text: string, p: number) => string;
|
|
98
|
+
/**
|
|
99
|
+
* Slow background drift in [-1, 1]: one cycle every ~18 s.
|
|
100
|
+
* @param {number} t seconds
|
|
101
|
+
* @returns {number}
|
|
102
|
+
*/
|
|
103
|
+
export declare const driftOf: (t: number) => number;
|
|
104
|
+
/**
|
|
105
|
+
* Root scale of the supported camera moves; other moves are applied by the build.
|
|
106
|
+
* @param {string} move
|
|
107
|
+
* @param {number} t
|
|
108
|
+
* @param {number} duration
|
|
109
|
+
* @returns {number}
|
|
110
|
+
*/
|
|
111
|
+
export declare const cameraScale: (move: string, t: number, duration: number) => number;
|
|
112
|
+
/**
|
|
113
|
+
* Opacity multiplier of `focus`: elements outside the focus dim.
|
|
114
|
+
* @param {string} focus focused key; empty means no focus
|
|
115
|
+
* @param {string} keys space-separated keys of the element; empty means never dim
|
|
116
|
+
* @returns {number}
|
|
117
|
+
*/
|
|
118
|
+
export declare const dimOpacity: (focus: string, keys: string) => number;
|
|
119
|
+
/**
|
|
120
|
+
* Opacity of an overlay at local time t: eased fade in, hold, eased fade out.
|
|
121
|
+
* @param {number} t seconds from the start of the overlay window
|
|
122
|
+
* @param {number} duration window length in seconds
|
|
123
|
+
* @param {string} easing
|
|
124
|
+
* @returns {number}
|
|
125
|
+
*/
|
|
126
|
+
export declare const overlayEnvelope: (t: number, duration: number, easing: string) => number;
|
|
127
|
+
/**
|
|
128
|
+
* Countdown value and pulse scale at local time t: one equal step per integer from -> to.
|
|
129
|
+
* @param {number} t
|
|
130
|
+
* @param {number} duration
|
|
131
|
+
* @param {number} from
|
|
132
|
+
* @param {number} to
|
|
133
|
+
* @returns {{ value: number, scale: number }}
|
|
134
|
+
*/
|
|
135
|
+
export declare const countdownState: (t: number, duration: number, from: number, to: number) => {
|
|
136
|
+
value: number;
|
|
137
|
+
scale: number;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Linear value from -> to across the duration (progress bars).
|
|
141
|
+
* @param {number} t
|
|
142
|
+
* @param {number} duration
|
|
143
|
+
* @param {number} from
|
|
144
|
+
* @param {number} to
|
|
145
|
+
* @returns {number}
|
|
146
|
+
*/
|
|
147
|
+
export declare const spanProgress: (t: number, duration: number, from: number, to: number) => number;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Pure timeline core: every value is a function of (inputs, t) only.
|
|
3
|
+
import { clamp01, ease } from "./easing.js";
|
|
4
|
+
/** @typedef {{ from: number, to: number, start: number, end: number, ease: string }} Track */
|
|
5
|
+
/** @typedef {{ baseDelay: number, staggerFrames: number, rise: number }} Plan */
|
|
6
|
+
/** @typedef {{ opacity: number, y: number, dash: number, scale: number }} EntranceState */
|
|
7
|
+
export const ENTRANCE_SECONDS = 0.6;
|
|
8
|
+
export const MIN_STAGGER_FRAMES = 3;
|
|
9
|
+
export const MAX_STAGGER_FRAMES = 6;
|
|
10
|
+
export const COUNT_SECONDS = 1.4;
|
|
11
|
+
/** Share of the scene a count-up may take, and its ceiling in seconds. */
|
|
12
|
+
export const COUNT_SHARE = 0.25;
|
|
13
|
+
export const MAX_COUNT_SECONDS = 3;
|
|
14
|
+
/** Opacity of a "rise" element at progress 0, so no scene opens on a blank frame. */
|
|
15
|
+
export const RISE_FLOOR = 0.45;
|
|
16
|
+
/** Size of a counting number at progress 0, relative to its final size. */
|
|
17
|
+
export const COUNT_SCALE_FROM = 0.7;
|
|
18
|
+
/** Fade-in and fade-out length of an overlay window, in seconds. */
|
|
19
|
+
export const ENVELOPE_SECONDS = 0.4;
|
|
20
|
+
/** Extra size of a countdown number at the start of each step. */
|
|
21
|
+
export const PULSE_SCALE = 0.25;
|
|
22
|
+
const PULSE_SHARE = 0.4;
|
|
23
|
+
const DRIFT_RATE = 0.35;
|
|
24
|
+
const GROUP_SIZE = /\B(?=(\d{3})+(?!\d))/g;
|
|
25
|
+
const LEADING_NUMBER = /^(\D*?)(\d[\d,]*(?:\.\d+)?)(\D*)$/;
|
|
26
|
+
const DIM_OPACITY = 0.3;
|
|
27
|
+
const MOVE_DELTA = 0.04;
|
|
28
|
+
/**
|
|
29
|
+
* Value of a single-segment track at time t (seconds).
|
|
30
|
+
* @param {Track} track
|
|
31
|
+
* @param {number} t
|
|
32
|
+
* @returns {number}
|
|
33
|
+
*/
|
|
34
|
+
export const sample = (track, t) => {
|
|
35
|
+
const span = track.end - track.start;
|
|
36
|
+
const progress = span > 0 ? clamp01((t - track.start) / span) : Number(t >= track.end);
|
|
37
|
+
return track.from + (track.to - track.from) * ease(track.ease, progress);
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Stagger between consecutive steps, in seconds, from a frame count.
|
|
41
|
+
* @param {number} frames 3-6
|
|
42
|
+
* @param {number} fps
|
|
43
|
+
* @returns {number}
|
|
44
|
+
*/
|
|
45
|
+
export const staggerSeconds = (frames, fps) => {
|
|
46
|
+
if (frames < MIN_STAGGER_FRAMES || frames > MAX_STAGGER_FRAMES) {
|
|
47
|
+
throw new RangeError(`stagger must be ${MIN_STAGGER_FRAMES}-${MAX_STAGGER_FRAMES} frames`);
|
|
48
|
+
}
|
|
49
|
+
return frames / fps;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Progress track (0 -> 1) of the entrance of step `step`.
|
|
53
|
+
* @param {number} step
|
|
54
|
+
* @param {Plan} plan
|
|
55
|
+
* @param {number} fps
|
|
56
|
+
* @param {string} easing
|
|
57
|
+
* @returns {Track}
|
|
58
|
+
*/
|
|
59
|
+
export const entranceTrack = (step, plan, fps, easing) => {
|
|
60
|
+
const start = plan.baseDelay + step * staggerSeconds(plan.staggerFrames, fps);
|
|
61
|
+
return { from: 0, to: 1, start, end: start + ENTRANCE_SECONDS, ease: easing };
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} kind
|
|
65
|
+
* @param {number} p
|
|
66
|
+
* @returns {number}
|
|
67
|
+
*/
|
|
68
|
+
const entranceOpacity = (kind, p) => {
|
|
69
|
+
if (kind === "draw")
|
|
70
|
+
return 1;
|
|
71
|
+
return kind === "rise" ? RISE_FLOOR + (1 - RISE_FLOOR) * clamp01(p) : clamp01(p);
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Visual state of an element kind at entrance progress p.
|
|
75
|
+
* @param {string} kind rise | fade | draw | grow
|
|
76
|
+
* @param {number} p progress; may overshoot for springs
|
|
77
|
+
* @param {number} rise rise distance in px
|
|
78
|
+
* @returns {EntranceState}
|
|
79
|
+
*/
|
|
80
|
+
export const entranceState = (kind, p, rise) => ({
|
|
81
|
+
opacity: entranceOpacity(kind, p),
|
|
82
|
+
y: kind === "rise" ? (1 - p) * rise : 0,
|
|
83
|
+
dash: 1 - p,
|
|
84
|
+
scale: kind === "grow" ? clamp01(p) : 1,
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* Count-up length: COUNT_SECONDS, stretched to a quarter of a long scene (max MAX_COUNT_SECONDS).
|
|
88
|
+
* @param {number} duration scene seconds; 0 or unknown keeps COUNT_SECONDS
|
|
89
|
+
* @returns {number}
|
|
90
|
+
*/
|
|
91
|
+
export const countSeconds = (duration) => Math.min(MAX_COUNT_SECONDS, Math.max(COUNT_SECONDS, duration * COUNT_SHARE));
|
|
92
|
+
/**
|
|
93
|
+
* Track of a count-up that starts with the entrance of `step`.
|
|
94
|
+
* @param {number} step
|
|
95
|
+
* @param {Plan} plan
|
|
96
|
+
* @param {number} fps
|
|
97
|
+
* @param {string} easing
|
|
98
|
+
* @param {number} [duration] scene seconds
|
|
99
|
+
* @returns {Track}
|
|
100
|
+
*/
|
|
101
|
+
export const countTrack = (step, plan, fps, easing, duration = 0) => {
|
|
102
|
+
const { start } = entranceTrack(step, plan, fps, easing);
|
|
103
|
+
return { from: 0, to: 1, start, end: start + countSeconds(duration), ease: easing };
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Scale of a counting number at count progress p: grows from COUNT_SCALE_FROM to 1.
|
|
107
|
+
* @param {number} p
|
|
108
|
+
* @returns {number}
|
|
109
|
+
*/
|
|
110
|
+
export const countScale = (p) => COUNT_SCALE_FROM + (1 - COUNT_SCALE_FROM) * clamp01(p);
|
|
111
|
+
/** @param {string} digits */
|
|
112
|
+
const withGrouping = (digits) => digits.replace(GROUP_SIZE, ",");
|
|
113
|
+
/**
|
|
114
|
+
* The first number of `text` scaled to progress p, keeping prefix, suffix, decimals and
|
|
115
|
+
* thousands separators; p >= 1 and text without one leading number return `text` verbatim.
|
|
116
|
+
* @param {string} text
|
|
117
|
+
* @param {number} p
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
export const countText = (text, p) => {
|
|
121
|
+
const match = LEADING_NUMBER.exec(text);
|
|
122
|
+
if (!match || p >= 1)
|
|
123
|
+
return text;
|
|
124
|
+
const [, prefix = "", number = "", suffix = ""] = match;
|
|
125
|
+
const decimals = number.includes(".") ? number.length - number.indexOf(".") - 1 : 0;
|
|
126
|
+
const [whole = "", fraction] = (parseFloat(number.replaceAll(",", "")) * clamp01(p))
|
|
127
|
+
.toFixed(decimals)
|
|
128
|
+
.split(".");
|
|
129
|
+
const grouped = number.includes(",") ? withGrouping(whole) : whole;
|
|
130
|
+
return `${prefix}${fraction === undefined ? grouped : `${grouped}.${fraction}`}${suffix}`;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Slow background drift in [-1, 1]: one cycle every ~18 s.
|
|
134
|
+
* @param {number} t seconds
|
|
135
|
+
* @returns {number}
|
|
136
|
+
*/
|
|
137
|
+
export const driftOf = (t) => Math.sin(t * DRIFT_RATE);
|
|
138
|
+
/**
|
|
139
|
+
* Root scale of the supported camera moves; other moves are applied by the build.
|
|
140
|
+
* @param {string} move
|
|
141
|
+
* @param {number} t
|
|
142
|
+
* @param {number} duration
|
|
143
|
+
* @returns {number}
|
|
144
|
+
*/
|
|
145
|
+
export const cameraScale = (move, t, duration) => {
|
|
146
|
+
const progress = duration > 0 ? clamp01(t / duration) : 0;
|
|
147
|
+
if (move === "push-in")
|
|
148
|
+
return 1 + MOVE_DELTA * progress;
|
|
149
|
+
if (move === "pull-out")
|
|
150
|
+
return 1 + MOVE_DELTA * (1 - progress);
|
|
151
|
+
return 1;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Opacity multiplier of `focus`: elements outside the focus dim.
|
|
155
|
+
* @param {string} focus focused key; empty means no focus
|
|
156
|
+
* @param {string} keys space-separated keys of the element; empty means never dim
|
|
157
|
+
* @returns {number}
|
|
158
|
+
*/
|
|
159
|
+
export const dimOpacity = (focus, keys) => !focus || !keys || keys.split(" ").includes(focus) ? 1 : DIM_OPACITY;
|
|
160
|
+
/**
|
|
161
|
+
* Opacity of an overlay at local time t: eased fade in, hold, eased fade out.
|
|
162
|
+
* @param {number} t seconds from the start of the overlay window
|
|
163
|
+
* @param {number} duration window length in seconds
|
|
164
|
+
* @param {string} easing
|
|
165
|
+
* @returns {number}
|
|
166
|
+
*/
|
|
167
|
+
export const overlayEnvelope = (t, duration, easing) => {
|
|
168
|
+
const edge = Math.min(ENVELOPE_SECONDS, duration / 2);
|
|
169
|
+
if (!(edge > 0))
|
|
170
|
+
return 1;
|
|
171
|
+
return clamp01(Math.min(ease(easing, t / edge), ease(easing, (duration - t) / edge)));
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Countdown value and pulse scale at local time t: one equal step per integer from -> to.
|
|
175
|
+
* @param {number} t
|
|
176
|
+
* @param {number} duration
|
|
177
|
+
* @param {number} from
|
|
178
|
+
* @param {number} to
|
|
179
|
+
* @returns {{ value: number, scale: number }}
|
|
180
|
+
*/
|
|
181
|
+
export const countdownState = (t, duration, from, to) => {
|
|
182
|
+
const steps = Math.abs(to - from) + 1;
|
|
183
|
+
const length = duration / steps;
|
|
184
|
+
const index = Math.min(steps - 1, Math.floor(clamp01(t / duration) * steps));
|
|
185
|
+
const local = length > 0 ? (t - index * length) / length : 1;
|
|
186
|
+
return {
|
|
187
|
+
value: from + Math.sign(to - from) * index,
|
|
188
|
+
scale: 1 + PULSE_SCALE * (1 - ease("out", local / PULSE_SHARE)),
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Linear value from -> to across the duration (progress bars).
|
|
193
|
+
* @param {number} t
|
|
194
|
+
* @param {number} duration
|
|
195
|
+
* @param {number} from
|
|
196
|
+
* @param {number} to
|
|
197
|
+
* @returns {number}
|
|
198
|
+
*/
|
|
199
|
+
export const spanProgress = (t, duration, from, to) => from + (to - from) * (duration > 0 ? clamp01(t / duration) : 1);
|