@anvpx/motion-scenes 0.0.0-stage → 0.1.1
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 +176 -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/compose.d.ts +21 -0
- package/dist/runtime/compose.js +43 -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 +4 -0
- package/dist/runtime-files.js +18 -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 +59 -0
- package/dist/scenes/scene3d/scene.d.ts +2 -0
- package/dist/scenes/scene3d/scene.js +436 -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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { textWidth } from "./measure.js";
|
|
2
|
+
const STEP_PX = 2;
|
|
3
|
+
const DEFAULT_MIN_LAST = 0.35;
|
|
4
|
+
const buildTable = (words, k, fontPx) => {
|
|
5
|
+
const n = words.length;
|
|
6
|
+
const w = (a, b) => textWidth(words.slice(a, b).join(" "), fontPx);
|
|
7
|
+
const best = Array.from({ length: k + 1 }, () => Array(n + 1).fill(Infinity));
|
|
8
|
+
const cut = Array.from({ length: k + 1 }, () => Array(n + 1).fill(0));
|
|
9
|
+
best[0][0] = 0;
|
|
10
|
+
for (let j = 1; j <= k; j++) {
|
|
11
|
+
for (let i = j; i <= n; i++) {
|
|
12
|
+
for (let c = j - 1; c < i; c++) {
|
|
13
|
+
const cost = Math.max(best[j - 1][c], w(c, i));
|
|
14
|
+
if (cost < best[j][i]) {
|
|
15
|
+
best[j][i] = cost;
|
|
16
|
+
cut[j][i] = c;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { best, cut };
|
|
22
|
+
};
|
|
23
|
+
/** Split into exactly `k` lines minimising the widest line (ties: earliest cuts). */
|
|
24
|
+
const balanced = (words, k, fontPx) => {
|
|
25
|
+
const { cut } = buildTable(words, k, fontPx);
|
|
26
|
+
const lines = [];
|
|
27
|
+
let end = words.length;
|
|
28
|
+
for (let j = k; j >= 1; j--) {
|
|
29
|
+
const start = cut[j][end];
|
|
30
|
+
lines.unshift(words.slice(start, end).join(" "));
|
|
31
|
+
end = start;
|
|
32
|
+
}
|
|
33
|
+
return lines;
|
|
34
|
+
};
|
|
35
|
+
const lastLineOk = (lines, fontPx, minLast) => {
|
|
36
|
+
if (lines.length < 2)
|
|
37
|
+
return true;
|
|
38
|
+
const widths = lines.map((line) => textWidth(line, fontPx));
|
|
39
|
+
return (widths.at(-1) ?? 0) >= minLast * Math.max(...widths);
|
|
40
|
+
};
|
|
41
|
+
const validate = (opts) => {
|
|
42
|
+
const { boxWidth, maxFont, minFont, maxLines } = opts;
|
|
43
|
+
if (!(boxWidth > 0))
|
|
44
|
+
throw new RangeError(`fitText: boxWidth must be > 0, got ${boxWidth}`);
|
|
45
|
+
if (!(minFont > 0) || maxFont < minFont)
|
|
46
|
+
throw new RangeError(`fitText: font range must satisfy 0 < minFont <= maxFont, got ${minFont}..${maxFont}`);
|
|
47
|
+
if (!Number.isInteger(maxLines) || maxLines < 1)
|
|
48
|
+
throw new RangeError(`fitText: maxLines must be an integer >= 1, got ${maxLines}`);
|
|
49
|
+
};
|
|
50
|
+
/** First line count (fewest lines) at `px` that fits the box with a non-stub last line. */
|
|
51
|
+
const wrapAt = (words, px, opts) => {
|
|
52
|
+
const minLast = opts.minLastLine ?? DEFAULT_MIN_LAST;
|
|
53
|
+
const most = Math.min(opts.maxLines, Math.max(words.length, 1));
|
|
54
|
+
for (let k = 1; k <= most; k++) {
|
|
55
|
+
const lines = words.length === 0 ? [""] : balanced(words, k, px);
|
|
56
|
+
const widest = Math.max(...lines.map((line) => textWidth(line, px)));
|
|
57
|
+
if (widest <= opts.boxWidth && lastLineOk(lines, px, minLast))
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Largest font in [minFont, maxFont] whose balanced wrap fits the box in at most `maxLines`
|
|
64
|
+
* lines, never breaking a word and never leaving a stub last line. When nothing fits, returns the
|
|
65
|
+
* single-line text at `minFont` with `fits: false` and the offending words.
|
|
66
|
+
*/
|
|
67
|
+
export const fitText = (text, opts) => {
|
|
68
|
+
validate(opts);
|
|
69
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
70
|
+
const overlongAt = (px) => words.filter((word) => textWidth(word, px) > opts.boxWidth);
|
|
71
|
+
for (let px = opts.maxFont; px >= opts.minFont; px -= STEP_PX) {
|
|
72
|
+
const lines = overlongAt(px).length === 0 ? wrapAt(words, px, opts) : undefined;
|
|
73
|
+
if (lines !== undefined)
|
|
74
|
+
return { lines, fontPx: px, fits: true, overlong: [] };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
lines: [words.join(" ")],
|
|
78
|
+
fontPx: opts.minFont,
|
|
79
|
+
fits: false,
|
|
80
|
+
overlong: overlongAt(opts.minFont),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type FmtOpts = {
|
|
2
|
+
/**
|
|
3
|
+
* "$", "€", "£", "¥" prefix; anything else is appended ("%", " km")
|
|
4
|
+
*/
|
|
5
|
+
unit?: string;
|
|
6
|
+
/**
|
|
7
|
+
* fixed decimals (default 0; 1 when compact above 1000)
|
|
8
|
+
*/
|
|
9
|
+
decimals?: number;
|
|
10
|
+
/**
|
|
11
|
+
* show "+" for positive values
|
|
12
|
+
*/
|
|
13
|
+
sign?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* K / M / B / T suffix
|
|
16
|
+
*/
|
|
17
|
+
compact?: boolean;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* @param {number} value
|
|
21
|
+
* @param {FmtOpts} [opts]
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export declare const fmt: (value: number, opts?: FmtOpts) => string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Locale-free, deterministic number formatting for tabular numerals (no Intl, no toLocaleString).
|
|
3
|
+
const MINUS = "−";
|
|
4
|
+
const TIERS = ["", "K", "M", "B", "T"];
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} FmtOpts
|
|
7
|
+
* @property {string} [unit] "$", "€", "£", "¥" prefix; anything else is appended ("%", " km")
|
|
8
|
+
* @property {number} [decimals] fixed decimals (default 0; 1 when compact above 1000)
|
|
9
|
+
* @property {boolean} [sign] show "+" for positive values
|
|
10
|
+
* @property {boolean} [compact] K / M / B / T suffix
|
|
11
|
+
*/
|
|
12
|
+
/** @param {string} digits */
|
|
13
|
+
const group = (digits) => digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
14
|
+
/**
|
|
15
|
+
* @param {number} abs
|
|
16
|
+
* @param {number} decimals
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
const fixed = (abs, decimals) => {
|
|
20
|
+
const [int = "0", frac] = abs.toFixed(decimals).split(".");
|
|
21
|
+
return frac === undefined ? group(int) : `${group(int)}.${frac}`;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* @param {number} abs
|
|
25
|
+
* @param {number | undefined} decimals
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
const compactBody = (abs, decimals) => {
|
|
29
|
+
let tier = 0;
|
|
30
|
+
let scaled = abs;
|
|
31
|
+
while (scaled >= 1000 && tier < TIERS.length - 1) {
|
|
32
|
+
scaled /= 1000;
|
|
33
|
+
tier++;
|
|
34
|
+
}
|
|
35
|
+
const d = decimals ?? (tier === 0 ? 0 : 1);
|
|
36
|
+
// rounding can reach 1000 of this tier (999.95K -> 1000.0K): promote once
|
|
37
|
+
if (tier < TIERS.length - 1 && Number(scaled.toFixed(d)) >= 1000) {
|
|
38
|
+
scaled /= 1000;
|
|
39
|
+
tier++;
|
|
40
|
+
}
|
|
41
|
+
return `${fixed(scaled, d)}${TIERS[tier]}`;
|
|
42
|
+
};
|
|
43
|
+
/** @param {string} unit */
|
|
44
|
+
const isPrefix = (unit) => /^[$€£¥]/.test(unit);
|
|
45
|
+
/**
|
|
46
|
+
* @param {number} value
|
|
47
|
+
* @param {FmtOpts} [opts]
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export const fmt = (value, opts = {}) => {
|
|
51
|
+
if (!Number.isFinite(value))
|
|
52
|
+
throw new RangeError(`fmt: value must be finite, got ${value}`);
|
|
53
|
+
const { unit = "", decimals, sign = false, compact = false } = opts;
|
|
54
|
+
const abs = Math.abs(value);
|
|
55
|
+
const body = compact ? compactBody(abs, decimals) : fixed(abs, decimals ?? 0);
|
|
56
|
+
const zero = Number(body.replace(/[^0-9.]/g, "")) === 0;
|
|
57
|
+
const mark = zero ? "" : value < 0 ? MINUS : sign ? "+" : "";
|
|
58
|
+
return isPrefix(unit) ? `${mark}${unit}${body}` : `${mark}${body}${unit}`;
|
|
59
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EmitContext } from "@anvpx/motion-schema";
|
|
2
|
+
export type Frame = "landscape" | "portrait" | "square";
|
|
3
|
+
export declare const frameOf: (ctx: Pick<EmitContext, "width" | "height">) => Frame;
|
|
4
|
+
export interface FrameRules {
|
|
5
|
+
/** Portrait and square. */
|
|
6
|
+
readonly tall?: string;
|
|
7
|
+
readonly portrait?: string;
|
|
8
|
+
readonly square?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Emit-time layout by frame aspect. Rules are written against `#scene-root`; landscape frames get
|
|
12
|
+
* no extra CSS, so existing 16:9 output stays byte-identical.
|
|
13
|
+
*/
|
|
14
|
+
export declare const frameCss: (ctx: Pick<EmitContext, "width" | "height">, root: string, rules: FrameRules) => string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Height over width above which a frame is portrait; near 1 is square. */
|
|
2
|
+
const PORTRAIT_RATIO = 1.2;
|
|
3
|
+
const SQUARE_RATIO = 0.85;
|
|
4
|
+
export const frameOf = (ctx) => {
|
|
5
|
+
const ratio = ctx.height / ctx.width;
|
|
6
|
+
if (ratio > PORTRAIT_RATIO)
|
|
7
|
+
return "portrait";
|
|
8
|
+
return ratio >= SQUARE_RATIO ? "square" : "landscape";
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Emit-time layout by frame aspect. Rules are written against `#scene-root`; landscape frames get
|
|
12
|
+
* no extra CSS, so existing 16:9 output stays byte-identical.
|
|
13
|
+
*/
|
|
14
|
+
export const frameCss = (ctx, root, rules) => {
|
|
15
|
+
const frame = frameOf(ctx);
|
|
16
|
+
if (frame === "landscape")
|
|
17
|
+
return "";
|
|
18
|
+
const parts = [rules.tall, frame === "portrait" ? rules.portrait : rules.square];
|
|
19
|
+
const css = parts.filter((part) => part !== undefined).join("\n");
|
|
20
|
+
return css === "" ? "" : `\n/* frame: ${frame} */\n${css.replaceAll("#scene-root", root)}\n`;
|
|
21
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Names of icons whose name, keyword tags or group contain every query token (alphabetical). */
|
|
2
|
+
export declare const searchIconNames: (query: string) => string[];
|
|
3
|
+
/** Icon names of a group (`business`, `data`, ...) or carrying a keyword tag; alphabetical. */
|
|
4
|
+
export declare const listIcons: (tag: string) => string[];
|
|
5
|
+
/** Each curated group with how many icons it holds. */
|
|
6
|
+
export declare const iconGroups: () => {
|
|
7
|
+
group: string;
|
|
8
|
+
count: number;
|
|
9
|
+
}[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ICON_GROUPS, ICONS } from "@anvpx/motion-assets";
|
|
2
|
+
const groupsOf = new Map();
|
|
3
|
+
for (const [group, names] of Object.entries(ICON_GROUPS)) {
|
|
4
|
+
for (const name of names)
|
|
5
|
+
groupsOf.set(name, [...(groupsOf.get(name) ?? []), group]);
|
|
6
|
+
}
|
|
7
|
+
const words = (name, tags) => [name, ...tags, ...(groupsOf.get(name) ?? [])].join(" ").toLowerCase();
|
|
8
|
+
/** Names of icons whose name, keyword tags or group contain every query token (alphabetical). */
|
|
9
|
+
export const searchIconNames = (query) => {
|
|
10
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
11
|
+
return ICONS.filter(({ name, tags }) => {
|
|
12
|
+
const hay = words(name, tags);
|
|
13
|
+
return tokens.every((token) => hay.includes(token));
|
|
14
|
+
})
|
|
15
|
+
.map(({ name }) => name)
|
|
16
|
+
.sort();
|
|
17
|
+
};
|
|
18
|
+
/** Icon names of a group (`business`, `data`, ...) or carrying a keyword tag; alphabetical. */
|
|
19
|
+
export const listIcons = (tag) => {
|
|
20
|
+
const wanted = tag.toLowerCase();
|
|
21
|
+
return ICONS.filter(({ name, tags }) => (groupsOf.get(name) ?? []).includes(wanted) || tags.some((t) => t.toLowerCase() === wanted))
|
|
22
|
+
.map(({ name }) => name)
|
|
23
|
+
.sort();
|
|
24
|
+
};
|
|
25
|
+
/** Each curated group with how many icons it holds. */
|
|
26
|
+
export const iconGroups = () => Object.entries(ICON_GROUPS).map(([group, names]) => ({ group, count: names.length }));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { iconLedgerRows, LUCIDE_LICENCE_TEXT } from "@anvpx/motion-assets";
|
|
2
|
+
export interface SceneIconOptions {
|
|
3
|
+
/** Config pointer of the offending prop, e.g. `props.items[2].icon`. */
|
|
4
|
+
readonly at: string;
|
|
5
|
+
readonly size?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Runs `run` and returns its value with the distinct, sorted names of the icons it drew. */
|
|
8
|
+
export declare const collectIcons: <T>(run: () => T) => {
|
|
9
|
+
readonly value: T;
|
|
10
|
+
readonly icons: string[];
|
|
11
|
+
};
|
|
12
|
+
/** Schema-time check: a message with 3 suggestions for an unknown icon name, else undefined. */
|
|
13
|
+
export declare const iconProblem: (name: string) => string | undefined;
|
|
14
|
+
/** Vendored Lucide icon as a `currentColor` SVG; unknown names throw a config error with 3 suggestions. */
|
|
15
|
+
export declare const sceneIcon: (name: string, opts: SceneIconOptions) => string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { iconNameError, iconSvg } from "@anvpx/motion-assets";
|
|
2
|
+
export { iconLedgerRows, LUCIDE_LICENCE_TEXT } from "@anvpx/motion-assets";
|
|
3
|
+
/** Inline HTML svg needs no namespace; dropping it keeps fragments free of external urls. */
|
|
4
|
+
const XMLNS = ' xmlns="http://www.w3.org/2000/svg"';
|
|
5
|
+
/** One frame per active `collectIcons` run; `sceneIcon` reports to the innermost. */
|
|
6
|
+
const collecting = [];
|
|
7
|
+
/** Runs `run` and returns its value with the distinct, sorted names of the icons it drew. */
|
|
8
|
+
export const collectIcons = (run) => {
|
|
9
|
+
const used = new Set();
|
|
10
|
+
collecting.push(used);
|
|
11
|
+
try {
|
|
12
|
+
const value = run();
|
|
13
|
+
return { value, icons: [...used].sort() };
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
collecting.pop();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/** Schema-time check: a message with 3 suggestions for an unknown icon name, else undefined. */
|
|
20
|
+
export const iconProblem = (name) => iconNameError(name);
|
|
21
|
+
/** Vendored Lucide icon as a `currentColor` SVG; unknown names throw a config error with 3 suggestions. */
|
|
22
|
+
export const sceneIcon = (name, opts) => {
|
|
23
|
+
const problem = iconNameError(name);
|
|
24
|
+
if (problem !== undefined)
|
|
25
|
+
throw new Error(`config error at ${opts.at}: ${problem}`);
|
|
26
|
+
collecting.at(-1)?.add(name);
|
|
27
|
+
return iconSvg(name, opts.size).replace(XMLNS, "");
|
|
28
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type { FitTextOptions, FittedText } from "./fit-text.ts";
|
|
2
|
+
export { fitText } from "./fit-text.ts";
|
|
3
|
+
export { fmt } from "./format.js";
|
|
4
|
+
export type { Frame, FrameRules } from "./frame.ts";
|
|
5
|
+
export { frameCss, frameOf } from "./frame.ts";
|
|
6
|
+
export type { SceneIconOptions } from "./icon.ts";
|
|
7
|
+
export { collectIcons, iconLedgerRows, LUCIDE_LICENCE_TEXT, sceneIcon } from "./icon.ts";
|
|
8
|
+
export type { AccentSegment } from "./markup.ts";
|
|
9
|
+
export { accentHtml, accentSegments, esc, stripAccent } from "./markup.ts";
|
|
10
|
+
export { textWidth } from "./measure.ts";
|
|
11
|
+
export { clamp, countUp, drawOn, lerp, progress, revealProgress, revealTimings, scale, staggerDuration, staggerStart, wipe, } from "./motion.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { fitText } from "./fit-text.js";
|
|
2
|
+
export { fmt } from "./format.js";
|
|
3
|
+
export { frameCss, frameOf } from "./frame.js";
|
|
4
|
+
export { collectIcons, iconLedgerRows, LUCIDE_LICENCE_TEXT, sceneIcon } from "./icon.js";
|
|
5
|
+
export { accentHtml, accentSegments, esc, stripAccent } from "./markup.js";
|
|
6
|
+
export { textWidth } from "./measure.js";
|
|
7
|
+
export { clamp, countUp, drawOn, lerp, progress, revealProgress, revealTimings, scale, staggerDuration, staggerStart, wipe, } from "./motion.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** HTML-escape text for element content and attribute values. */
|
|
2
|
+
export declare const esc: (text: string) => string;
|
|
3
|
+
export interface AccentSegment {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly accent: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Splits `[x]` accent markup into segments; unmatched or empty brackets stay literal. */
|
|
8
|
+
export declare const accentSegments: (text: string) => AccentSegment[];
|
|
9
|
+
/** Plain text of accent markup (brackets removed). */
|
|
10
|
+
export declare const stripAccent: (text: string) => string;
|
|
11
|
+
/** Escaped HTML for accent markup: the only tags emitted are `<span class="accent">`. */
|
|
12
|
+
export declare const accentHtml: (text: string) => string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const ESCAPES = {
|
|
2
|
+
"&": "&",
|
|
3
|
+
"<": "<",
|
|
4
|
+
">": ">",
|
|
5
|
+
'"': """,
|
|
6
|
+
"'": "'",
|
|
7
|
+
};
|
|
8
|
+
/** HTML-escape text for element content and attribute values. */
|
|
9
|
+
export const esc = (text) => text.replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
|
|
10
|
+
/** Splits `[x]` accent markup into segments; unmatched or empty brackets stay literal. */
|
|
11
|
+
export const accentSegments = (text) => {
|
|
12
|
+
const segments = [];
|
|
13
|
+
let plain = "";
|
|
14
|
+
const flush = () => {
|
|
15
|
+
if (plain !== "")
|
|
16
|
+
segments.push({ text: plain, accent: false });
|
|
17
|
+
plain = "";
|
|
18
|
+
};
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (i < text.length) {
|
|
21
|
+
const close = text[i] === "[" ? text.indexOf("]", i + 1) : -1;
|
|
22
|
+
const inner = close > i + 1 ? text.slice(i + 1, close) : "";
|
|
23
|
+
if (inner !== "" && !inner.includes("[")) {
|
|
24
|
+
flush();
|
|
25
|
+
segments.push({ text: inner, accent: true });
|
|
26
|
+
i = close + 1;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
plain += text[i];
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
flush();
|
|
34
|
+
return segments;
|
|
35
|
+
};
|
|
36
|
+
/** Plain text of accent markup (brackets removed). */
|
|
37
|
+
export const stripAccent = (text) => accentSegments(text)
|
|
38
|
+
.map((s) => s.text)
|
|
39
|
+
.join("");
|
|
40
|
+
/** Escaped HTML for accent markup: the only tags emitted are `<span class="accent">`. */
|
|
41
|
+
export const accentHtml = (text) => accentSegments(text)
|
|
42
|
+
.map((s) => (s.accent ? `<span class="accent">${esc(s.text)}</span>` : esc(s.text)))
|
|
43
|
+
.join("");
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic text-width estimate for the bold sans (Inter) used by scene text.
|
|
3
|
+
* No font is loaded: glyph classes carry a width in em, generous enough that real text never
|
|
4
|
+
* ends up wider than the estimate.
|
|
5
|
+
*/
|
|
6
|
+
/** Estimated width in px of `text` set at `fontPx`. */
|
|
7
|
+
export declare const textWidth: (text: string, fontPx: number) => number;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic text-width estimate for the bold sans (Inter) used by scene text.
|
|
3
|
+
* No font is loaded: glyph classes carry a width in em, generous enough that real text never
|
|
4
|
+
* ends up wider than the estimate.
|
|
5
|
+
*/
|
|
6
|
+
const SPACE_EM = 0.28;
|
|
7
|
+
const UPPER_EM = 0.72;
|
|
8
|
+
const DIGIT_EM = 0.62;
|
|
9
|
+
const LOWER_EM = 0.6;
|
|
10
|
+
const OTHER_EM = 0.8;
|
|
11
|
+
/** Glyphs whose width differs from their class (narrow, slim and wide letters, punctuation). */
|
|
12
|
+
const SPECIAL = [
|
|
13
|
+
[" ", SPACE_EM],
|
|
14
|
+
["iljI.,:;'|!`", 0.32],
|
|
15
|
+
["frt()[]{}/\\-", 0.42],
|
|
16
|
+
["mw", 0.9],
|
|
17
|
+
["MW@%&", 1],
|
|
18
|
+
];
|
|
19
|
+
const SPECIAL_EM = new Map(SPECIAL.flatMap(([chars, em]) => [...chars].map((c) => [c, em])));
|
|
20
|
+
const classEm = (char) => {
|
|
21
|
+
if (/[A-Z]/.test(char))
|
|
22
|
+
return UPPER_EM;
|
|
23
|
+
if (/[0-9]/.test(char))
|
|
24
|
+
return DIGIT_EM;
|
|
25
|
+
return /[a-z]/.test(char) ? LOWER_EM : OTHER_EM;
|
|
26
|
+
};
|
|
27
|
+
const glyphEm = (char) => SPECIAL_EM.get(char) ?? classEm(char);
|
|
28
|
+
/** Estimated width in px of `text` set at `fontPx`. */
|
|
29
|
+
export const textWidth = (text, fontPx) => {
|
|
30
|
+
let em = 0;
|
|
31
|
+
for (const char of text)
|
|
32
|
+
em += glyphEm(char);
|
|
33
|
+
return Math.ceil(em * fontPx);
|
|
34
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Smallest / largest fraction of an item's duration shared with the next item in a stagger. */
|
|
2
|
+
export declare const MIN_OVERLAP = 0.05;
|
|
3
|
+
export declare const MAX_OVERLAP = 0.9;
|
|
4
|
+
/**
|
|
5
|
+
* @param {number} v
|
|
6
|
+
* @param {number} lo
|
|
7
|
+
* @param {number} hi
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
export declare const clamp: (v: number, lo: number, hi: number) => number;
|
|
11
|
+
/**
|
|
12
|
+
* @param {number} a
|
|
13
|
+
* @param {number} b
|
|
14
|
+
* @param {number} p
|
|
15
|
+
* @returns {number}
|
|
16
|
+
*/
|
|
17
|
+
export declare const lerp: (a: number, b: number, p: number) => number;
|
|
18
|
+
/** Linear map of `v` from [inMin, inMax] to [outMin, outMax], clamped to the input range.
|
|
19
|
+
* @param {number} v
|
|
20
|
+
* @param {number} inMin
|
|
21
|
+
* @param {number} inMax
|
|
22
|
+
* @param {number} outMin
|
|
23
|
+
* @param {number} outMax
|
|
24
|
+
* @returns {number}
|
|
25
|
+
*/
|
|
26
|
+
export declare const scale: (v: number, inMin: number, inMax: number, outMin: number, outMax: number) => number;
|
|
27
|
+
/** Monotone 0..1 progress of `t` through [start, end]; a zero-length window is a step.
|
|
28
|
+
* @param {number} t
|
|
29
|
+
* @param {number} start
|
|
30
|
+
* @param {number} end
|
|
31
|
+
* @returns {number}
|
|
32
|
+
*/
|
|
33
|
+
export declare const progress: (t: number, start: number, end: number) => number;
|
|
34
|
+
/**
|
|
35
|
+
* Duration of each item when `n` items share `span` seconds and overlap by `overlap` (a fraction of
|
|
36
|
+
* the item duration, clamped to [0.05, 0.9]). The last item ends exactly at `span`.
|
|
37
|
+
* @param {number} n
|
|
38
|
+
* @param {number} span
|
|
39
|
+
* @param {number} overlap
|
|
40
|
+
* @returns {number}
|
|
41
|
+
*/
|
|
42
|
+
export declare const staggerDuration: (n: number, span: number, overlap: number) => number;
|
|
43
|
+
/**
|
|
44
|
+
* Start time of item `i` (0-based). Offset between items is always smaller than an item's duration.
|
|
45
|
+
* @param {number} i
|
|
46
|
+
* @param {number} n
|
|
47
|
+
* @param {number} span
|
|
48
|
+
* @param {number} overlap
|
|
49
|
+
* @returns {number}
|
|
50
|
+
*/
|
|
51
|
+
export declare const staggerStart: (i: number, n: number, span: number, overlap: number) => number;
|
|
52
|
+
/**
|
|
53
|
+
* Count from `from` (default 0) to `value`, easing out; exact at the end.
|
|
54
|
+
* @param {number} t
|
|
55
|
+
* @param {number} value
|
|
56
|
+
* @param {{ from?: number, start?: number, dur?: number }} [opts]
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
export declare const countUp: (t: number, value: number, opts?: {
|
|
60
|
+
from?: number;
|
|
61
|
+
start?: number;
|
|
62
|
+
dur?: number;
|
|
63
|
+
}) => number;
|
|
64
|
+
/** Stroke-dashoffset that draws a path of length `len` on as `p` goes 0..1.
|
|
65
|
+
* @param {number} len
|
|
66
|
+
* @param {number} p
|
|
67
|
+
* @returns {number}
|
|
68
|
+
*/
|
|
69
|
+
export declare const drawOn: (len: number, p: number) => number;
|
|
70
|
+
/** Left-to-right clip-path reveal; `p` 0..1.
|
|
71
|
+
* @param {number} p
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
export declare const wipe: (p: number) => string;
|
|
75
|
+
/**
|
|
76
|
+
* Word-by-word timings: `n` words spread over `span` seconds with `overlap` between neighbours.
|
|
77
|
+
* @param {number} n
|
|
78
|
+
* @param {number} span
|
|
79
|
+
* @param {number} [overlap]
|
|
80
|
+
* @returns {{ start: number, end: number }[]}
|
|
81
|
+
*/
|
|
82
|
+
export declare const revealTimings: (n: number, span: number, overlap?: number) => {
|
|
83
|
+
start: number;
|
|
84
|
+
end: number;
|
|
85
|
+
}[];
|
|
86
|
+
/**
|
|
87
|
+
* Eased 0..1 progress of each of `n` words at time `t`.
|
|
88
|
+
* @param {number} n
|
|
89
|
+
* @param {number} t
|
|
90
|
+
* @param {number} span
|
|
91
|
+
* @param {number} [overlap]
|
|
92
|
+
* @returns {number[]}
|
|
93
|
+
*/
|
|
94
|
+
export declare const revealProgress: (n: number, t: number, span: number, overlap?: number) => number[];
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Pure motion helpers shared by scenes. No DOM, no clock, no state: the same inputs always give
|
|
3
|
+
// the same outputs. One implementation serves emit time (TS) and the browser seek runtime.
|
|
4
|
+
import { clamp01, ease } from "../runtime/easing.js";
|
|
5
|
+
/** Smallest / largest fraction of an item's duration shared with the next item in a stagger. */
|
|
6
|
+
export const MIN_OVERLAP = 0.05;
|
|
7
|
+
export const MAX_OVERLAP = 0.9;
|
|
8
|
+
/**
|
|
9
|
+
* @param {number} v
|
|
10
|
+
* @param {number} lo
|
|
11
|
+
* @param {number} hi
|
|
12
|
+
* @returns {number}
|
|
13
|
+
*/
|
|
14
|
+
export const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
15
|
+
/**
|
|
16
|
+
* @param {number} a
|
|
17
|
+
* @param {number} b
|
|
18
|
+
* @param {number} p
|
|
19
|
+
* @returns {number}
|
|
20
|
+
*/
|
|
21
|
+
export const lerp = (a, b, p) => a + (b - a) * p;
|
|
22
|
+
/** Linear map of `v` from [inMin, inMax] to [outMin, outMax], clamped to the input range.
|
|
23
|
+
* @param {number} v
|
|
24
|
+
* @param {number} inMin
|
|
25
|
+
* @param {number} inMax
|
|
26
|
+
* @param {number} outMin
|
|
27
|
+
* @param {number} outMax
|
|
28
|
+
* @returns {number}
|
|
29
|
+
*/
|
|
30
|
+
export const scale = (v, inMin, inMax, outMin, outMax) => lerp(outMin, outMax, inMax === inMin ? (v >= inMax ? 1 : 0) : clamp01((v - inMin) / (inMax - inMin)));
|
|
31
|
+
/** Monotone 0..1 progress of `t` through [start, end]; a zero-length window is a step.
|
|
32
|
+
* @param {number} t
|
|
33
|
+
* @param {number} start
|
|
34
|
+
* @param {number} end
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
export const progress = (t, start, end) => end > start ? clamp01((t - start) / (end - start)) : t >= start ? 1 : 0;
|
|
38
|
+
/**
|
|
39
|
+
* @param {number} n
|
|
40
|
+
* @param {number} span
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
const assertStagger = (n, span) => {
|
|
44
|
+
if (!Number.isInteger(n) || n < 1)
|
|
45
|
+
throw new RangeError(`stagger: n must be an integer >= 1, got ${n}`);
|
|
46
|
+
if (!(span > 0) || !Number.isFinite(span))
|
|
47
|
+
throw new RangeError(`stagger: span must be > 0, got ${span}`);
|
|
48
|
+
};
|
|
49
|
+
/** @param {number} overlap */
|
|
50
|
+
const overlapOf = (overlap) => clamp(Number.isFinite(overlap) ? overlap : 0, MIN_OVERLAP, MAX_OVERLAP);
|
|
51
|
+
/**
|
|
52
|
+
* Duration of each item when `n` items share `span` seconds and overlap by `overlap` (a fraction of
|
|
53
|
+
* the item duration, clamped to [0.05, 0.9]). The last item ends exactly at `span`.
|
|
54
|
+
* @param {number} n
|
|
55
|
+
* @param {number} span
|
|
56
|
+
* @param {number} overlap
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
export const staggerDuration = (n, span, overlap) => {
|
|
60
|
+
assertStagger(n, span);
|
|
61
|
+
return span / (1 + (n - 1) * (1 - overlapOf(overlap)));
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Start time of item `i` (0-based). Offset between items is always smaller than an item's duration.
|
|
65
|
+
* @param {number} i
|
|
66
|
+
* @param {number} n
|
|
67
|
+
* @param {number} span
|
|
68
|
+
* @param {number} overlap
|
|
69
|
+
* @returns {number}
|
|
70
|
+
*/
|
|
71
|
+
export const staggerStart = (i, n, span, overlap) => i * staggerDuration(n, span, overlap) * (1 - overlapOf(overlap));
|
|
72
|
+
/**
|
|
73
|
+
* Count from `from` (default 0) to `value`, easing out; exact at the end.
|
|
74
|
+
* @param {number} t
|
|
75
|
+
* @param {number} value
|
|
76
|
+
* @param {{ from?: number, start?: number, dur?: number }} [opts]
|
|
77
|
+
* @returns {number}
|
|
78
|
+
*/
|
|
79
|
+
export const countUp = (t, value, opts = {}) => {
|
|
80
|
+
const { from = 0, start = 0, dur = 1 } = opts;
|
|
81
|
+
const p = progress(t, start, start + dur);
|
|
82
|
+
return p >= 1 ? value : lerp(from, value, ease("out", p));
|
|
83
|
+
};
|
|
84
|
+
/** Stroke-dashoffset that draws a path of length `len` on as `p` goes 0..1.
|
|
85
|
+
* @param {number} len
|
|
86
|
+
* @param {number} p
|
|
87
|
+
* @returns {number}
|
|
88
|
+
*/
|
|
89
|
+
export const drawOn = (len, p) => len * (1 - clamp01(p));
|
|
90
|
+
/** Left-to-right clip-path reveal; `p` 0..1.
|
|
91
|
+
* @param {number} p
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
export const wipe = (p) => `inset(0 ${Math.round((1 - clamp01(p)) * 100000) / 1000}% 0 0)`;
|
|
95
|
+
/**
|
|
96
|
+
* Word-by-word timings: `n` words spread over `span` seconds with `overlap` between neighbours.
|
|
97
|
+
* @param {number} n
|
|
98
|
+
* @param {number} span
|
|
99
|
+
* @param {number} [overlap]
|
|
100
|
+
* @returns {{ start: number, end: number }[]}
|
|
101
|
+
*/
|
|
102
|
+
export const revealTimings = (n, span, overlap = 0.5) => {
|
|
103
|
+
const dur = staggerDuration(n, span, overlap);
|
|
104
|
+
return Array.from({ length: n }, (_, i) => {
|
|
105
|
+
const start = staggerStart(i, n, span, overlap);
|
|
106
|
+
return { start, end: i === n - 1 ? span : start + dur };
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Eased 0..1 progress of each of `n` words at time `t`.
|
|
111
|
+
* @param {number} n
|
|
112
|
+
* @param {number} t
|
|
113
|
+
* @param {number} span
|
|
114
|
+
* @param {number} [overlap]
|
|
115
|
+
* @returns {number[]}
|
|
116
|
+
*/
|
|
117
|
+
export const revealProgress = (n, t, span, overlap = 0.5) => revealTimings(n, span, overlap).map(({ start, end }) => ease("out", progress(t, start, end)));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** sha256 over sorted relative paths and LF-normalised contents of the shipped source files. */
|
|
2
|
+
export declare const hashSourceTree: (dir: string) => string;
|
|
3
|
+
/** Fingerprint of every scene, shared module and runtime file: edits invalidate build caches. */
|
|
4
|
+
export declare const sceneSourceHash: () => string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const SOURCE_FILE = /\.(?:ts|js|css)$/;
|
|
5
|
+
const NOT_SHIPPED = /\.test\.ts$|^test-fixtures\.ts$|^__snapshots__$/;
|
|
6
|
+
const listFiles = (dir, prefix = "") => readdirSync(dir, { withFileTypes: true })
|
|
7
|
+
.filter((item) => !NOT_SHIPPED.test(item.name))
|
|
8
|
+
.flatMap((item) => item.isDirectory()
|
|
9
|
+
? listFiles(`${dir}/${item.name}`, `${prefix}${item.name}/`)
|
|
10
|
+
: SOURCE_FILE.test(item.name)
|
|
11
|
+
? [`${prefix}${item.name}`]
|
|
12
|
+
: []);
|
|
13
|
+
/** sha256 over sorted relative paths and LF-normalised contents of the shipped source files. */
|
|
14
|
+
export const hashSourceTree = (dir) => {
|
|
15
|
+
const hash = createHash("sha256");
|
|
16
|
+
for (const file of listFiles(dir).sort()) {
|
|
17
|
+
hash.update(`${file}\0${readFileSync(`${dir}/${file}`, "utf8").replaceAll("\r\n", "\n")}\0`);
|
|
18
|
+
}
|
|
19
|
+
return hash.digest("hex");
|
|
20
|
+
};
|
|
21
|
+
let memo;
|
|
22
|
+
/** Fingerprint of every scene, shared module and runtime file: edits invalidate build caches. */
|
|
23
|
+
export const sceneSourceHash = () => {
|
|
24
|
+
memo ??= hashSourceTree(fileURLToPath(new URL("./", import.meta.url)));
|
|
25
|
+
return memo;
|
|
26
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type EmitContext, type SceneEntry } from "@anvpx/motion-schema";
|
|
2
|
+
import type { ZodType } from "zod";
|
|
3
|
+
export declare const emitContext: (overrides?: Partial<EmitContext>) => EmitContext;
|
|
4
|
+
export declare const entryOf: (type: string, props: Record<string, unknown>, extra?: Partial<SceneEntry>) => SceneEntry;
|
|
5
|
+
/** Issues as `<json-pointer>: <message>` lines. */
|
|
6
|
+
export declare const issuesOf: (schema: ZodType, value: unknown) => string[];
|
|
7
|
+
export declare const words: (count: number) => string;
|
|
8
|
+
/** Markup and quotes that must never reach the fragment unescaped. */
|
|
9
|
+
export declare const HOSTILE = "<script>alert(\"x\")</script>";
|