@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 anvpx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @anvpx/motion-build
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Compiler: `storyboard.json` -> HyperFrames project. Depends only on `schema`, `themes`, `scenes`.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const result = buildProject({ storyboard, outDir, assetsDir?, ledger?, timing? });
|
|
7
|
+
// { ok: true, outDir, files, manifest, licences, warnings } | { ok: false, errors }
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Pipeline
|
|
11
|
+
|
|
12
|
+
validate (schema + each scene's `configSchema`) -> theme -> timing -> lint -> assets -> emit -> write.
|
|
13
|
+
Output: `index.html` (root composition, CSP, theme vars, scenes inline in `.clip` wrappers),
|
|
14
|
+
`boot.js` (the only file that knows `hf-seek`; initialises `window.__timelines`), `runtime/*`, `scenes/<id>/{fragment.html,style.css,scene.js}`,
|
|
15
|
+
`assets/*`, `licences.json`, `LICENSE-lucide.txt` (when an icon is used), `manifest.json` (storyboard sha256, package + hyperframes versions, per-file sha256, used `icons`, no timestamps).
|
|
16
|
+
Output is byte-deterministic. Fragments containing `<script` or `on*=` are rejected (`emit/fragment-guard.ts`). CSP keeps `script-src 'unsafe-inline'` (Constitution D1).
|
|
17
|
+
|
|
18
|
+
## Incremental cache (`cacheDir`)
|
|
19
|
+
|
|
20
|
+
`buildProject({ ..., cacheDir })` caches each scene's emitted files (`fragment.html`, `style.css`, `scene.js`) under
|
|
21
|
+
`cacheDir`; `index.html`, `boot.js` and `manifest.json` are always regenerated, so output is byte-identical to an uncached
|
|
22
|
+
build and `manifest.json` carries no cache data. Result gains `cache: { hits, misses }`. Key = sha256 of canonical JSON of
|
|
23
|
+
scene entry (type, props, `over`), placement (fps, size, duration, start, total), theme hash, hashes of the assets the
|
|
24
|
+
scene referenced, package versions, hyperframes pin and node major. Files are hex key names, written tmp + rename;
|
|
25
|
+
a corrupt, tampered or unwritable entry is a miss, never an error. `clearCache(dir)` removes cache files only.
|
|
26
|
+
Caveat: the key uses package versions, so after editing scene plugin code bump the version or run `mk build --clean-cache`.
|
|
27
|
+
|
|
28
|
+
## Timing and lint
|
|
29
|
+
|
|
30
|
+
Duration = `entry.duration` ?? `max(3, ceil(words/3 + 1.5))`; a voice `TimingManifest` overrides by scene id.
|
|
31
|
+
`transition` is the transition INTO the scene (ignored on the first). `crossfade` overlaps the previous scene
|
|
32
|
+
by 0.5 s; other values warn `transition X not implemented, using cut`.
|
|
33
|
+
Lint errors block the build: hold >= 2 s, reading time (<= 180 wpm), transition <= 0.8 s. Words <= 40 are
|
|
34
|
+
enforced by scene schemas. One rule per file in `src/lint/`. Flow layout rules (`src/lint/flow.ts`, from the scene's own layout): a word wider than its node (error, "would break mid-word"), an edge label overlapping a node or another label (error) or a crossing edge (warning), and node text that does not fit at 28 px (error: fewer nodes, shorter labels, or `direction: "tb"`).
|
|
35
|
+
|
|
36
|
+
## Overlays
|
|
37
|
+
|
|
38
|
+
`layer: "overlay"` entries (`over` = base scene id, `offset` seconds from its start, own `duration`) do not advance
|
|
39
|
+
the base timeline. `resolveTimeline` returns `scenes` (base; the crossfade/overlap rule applies only to these) and
|
|
40
|
+
`overlays` (absolute `start`/`end`). Overlay clips are emitted after all base clips in declared order (later = higher
|
|
41
|
+
z-order), each with its own fragment/style/scene.js; `boot.js` shows one only inside its window. `manifest.json` gains
|
|
42
|
+
`overlays: [{id, over, start, end}]` when any exist. Lint (`src/lint/overlays.ts`): `over` must be an existing base
|
|
43
|
+
scene (error), overlay must end by the base scene end (error), 3+ simultaneous overlays (warning). Overlay-only scene
|
|
44
|
+
types need `layer: "overlay"` and base types cannot be overlays (validate-full). Voice timing covers base scenes only.
|
|
45
|
+
Fixture: `fixtures/demo-overlays.storyboard.json` (all five overlay scenes on the pricing demo).
|
|
46
|
+
|
|
47
|
+
## Error contract
|
|
48
|
+
|
|
49
|
+
Every error is `{ path, message, suggestion? }`; `path` is an RFC 6901 pointer, e.g.
|
|
50
|
+
`/scenes/1/props/items/0: must not be empty`, `/scenes/0/type: unknown scene type "x" (use one of: ...)`.
|
|
51
|
+
|
|
52
|
+
Assets: refs are relative to `assetsDir`; absolute paths, `..`, URLs and symlink escapes are rejected. Every
|
|
53
|
+
referenced image needs a ledger row (licence, sourceUrl, sha256 matching the file; attribution when the licence needs it):
|
|
54
|
+
without a `ledger` (CLI `--ledger <assets.json>`) or without that row the build fails with `no ledger row` and writes nothing.
|
|
55
|
+
|
|
56
|
+
Licences: `licences.json` has one row per font file, per distinct inlined Lucide icon (`icons/svg/<name>.svg`; ISC, or
|
|
57
|
+
ISC AND MIT for the Feather-derived icons) and per copied image (`assets/<ref>`), so it covers every font, icon and
|
|
58
|
+
image in the output. Scenes report the icons they draw (also on a cache hit); `LICENSE-lucide.txt` is emitted whenever
|
|
59
|
+
an icon is used.
|
|
60
|
+
|
|
61
|
+
## Integration (real HyperFrames, not in the fast run)
|
|
62
|
+
|
|
63
|
+
`pnpm test:integration` builds `fixtures/demo.storyboard.json` into `dist/.out/demo`, runs `hyperframes check`
|
|
64
|
+
(exit 0), snapshots one frame per scene, renders a 15 fps draft twice and compares md5. A second case builds
|
|
65
|
+
`demo-overlays`, checks it, snapshots overlay windows (must differ from the base-only build; a quiet frame must match)
|
|
66
|
+
and verifies a 15 fps draft with `verifyMedia`.
|
|
67
|
+
Needs Chrome: `npx hyperframes browser ensure`. Fonts are bundled offline (`@fontsource/inter` 5.3.0: `fonts/inter-latin-{400,600,700,800}.woff2`, `fonts.css`, `LICENSE.txt`, OFL-1.1 rows in `licences.json`); `boot.js` also waits on `document.fonts.ready`. No network at render time.
|
|
68
|
+
Outputs live in `dist/.out/` (git-ignored; biome ignores `dist`, so generated files are not linted).
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
const FACTORIES = [scene0, scene1, scene2, scene3, scene4];
|
|
8
|
+
window.__timelines = window.__timelines || {};
|
|
9
|
+
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"}}]};
|
|
10
|
+
|
|
11
|
+
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
12
|
+
|
|
13
|
+
const ease = (p) => 1 - (1 - p) ** 3;
|
|
14
|
+
|
|
15
|
+
/** Eased progress 0..1 of a scene entering over its predecessor (1 when no overlap transition). */
|
|
16
|
+
const transOf = (t, scene) =>
|
|
17
|
+
scene?.overlap > 0 && (scene.push || scene.wipe || scene.zoomThrough)
|
|
18
|
+
? ease(clamp((t - scene.start) / scene.overlap, 0, 1))
|
|
19
|
+
: 1;
|
|
20
|
+
|
|
21
|
+
/** Push progress 0..1 of a scene entering over its predecessor (1 when it is not a push). */
|
|
22
|
+
const pushOf = (t, scene) => (scene?.push && scene.overlap > 0 ? transOf(t, scene) : 1);
|
|
23
|
+
|
|
24
|
+
/** Pure function of global time `t`: local scene time, visibility and fade opacity. */
|
|
25
|
+
const stateAt = (t, scene) => {
|
|
26
|
+
const end = scene.start + scene.duration;
|
|
27
|
+
const live = t >= scene.start && (t < end || (scene.final && t <= end));
|
|
28
|
+
const fade = scene.overlap > 0 ? clamp((t - scene.start) / scene.overlap, 0, 1) : 1;
|
|
29
|
+
return {
|
|
30
|
+
local: clamp(t - scene.start, 0, scene.duration),
|
|
31
|
+
visible: live,
|
|
32
|
+
opacity: live ? (scene.push || scene.wipe ? 1 : fade) : 0,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const mounts = CONFIG.scenes.map((scene, index) => {
|
|
37
|
+
const clip = document.getElementById(scene.clipId);
|
|
38
|
+
const ctx = { fps: CONFIG.fps, duration: scene.duration };
|
|
39
|
+
const runtime = FACTORIES[index](clip.firstElementChild, scene.config, ctx);
|
|
40
|
+
return { scene, clip, runtime };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const ready = Promise.all([document.fonts.ready, ...mounts.map((mount) => mount.runtime.ready)]);
|
|
44
|
+
|
|
45
|
+
/** Horizontal offset in percent: an entering push scene starts one frame to the right, the one it replaces leaves to the left. */
|
|
46
|
+
const offsetOf = (t, scene, next) => {
|
|
47
|
+
if (scene.push) return (1 - pushOf(t, scene)) * 100;
|
|
48
|
+
return next?.push ? -pushOf(t, next) * 100 : null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Scale punch of a zoom-through: incoming shrinks 1.25 -> 1, outgoing grows 1 -> 1.12. */
|
|
52
|
+
const ZOOM_IN_FROM = 1.25;
|
|
53
|
+
const ZOOM_OUT_TO = 1.12;
|
|
54
|
+
|
|
55
|
+
const scaleOf = (t, scene, next) => {
|
|
56
|
+
if (scene.zoomThrough) return ZOOM_IN_FROM - (ZOOM_IN_FROM - 1) * transOf(t, scene);
|
|
57
|
+
if (next?.zoomThrough) return 1 + (ZOOM_OUT_TO - 1) * transOf(t, next);
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Directional reveal of a wipe: incoming clip opens left -> right. */
|
|
62
|
+
const clipOf = (t, scene) => {
|
|
63
|
+
if (!scene.wipe) return null;
|
|
64
|
+
const p = transOf(t, scene);
|
|
65
|
+
if (p >= 1) return "";
|
|
66
|
+
return `inset(0 ${(1 - p) * 100}% 0 0)`;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const apply = (t) =>
|
|
70
|
+
mounts.map(({ scene, clip, runtime }, index) => {
|
|
71
|
+
const state = stateAt(t, scene);
|
|
72
|
+
clip.style.visibility = state.visible ? "visible" : "hidden";
|
|
73
|
+
clip.style.opacity = String(state.opacity);
|
|
74
|
+
const next = CONFIG.scenes[index + 1];
|
|
75
|
+
const offset = offsetOf(t, scene, next);
|
|
76
|
+
if (offset !== null) clip.style.transform = `translateX(${offset}%)`;
|
|
77
|
+
else {
|
|
78
|
+
const scale = scaleOf(t, scene, next);
|
|
79
|
+
if (scale !== null) clip.style.transform = `scale(${scale})`;
|
|
80
|
+
}
|
|
81
|
+
const clipPath = clipOf(t, scene);
|
|
82
|
+
if (clipPath !== null) clip.style.clipPath = clipPath;
|
|
83
|
+
return runtime.seek(state.local);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
window.addEventListener("hf-seek", (event) => {
|
|
87
|
+
event.detail.waitUntil(Promise.all([ready, ...apply(event.detail.time)]));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
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 { mountTimeline } from "../../runtime/mount.js";
|
|
3
|
+
/** @type {import("../../runtime/mount.js").CreateScene} */
|
|
4
|
+
export function createScene(root, config, ctx) {
|
|
5
|
+
return mountTimeline(root, config, ctx, { baseDelay: 0.2, staggerFrames: 4, rise: 56 });
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { mountTimeline } from "../../runtime/mount.js";
|
|
3
|
+
/** @type {import("../../runtime/mount.js").CreateScene} */
|
|
4
|
+
export function createScene(root, config, ctx) {
|
|
5
|
+
return mountTimeline(root, config, ctx, { baseDelay: 0.3, staggerFrames: 6, rise: 48 });
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { mountTimeline } from "../../runtime/mount.js";
|
|
3
|
+
/** @type {import("../../runtime/mount.js").CreateScene} */
|
|
4
|
+
export function createScene(root, config, ctx) {
|
|
5
|
+
return mountTimeline(root, config, ctx, { baseDelay: 0.3, staggerFrames: 6, rise: 32 });
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { mountTimeline } from "../../runtime/mount.js";
|
|
3
|
+
/** @type {import("../../runtime/mount.js").CreateScene} */
|
|
4
|
+
export function createScene(root, config, ctx) {
|
|
5
|
+
return mountTimeline(root, config, ctx, { baseDelay: 0.2, staggerFrames: 6, rise: 24 });
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { mountTimeline } from "../../runtime/mount.js";
|
|
3
|
+
/** @type {import("../../runtime/mount.js").CreateScene} */
|
|
4
|
+
export function createScene(root, config, ctx) {
|
|
5
|
+
return mountTimeline(root, config, ctx, { baseDelay: 0.2, staggerFrames: 5, rise: 32 });
|
|
6
|
+
}
|