@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.
Files changed (219) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -2
  3. package/dist/catalog.d.ts +17 -0
  4. package/dist/catalog.js +51 -0
  5. package/dist/custom.d.ts +86 -0
  6. package/dist/custom.js +170 -0
  7. package/dist/define-scene.d.ts +31 -0
  8. package/dist/define-scene.js +128 -0
  9. package/dist/html.d.ts +5 -0
  10. package/dist/html.js +14 -0
  11. package/dist/index.d.ts +11 -0
  12. package/dist/index.js +11 -0
  13. package/dist/position.d.ts +21 -0
  14. package/dist/position.js +16 -0
  15. package/dist/registry.d.ts +4 -0
  16. package/dist/registry.js +68 -0
  17. package/dist/runtime/easing.d.ts +14 -0
  18. package/dist/runtime/easing.js +45 -0
  19. package/dist/runtime/mount.d.ts +50 -0
  20. package/dist/runtime/mount.js +153 -0
  21. package/dist/runtime/timeline.d.ts +147 -0
  22. package/dist/runtime/timeline.js +199 -0
  23. package/dist/runtime-files.d.ts +2 -0
  24. package/dist/runtime-files.js +15 -0
  25. package/dist/scene-contract.d.ts +7 -0
  26. package/dist/scene-contract.js +104 -0
  27. package/dist/scene3d/post.d.ts +12 -0
  28. package/dist/scene3d/post.js +16 -0
  29. package/dist/scene3d/reflector.d.ts +9 -0
  30. package/dist/scene3d/reflector.js +17 -0
  31. package/dist/scene3d/textures.d.ts +9 -0
  32. package/dist/scene3d/textures.js +11 -0
  33. package/dist/scene3d/volumetric.d.ts +9 -0
  34. package/dist/scene3d/volumetric.js +16 -0
  35. package/dist/scenes/bug/index.d.ts +1 -0
  36. package/dist/scenes/bug/index.js +45 -0
  37. package/dist/scenes/bug/scene.d.ts +2 -0
  38. package/dist/scenes/bug/scene.js +6 -0
  39. package/dist/scenes/bug/style.css +30 -0
  40. package/dist/scenes/bullets/index.d.ts +1 -0
  41. package/dist/scenes/bullets/index.js +128 -0
  42. package/dist/scenes/bullets/scene.d.ts +2 -0
  43. package/dist/scenes/bullets/scene.js +6 -0
  44. package/dist/scenes/bullets/style.css +91 -0
  45. package/dist/scenes/callout/index.d.ts +9 -0
  46. package/dist/scenes/callout/index.js +95 -0
  47. package/dist/scenes/callout/scene.d.ts +2 -0
  48. package/dist/scenes/callout/scene.js +43 -0
  49. package/dist/scenes/callout/style.css +97 -0
  50. package/dist/scenes/caption/index.d.ts +1 -0
  51. package/dist/scenes/caption/index.js +72 -0
  52. package/dist/scenes/caption/scene.d.ts +11 -0
  53. package/dist/scenes/caption/scene.js +33 -0
  54. package/dist/scenes/caption/style.css +33 -0
  55. package/dist/scenes/chapter/index.d.ts +1 -0
  56. package/dist/scenes/chapter/index.js +29 -0
  57. package/dist/scenes/chapter/scene.d.ts +2 -0
  58. package/dist/scenes/chapter/scene.js +6 -0
  59. package/dist/scenes/chapter/style.css +25 -0
  60. package/dist/scenes/chart/index.d.ts +1 -0
  61. package/dist/scenes/chart/index.js +24 -0
  62. package/dist/scenes/chart/render.d.ts +4 -0
  63. package/dist/scenes/chart/render.js +257 -0
  64. package/dist/scenes/chart/scale.d.ts +11 -0
  65. package/dist/scenes/chart/scale.js +53 -0
  66. package/dist/scenes/chart/scene.d.ts +2 -0
  67. package/dist/scenes/chart/scene.js +73 -0
  68. package/dist/scenes/chart/schema.d.ts +36 -0
  69. package/dist/scenes/chart/schema.js +109 -0
  70. package/dist/scenes/chart/style.css +425 -0
  71. package/dist/scenes/code/index.d.ts +1 -0
  72. package/dist/scenes/code/index.js +109 -0
  73. package/dist/scenes/code/scene.d.ts +2 -0
  74. package/dist/scenes/code/scene.js +108 -0
  75. package/dist/scenes/code/style.css +155 -0
  76. package/dist/scenes/code/tokenize.d.ts +8 -0
  77. package/dist/scenes/code/tokenize.js +145 -0
  78. package/dist/scenes/compare/index.d.ts +1 -0
  79. package/dist/scenes/compare/index.js +128 -0
  80. package/dist/scenes/compare/scene.d.ts +2 -0
  81. package/dist/scenes/compare/scene.js +58 -0
  82. package/dist/scenes/compare/style.css +187 -0
  83. package/dist/scenes/countdown/index.d.ts +1 -0
  84. package/dist/scenes/countdown/index.js +46 -0
  85. package/dist/scenes/countdown/scene.d.ts +2 -0
  86. package/dist/scenes/countdown/scene.js +21 -0
  87. package/dist/scenes/countdown/style.css +32 -0
  88. package/dist/scenes/flow/index.d.ts +1 -0
  89. package/dist/scenes/flow/index.js +66 -0
  90. package/dist/scenes/flow/inspect.d.ts +26 -0
  91. package/dist/scenes/flow/inspect.js +25 -0
  92. package/dist/scenes/flow/layout.d.ts +83 -0
  93. package/dist/scenes/flow/layout.js +222 -0
  94. package/dist/scenes/flow/measure.d.ts +15 -0
  95. package/dist/scenes/flow/measure.js +33 -0
  96. package/dist/scenes/flow/plan.d.ts +42 -0
  97. package/dist/scenes/flow/plan.js +117 -0
  98. package/dist/scenes/flow/scene.d.ts +2 -0
  99. package/dist/scenes/flow/scene.js +6 -0
  100. package/dist/scenes/flow/schema.d.ts +49 -0
  101. package/dist/scenes/flow/schema.js +60 -0
  102. package/dist/scenes/flow/style.css +54 -0
  103. package/dist/scenes/hero/index.d.ts +3 -0
  104. package/dist/scenes/hero/index.js +77 -0
  105. package/dist/scenes/hero/scene.d.ts +2 -0
  106. package/dist/scenes/hero/scene.js +6 -0
  107. package/dist/scenes/hero/style.css +143 -0
  108. package/dist/scenes/icon-grid/index.d.ts +1 -0
  109. package/dist/scenes/icon-grid/index.js +69 -0
  110. package/dist/scenes/icon-grid/scene.d.ts +2 -0
  111. package/dist/scenes/icon-grid/scene.js +6 -0
  112. package/dist/scenes/icon-grid/style.css +74 -0
  113. package/dist/scenes/kinetic/index.d.ts +1 -0
  114. package/dist/scenes/kinetic/index.js +62 -0
  115. package/dist/scenes/kinetic/scene.d.ts +2 -0
  116. package/dist/scenes/kinetic/scene.js +98 -0
  117. package/dist/scenes/kinetic/style.css +94 -0
  118. package/dist/scenes/kpi-grid/index.d.ts +1 -0
  119. package/dist/scenes/kpi-grid/index.js +161 -0
  120. package/dist/scenes/kpi-grid/scene.d.ts +2 -0
  121. package/dist/scenes/kpi-grid/scene.js +6 -0
  122. package/dist/scenes/kpi-grid/style.css +141 -0
  123. package/dist/scenes/lower-third/index.d.ts +1 -0
  124. package/dist/scenes/lower-third/index.js +33 -0
  125. package/dist/scenes/lower-third/scene.d.ts +2 -0
  126. package/dist/scenes/lower-third/scene.js +6 -0
  127. package/dist/scenes/lower-third/style.css +45 -0
  128. package/dist/scenes/media-frame/index.d.ts +1 -0
  129. package/dist/scenes/media-frame/index.js +99 -0
  130. package/dist/scenes/media-frame/scene.d.ts +2 -0
  131. package/dist/scenes/media-frame/scene.js +85 -0
  132. package/dist/scenes/media-frame/style.css +275 -0
  133. package/dist/scenes/outro/index.d.ts +1 -0
  134. package/dist/scenes/outro/index.js +54 -0
  135. package/dist/scenes/outro/scene.d.ts +2 -0
  136. package/dist/scenes/outro/scene.js +6 -0
  137. package/dist/scenes/outro/style.css +92 -0
  138. package/dist/scenes/poster/index.d.ts +3 -0
  139. package/dist/scenes/poster/index.js +62 -0
  140. package/dist/scenes/poster/scene.d.ts +2 -0
  141. package/dist/scenes/poster/scene.js +6 -0
  142. package/dist/scenes/poster/style.css +102 -0
  143. package/dist/scenes/progress/index.d.ts +1 -0
  144. package/dist/scenes/progress/index.js +40 -0
  145. package/dist/scenes/progress/scene.d.ts +2 -0
  146. package/dist/scenes/progress/scene.js +17 -0
  147. package/dist/scenes/progress/style.css +31 -0
  148. package/dist/scenes/quote/index.d.ts +3 -0
  149. package/dist/scenes/quote/index.js +69 -0
  150. package/dist/scenes/quote/scene.d.ts +2 -0
  151. package/dist/scenes/quote/scene.js +6 -0
  152. package/dist/scenes/quote/style.css +86 -0
  153. package/dist/scenes/scene3d/index.d.ts +1 -0
  154. package/dist/scenes/scene3d/index.js +58 -0
  155. package/dist/scenes/scene3d/scene.d.ts +2 -0
  156. package/dist/scenes/scene3d/scene.js +413 -0
  157. package/dist/scenes/scene3d/style.css +43 -0
  158. package/dist/scenes/stat/index.d.ts +4 -0
  159. package/dist/scenes/stat/index.js +72 -0
  160. package/dist/scenes/stat/scene.d.ts +2 -0
  161. package/dist/scenes/stat/scene.js +6 -0
  162. package/dist/scenes/stat/style.css +95 -0
  163. package/dist/scenes/steps/index.d.ts +1 -0
  164. package/dist/scenes/steps/index.js +147 -0
  165. package/dist/scenes/steps/scene.d.ts +2 -0
  166. package/dist/scenes/steps/scene.js +6 -0
  167. package/dist/scenes/steps/style.css +190 -0
  168. package/dist/scenes/sticker/index.d.ts +1 -0
  169. package/dist/scenes/sticker/index.js +48 -0
  170. package/dist/scenes/sticker/scene.d.ts +2 -0
  171. package/dist/scenes/sticker/scene.js +57 -0
  172. package/dist/scenes/sticker/style.css +88 -0
  173. package/dist/scenes/table/index.d.ts +1 -0
  174. package/dist/scenes/table/index.js +131 -0
  175. package/dist/scenes/table/scene.d.ts +2 -0
  176. package/dist/scenes/table/scene.js +6 -0
  177. package/dist/scenes/table/style.css +102 -0
  178. package/dist/scenes/timeline/index.d.ts +1 -0
  179. package/dist/scenes/timeline/index.js +68 -0
  180. package/dist/scenes/timeline/scene.d.ts +2 -0
  181. package/dist/scenes/timeline/scene.js +6 -0
  182. package/dist/scenes/timeline/style.css +161 -0
  183. package/dist/scenes/title/index.d.ts +1 -0
  184. package/dist/scenes/title/index.js +52 -0
  185. package/dist/scenes/title/scene.d.ts +2 -0
  186. package/dist/scenes/title/scene.js +6 -0
  187. package/dist/scenes/title/style.css +80 -0
  188. package/dist/scenes/waveform/index.d.ts +1 -0
  189. package/dist/scenes/waveform/index.js +39 -0
  190. package/dist/scenes/waveform/scene.d.ts +2 -0
  191. package/dist/scenes/waveform/scene.js +35 -0
  192. package/dist/scenes/waveform/style.css +44 -0
  193. package/dist/shared/fit-text.d.ts +24 -0
  194. package/dist/shared/fit-text.js +82 -0
  195. package/dist/shared/format.d.ts +24 -0
  196. package/dist/shared/format.js +59 -0
  197. package/dist/shared/frame.d.ts +14 -0
  198. package/dist/shared/frame.js +21 -0
  199. package/dist/shared/icon-search.d.ts +9 -0
  200. package/dist/shared/icon-search.js +26 -0
  201. package/dist/shared/icon.d.ts +15 -0
  202. package/dist/shared/icon.js +28 -0
  203. package/dist/shared/index.d.ts +11 -0
  204. package/dist/shared/index.js +7 -0
  205. package/dist/shared/markup.d.ts +12 -0
  206. package/dist/shared/markup.js +43 -0
  207. package/dist/shared/measure.d.ts +7 -0
  208. package/dist/shared/measure.js +34 -0
  209. package/dist/shared/motion.d.ts +94 -0
  210. package/dist/shared/motion.js +117 -0
  211. package/dist/source-hash.d.ts +4 -0
  212. package/dist/source-hash.js +26 -0
  213. package/dist/test-fixtures.d.ts +9 -0
  214. package/dist/test-fixtures.js +22 -0
  215. package/dist/text-file.d.ts +1 -0
  216. package/dist/text-file.js +3 -0
  217. package/dist/words.d.ts +11 -0
  218. package/dist/words.js +93 -0
  219. package/package.json +36 -3
@@ -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);
@@ -0,0 +1,2 @@
1
+ /** Shared browser modules; the build emits them once under `runtime/` and `shared/`. */
2
+ export declare const runtimeFiles: () => Map<string, string>;