@anvpx/motion-build 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 +67 -2
- package/dist/.out/demo/boot.js +90 -0
- package/dist/.out/demo/runtime/easing.js +45 -0
- package/dist/.out/demo/runtime/mount.js +153 -0
- package/dist/.out/demo/runtime/timeline.js +199 -0
- package/dist/.out/demo/scenes/count/scene.js +6 -0
- package/dist/.out/demo/scenes/intro/scene.js +6 -0
- package/dist/.out/demo/scenes/outro/scene.js +6 -0
- package/dist/.out/demo/scenes/path/scene.js +6 -0
- package/dist/.out/demo/scenes/signals/scene.js +6 -0
- package/dist/.out/demo/shared/format.js +59 -0
- package/dist/.out/demo/shared/motion.js +117 -0
- package/dist/.out/demo-overlays/boot.js +95 -0
- package/dist/.out/demo-overlays/runtime/easing.js +45 -0
- package/dist/.out/demo-overlays/runtime/mount.js +153 -0
- package/dist/.out/demo-overlays/runtime/timeline.js +199 -0
- package/dist/.out/demo-overlays/scenes/chapter-signals/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/count/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/flow-progress/scene.js +17 -0
- package/dist/.out/demo-overlays/scenes/intro/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/mark/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/outro/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/path/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/presenter/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/signals/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/tick/scene.js +21 -0
- package/dist/.out/demo-overlays/shared/format.js +59 -0
- package/dist/.out/demo-overlays/shared/motion.js +117 -0
- package/dist/assets.d.ts +36 -0
- package/dist/assets.js +95 -0
- package/dist/audio/approval.d.ts +45 -0
- package/dist/audio/approval.js +82 -0
- package/dist/audio/mix.d.ts +231 -0
- package/dist/audio/mix.js +491 -0
- package/dist/audio/music.d.ts +82 -0
- package/dist/audio/music.js +134 -0
- package/dist/audio/preview.d.ts +31 -0
- package/dist/audio/preview.js +31 -0
- package/dist/build.d.ts +65 -0
- package/dist/build.js +260 -0
- package/dist/cache.d.ts +59 -0
- package/dist/cache.js +249 -0
- package/dist/emit/boot.d.ts +16 -0
- package/dist/emit/boot.js +32 -0
- package/dist/emit/boot.template.js +85 -0
- package/dist/emit/fragment-guard.d.ts +5 -0
- package/dist/emit/fragment-guard.js +15 -0
- package/dist/emit/index-html.d.ts +26 -0
- package/dist/emit/index-html.js +63 -0
- package/dist/emit/safe-json.d.ts +2 -0
- package/dist/emit/safe-json.js +7 -0
- package/dist/emit/vendor.d.ts +7 -0
- package/dist/emit/vendor.js +57 -0
- package/dist/fonts.d.ts +39 -0
- package/dist/fonts.js +112 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/integration/render.integration.d.ts +1 -0
- package/dist/integration/render.integration.js +162 -0
- package/dist/lint/custom-source.d.ts +12 -0
- package/dist/lint/custom-source.js +130 -0
- package/dist/lint/fixtures.d.ts +24 -0
- package/dist/lint/fixtures.js +42 -0
- package/dist/lint/flow.d.ts +12 -0
- package/dist/lint/flow.js +38 -0
- package/dist/lint/frame.d.ts +3 -0
- package/dist/lint/frame.js +2 -0
- package/dist/lint/hook-first.d.ts +6 -0
- package/dist/lint/hook-first.js +27 -0
- package/dist/lint/index.d.ts +25 -0
- package/dist/lint/index.js +55 -0
- package/dist/lint/layout-estimate.d.ts +18 -0
- package/dist/lint/layout-estimate.js +165 -0
- package/dist/lint/linear-ease.d.ts +2 -0
- package/dist/lint/linear-ease.js +14 -0
- package/dist/lint/long-hold.d.ts +9 -0
- package/dist/lint/long-hold.js +52 -0
- package/dist/lint/min-hold.d.ts +4 -0
- package/dist/lint/min-hold.js +10 -0
- package/dist/lint/motion-craft.d.ts +18 -0
- package/dist/lint/motion-craft.js +112 -0
- package/dist/lint/narration-pace.d.ts +6 -0
- package/dist/lint/narration-pace.js +35 -0
- package/dist/lint/overlays.d.ts +9 -0
- package/dist/lint/overlays.js +64 -0
- package/dist/lint/reading-time.d.ts +5 -0
- package/dist/lint/reading-time.js +17 -0
- package/dist/lint/safe-zone-9x16.d.ts +11 -0
- package/dist/lint/safe-zone-9x16.js +101 -0
- package/dist/lint/text-floor.d.ts +8 -0
- package/dist/lint/text-floor.js +34 -0
- package/dist/lint/transition-implemented.d.ts +3 -0
- package/dist/lint/transition-implemented.js +15 -0
- package/dist/lint/transition-length.d.ts +4 -0
- package/dist/lint/transition-length.js +10 -0
- package/dist/lint/type-grid.d.ts +30 -0
- package/dist/lint/type-grid.js +147 -0
- package/dist/lint/types.d.ts +41 -0
- package/dist/lint/types.js +1 -0
- package/dist/lint/words-on-screen.d.ts +5 -0
- package/dist/lint/words-on-screen.js +24 -0
- package/dist/manifest.d.ts +53 -0
- package/dist/manifest.js +55 -0
- package/dist/render-manifest.d.ts +21 -0
- package/dist/render-manifest.js +7 -0
- package/dist/review/review.d.ts +15 -0
- package/dist/review/review.js +56 -0
- package/dist/templates/discover-check.d.ts +11 -0
- package/dist/templates/discover-check.js +36 -0
- package/dist/templates/discover.d.ts +37 -0
- package/dist/templates/discover.js +63 -0
- package/dist/templates/expand.d.ts +17 -0
- package/dist/templates/expand.js +184 -0
- package/dist/templates/fixtures.d.ts +4 -0
- package/dist/templates/fixtures.js +63 -0
- package/dist/templates/index.d.ts +8 -0
- package/dist/templates/index.js +6 -0
- package/dist/templates/instantiate.d.ts +43 -0
- package/dist/templates/instantiate.js +160 -0
- package/dist/templates/load.d.ts +16 -0
- package/dist/templates/load.js +122 -0
- package/dist/templates/pacing.d.ts +21 -0
- package/dist/templates/pacing.js +61 -0
- package/dist/templates/search.d.ts +64 -0
- package/dist/templates/search.js +129 -0
- package/dist/templates/semver.d.ts +2 -0
- package/dist/templates/semver.js +12 -0
- package/dist/templates/slots.d.ts +8 -0
- package/dist/templates/slots.js +116 -0
- package/dist/templates/style.d.ts +25 -0
- package/dist/templates/style.js +56 -0
- package/dist/templates/types.d.ts +29 -0
- package/dist/templates/types.js +1 -0
- package/dist/templates/validate.d.ts +17 -0
- package/dist/templates/validate.js +203 -0
- package/dist/timing.d.ts +38 -0
- package/dist/timing.js +88 -0
- package/dist/validate-full.d.ts +3 -0
- package/dist/validate-full.js +132 -0
- package/dist/verify.d.ts +32 -0
- package/dist/verify.js +180 -0
- package/dist/versions.d.ts +26 -0
- package/dist/versions.js +74 -0
- package/package.json +40 -3
|
@@ -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,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,95 @@
|
|
|
1
|
+
// Emitted boot glue: the only file that knows the renderer's `hf-seek` event.
|
|
2
|
+
import { createScene as scene0 } from "./scenes/intro/scene.js";
|
|
3
|
+
import { createScene as scene1 } from "./scenes/signals/scene.js";
|
|
4
|
+
import { createScene as scene2 } from "./scenes/count/scene.js";
|
|
5
|
+
import { createScene as scene3 } from "./scenes/path/scene.js";
|
|
6
|
+
import { createScene as scene4 } from "./scenes/outro/scene.js";
|
|
7
|
+
import { createScene as scene5 } from "./scenes/presenter/scene.js";
|
|
8
|
+
import { createScene as scene6 } from "./scenes/chapter-signals/scene.js";
|
|
9
|
+
import { createScene as scene7 } from "./scenes/mark/scene.js";
|
|
10
|
+
import { createScene as scene8 } from "./scenes/tick/scene.js";
|
|
11
|
+
import { createScene as scene9 } from "./scenes/flow-progress/scene.js";
|
|
12
|
+
const FACTORIES = [scene0, scene1, scene2, scene3, scene4, scene5, scene6, scene7, scene8, scene9];
|
|
13
|
+
window.__timelines = window.__timelines || {};
|
|
14
|
+
const CONFIG = {"fps":30,"scenes":[{"clipId":"clip-intro","start":0,"duration":5,"overlap":0,"final":false,"config":{"ease":"out","move":"push-in"}},{"clipId":"clip-signals","start":4.5,"duration":10,"overlap":0.5,"final":false,"config":{"ease":"out"}},{"clipId":"clip-count","start":14.5,"duration":6,"overlap":0,"final":false,"config":{"ease":"in-out"}},{"clipId":"clip-path","start":20,"duration":8,"overlap":0.5,"final":false,"config":{"ease":"out"}},{"clipId":"clip-outro","start":27.5,"duration":5,"overlap":0.5,"final":true,"config":{"ease":"out","move":"pull-out"}},{"clipId":"clip-presenter","start":1.5,"duration":3,"overlap":0,"final":false,"config":{"ease":"out"}},{"clipId":"clip-chapter-signals","start":5,"duration":4,"overlap":0,"final":false,"config":{"ease":"out"}},{"clipId":"clip-mark","start":14.5,"duration":6,"overlap":0,"final":false,"config":{"ease":"out"}},{"clipId":"clip-tick","start":15.5,"duration":3,"overlap":0,"final":false,"config":{"ease":"out"}},{"clipId":"clip-flow-progress","start":20,"duration":8,"overlap":0,"final":false,"config":{"ease":"out"}}]};
|
|
15
|
+
|
|
16
|
+
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
17
|
+
|
|
18
|
+
const ease = (p) => 1 - (1 - p) ** 3;
|
|
19
|
+
|
|
20
|
+
/** Eased progress 0..1 of a scene entering over its predecessor (1 when no overlap transition). */
|
|
21
|
+
const transOf = (t, scene) =>
|
|
22
|
+
scene?.overlap > 0 && (scene.push || scene.wipe || scene.zoomThrough)
|
|
23
|
+
? ease(clamp((t - scene.start) / scene.overlap, 0, 1))
|
|
24
|
+
: 1;
|
|
25
|
+
|
|
26
|
+
/** Push progress 0..1 of a scene entering over its predecessor (1 when it is not a push). */
|
|
27
|
+
const pushOf = (t, scene) => (scene?.push && scene.overlap > 0 ? transOf(t, scene) : 1);
|
|
28
|
+
|
|
29
|
+
/** Pure function of global time `t`: local scene time, visibility and fade opacity. */
|
|
30
|
+
const stateAt = (t, scene) => {
|
|
31
|
+
const end = scene.start + scene.duration;
|
|
32
|
+
const live = t >= scene.start && (t < end || (scene.final && t <= end));
|
|
33
|
+
const fade = scene.overlap > 0 ? clamp((t - scene.start) / scene.overlap, 0, 1) : 1;
|
|
34
|
+
return {
|
|
35
|
+
local: clamp(t - scene.start, 0, scene.duration),
|
|
36
|
+
visible: live,
|
|
37
|
+
opacity: live ? (scene.push || scene.wipe ? 1 : fade) : 0,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const mounts = CONFIG.scenes.map((scene, index) => {
|
|
42
|
+
const clip = document.getElementById(scene.clipId);
|
|
43
|
+
const ctx = { fps: CONFIG.fps, duration: scene.duration };
|
|
44
|
+
const runtime = FACTORIES[index](clip.firstElementChild, scene.config, ctx);
|
|
45
|
+
return { scene, clip, runtime };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const ready = Promise.all([document.fonts.ready, ...mounts.map((mount) => mount.runtime.ready)]);
|
|
49
|
+
|
|
50
|
+
/** Horizontal offset in percent: an entering push scene starts one frame to the right, the one it replaces leaves to the left. */
|
|
51
|
+
const offsetOf = (t, scene, next) => {
|
|
52
|
+
if (scene.push) return (1 - pushOf(t, scene)) * 100;
|
|
53
|
+
return next?.push ? -pushOf(t, next) * 100 : null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Scale punch of a zoom-through: incoming shrinks 1.25 -> 1, outgoing grows 1 -> 1.12. */
|
|
57
|
+
const ZOOM_IN_FROM = 1.25;
|
|
58
|
+
const ZOOM_OUT_TO = 1.12;
|
|
59
|
+
|
|
60
|
+
const scaleOf = (t, scene, next) => {
|
|
61
|
+
if (scene.zoomThrough) return ZOOM_IN_FROM - (ZOOM_IN_FROM - 1) * transOf(t, scene);
|
|
62
|
+
if (next?.zoomThrough) return 1 + (ZOOM_OUT_TO - 1) * transOf(t, next);
|
|
63
|
+
return null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Directional reveal of a wipe: incoming clip opens left -> right. */
|
|
67
|
+
const clipOf = (t, scene) => {
|
|
68
|
+
if (!scene.wipe) return null;
|
|
69
|
+
const p = transOf(t, scene);
|
|
70
|
+
if (p >= 1) return "";
|
|
71
|
+
return `inset(0 ${(1 - p) * 100}% 0 0)`;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const apply = (t) =>
|
|
75
|
+
mounts.map(({ scene, clip, runtime }, index) => {
|
|
76
|
+
const state = stateAt(t, scene);
|
|
77
|
+
clip.style.visibility = state.visible ? "visible" : "hidden";
|
|
78
|
+
clip.style.opacity = String(state.opacity);
|
|
79
|
+
const next = CONFIG.scenes[index + 1];
|
|
80
|
+
const offset = offsetOf(t, scene, next);
|
|
81
|
+
if (offset !== null) clip.style.transform = `translateX(${offset}%)`;
|
|
82
|
+
else {
|
|
83
|
+
const scale = scaleOf(t, scene, next);
|
|
84
|
+
if (scale !== null) clip.style.transform = `scale(${scale})`;
|
|
85
|
+
}
|
|
86
|
+
const clipPath = clipOf(t, scene);
|
|
87
|
+
if (clipPath !== null) clip.style.clipPath = clipPath;
|
|
88
|
+
return runtime.seek(state.local);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
window.addEventListener("hf-seek", (event) => {
|
|
92
|
+
event.detail.waitUntil(Promise.all([ready, ...apply(event.detail.time)]));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
apply(0);
|
|
@@ -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,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,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,6 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { mountOverlay } from "../../runtime/mount.js";
|
|
3
|
+
/** @type {import("../../runtime/mount.js").CreateScene} */
|
|
4
|
+
export function createScene(root, config, ctx) {
|
|
5
|
+
return mountOverlay(root, config, ctx, { baseDelay: 0.1, staggerFrames: 4, rise: 24 });
|
|
6
|
+
}
|