@maravilla-labs/frames 0.1.0 → 0.2.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/README.md +112 -69
- package/dist/index.d.ts +27 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +82 -14
package/README.md
CHANGED
|
@@ -1,92 +1,135 @@
|
|
|
1
1
|
# @maravilla-labs/frames
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Declare a timeline of animations and video playback for a page. The same
|
|
4
|
+
declaration works two ways:
|
|
5
5
|
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
paused and the Rust renderer steps `currentTime` frame-by-frame via
|
|
10
|
-
`window.__mvFrames.applyState(t)`, producing a deterministic MP4.
|
|
6
|
+
- In a normal browser, each item plays at its scheduled time on its own.
|
|
7
|
+
- When the page is rendered to video by the Maravilla runtime, the
|
|
8
|
+
timeline is stepped frame by frame to produce a deterministic clip.
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
Zero dependencies. Bring your own animation library if you want one;
|
|
11
|
+
nothing is required.
|
|
14
12
|
|
|
15
|
-
##
|
|
13
|
+
## Install
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Because the animation has `fill: "both"`, setting `currentTime` to any
|
|
21
|
-
value in `[0, duration]` deterministically positions every animated
|
|
22
|
-
property — no implicit clock involved.
|
|
23
|
-
|
|
24
|
-
The renderer signals its presence by setting
|
|
25
|
-
`window.__mvFramesRendererPresent = true` **before** navigating to the
|
|
26
|
-
page. `defineTimeline` reads that flag at call time:
|
|
27
|
-
|
|
28
|
-
- flag set ⇒ each animation is paused immediately; the renderer drives
|
|
29
|
-
state via `applyState(t)`.
|
|
30
|
-
- flag unset ⇒ animations play normally, so humans visiting the page
|
|
31
|
-
can preview what the renderer will capture.
|
|
15
|
+
```sh
|
|
16
|
+
pnpm add @maravilla-labs/frames
|
|
17
|
+
```
|
|
32
18
|
|
|
33
19
|
## Usage
|
|
34
20
|
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
21
|
+
```ts
|
|
22
|
+
import { defineTimeline } from "@maravilla-labs/frames";
|
|
23
|
+
|
|
24
|
+
defineTimeline({
|
|
25
|
+
duration: 5000, // total timeline length, ms
|
|
26
|
+
|
|
27
|
+
instructions: [
|
|
28
|
+
// Title fades in from t=0 over the first 500 ms.
|
|
29
|
+
{
|
|
30
|
+
at: 0,
|
|
31
|
+
duration: 500,
|
|
32
|
+
selector: "h1",
|
|
33
|
+
props: [{ opacity: 0 }, { opacity: 1 }],
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Subtitle slides up, starting 300 ms after the title begins.
|
|
37
|
+
{
|
|
38
|
+
at: 300,
|
|
39
|
+
duration: 600,
|
|
40
|
+
selector: ".subtitle",
|
|
41
|
+
props: [
|
|
42
|
+
{ opacity: 0, transform: "translateY(20px)" },
|
|
43
|
+
{ opacity: 1, transform: "translateY(0)" },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// A clip plays from t=2000 ms until t=5000 ms (3 s of video).
|
|
48
|
+
{
|
|
49
|
+
kind: "video",
|
|
50
|
+
at: 2000,
|
|
51
|
+
duration: 3000,
|
|
52
|
+
selector: "video.intro",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
52
56
|
```
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
Call `defineTimeline` once, after the elements referenced by every
|
|
59
|
+
`selector` exist in the DOM.
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
## How `at` and `duration` interact
|
|
62
|
+
|
|
63
|
+
| Field | Meaning |
|
|
64
|
+
| ---------- | ------------------------------------------------------------------ |
|
|
65
|
+
| `at` | When on the timeline this item starts, in ms |
|
|
66
|
+
| `duration` | How long this item runs from `at`, in ms. Omit for sensible default |
|
|
67
|
+
|
|
68
|
+
Two animations with the same `at` start at the same time. Two
|
|
69
|
+
animations with different `at` values start at different times. A
|
|
70
|
+
`duration` of `500` ms means the animation reaches its end keyframe
|
|
71
|
+
500 ms after it started, regardless of how long the total timeline is.
|
|
72
|
+
|
|
73
|
+
If you omit `duration` on an animation, it runs from `at` until the
|
|
74
|
+
end of the timeline. If you omit `duration` on a video, it plays for
|
|
75
|
+
its natural length.
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
the rendered pixels — see the runtime plan for the full pipeline.
|
|
77
|
+
## Animations
|
|
62
78
|
|
|
63
|
-
|
|
79
|
+
Each animation instruction maps to a single
|
|
80
|
+
[`Animation`](https://developer.mozilla.org/en-US/docs/Web/API/Animation)
|
|
81
|
+
created via `Element.animate(keyframes, { duration, fill: "both" })`.
|
|
82
|
+
Anything the Web Animations API supports as a keyframe is valid in
|
|
83
|
+
`props`:
|
|
64
84
|
|
|
65
|
-
|
|
85
|
+
```ts
|
|
86
|
+
{
|
|
87
|
+
at: 1000,
|
|
88
|
+
duration: 800,
|
|
89
|
+
selector: ".card",
|
|
90
|
+
props: [
|
|
91
|
+
{ transform: "scale(0.9)", opacity: 0 },
|
|
92
|
+
{ transform: "scale(1)", opacity: 1 },
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
```
|
|
66
96
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
97
|
+
## Video
|
|
98
|
+
|
|
99
|
+
Use `kind: "video"` to schedule a `<video>` element on the timeline.
|
|
100
|
+
Optionally seek inside the source file with `seek`:
|
|
70
101
|
|
|
71
102
|
```ts
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
{
|
|
104
|
+
kind: "video",
|
|
105
|
+
at: 4000, // start playing at t=4 s on the timeline
|
|
106
|
+
duration: 6000, // play for 6 s
|
|
107
|
+
seek: 12000, // start 12 s into the source file
|
|
108
|
+
selector: "video#hero",
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The `<video>` element itself stays as you'd write it in HTML — set
|
|
113
|
+
`src`, `preload="auto"`, and `muted` so the browser autoplay policy
|
|
114
|
+
doesn't block it.
|
|
115
|
+
|
|
116
|
+
```html
|
|
117
|
+
<video class="intro" src="/clips/intro.mp4" preload="auto" muted playsinline></video>
|
|
81
118
|
```
|
|
82
119
|
|
|
83
|
-
|
|
84
|
-
|
|
120
|
+
If the same composition will be rendered to a final video by the
|
|
121
|
+
Maravilla runtime, the audio track of the embedded `<video>` is not
|
|
122
|
+
captured (browsers don't expose decoded audio to the rendering API).
|
|
123
|
+
Use the `audio` option on `FRAMES.render(...)` to mix a soundtrack
|
|
124
|
+
into the final output instead.
|
|
85
125
|
|
|
86
126
|
## Bring your own animation library
|
|
87
127
|
|
|
88
|
-
Any library that exposes an explicit time value works
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
128
|
+
Any library that exposes an explicit time value works alongside this
|
|
129
|
+
helper. Drive its time cursor inside the same applyState-equivalent
|
|
130
|
+
hook and the renderer samples it deterministically. GSAP, Lottie, and
|
|
131
|
+
Three.js all fit this shape.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @maravilla-labs/frames — declare
|
|
3
|
-
*
|
|
2
|
+
* @maravilla-labs/frames — declare a timeline of animations and video
|
|
3
|
+
* playback for a page. The same declaration works two ways:
|
|
4
4
|
*
|
|
5
|
-
* In a normal browser
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* them via `window.__mvFrames.applyState(t)` for deterministic output.
|
|
5
|
+
* - In a normal browser, each item plays at its scheduled `at` time.
|
|
6
|
+
* - Inside the Maravilla renderer, the timeline is stepped frame by
|
|
7
|
+
* frame to produce deterministic video output.
|
|
9
8
|
*/
|
|
10
|
-
export type
|
|
9
|
+
export type AnimationInstr = {
|
|
10
|
+
/** Optional discriminator. Defaults to `"animation"`. */
|
|
11
|
+
kind?: "animation";
|
|
12
|
+
/** When on the timeline this animation starts, in milliseconds. */
|
|
11
13
|
at: number;
|
|
14
|
+
/** How long the animation runs from `at`, in ms. Defaults to (schema.duration - at). */
|
|
15
|
+
duration?: number;
|
|
16
|
+
/** CSS selector for the element being animated. */
|
|
12
17
|
selector: string;
|
|
18
|
+
/** Web Animations API keyframes, e.g. `[{ opacity: 0 }, { opacity: 1 }]`. */
|
|
13
19
|
props: Keyframe[];
|
|
14
20
|
};
|
|
21
|
+
export type VideoInstr = {
|
|
22
|
+
/** Discriminator — required for video instructions. */
|
|
23
|
+
kind: "video";
|
|
24
|
+
/** When on the timeline the video starts, in milliseconds. */
|
|
25
|
+
at: number;
|
|
26
|
+
/** How long the clip plays, in ms. Defaults to the video's natural duration. */
|
|
27
|
+
duration?: number;
|
|
28
|
+
/** Start offset WITHIN the video file, in ms. Defaults to 0. */
|
|
29
|
+
seek?: number;
|
|
30
|
+
/** CSS selector for the `<video>` element. */
|
|
31
|
+
selector: string;
|
|
32
|
+
};
|
|
33
|
+
export type Instr = AnimationInstr | VideoInstr;
|
|
15
34
|
export type TimelineSchema = {
|
|
16
35
|
duration: number;
|
|
17
|
-
instructions:
|
|
36
|
+
instructions: Instr[];
|
|
18
37
|
};
|
|
19
38
|
declare global {
|
|
20
39
|
interface Window {
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,yDAAyD;IACzD,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,mEAAmE;IACnE,EAAE,EAAE,MAAM,CAAC;IACX,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,uDAAuD;IACvD,IAAI,EAAE,OAAO,CAAC;IACd,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC;IACX,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG,cAAc,GAAG,UAAU,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,KAAK,EAAE,CAAA;CAAE,CAAC;AAEzE,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,UAAU,EAAE;YAAE,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;SAAE,CAAC;QACrD,yBAAyB,CAAC,EAAE,OAAO,CAAC;KACrC;CACF;AAMD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAqD3D"}
|
package/dist/index.js
CHANGED
|
@@ -1,29 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @maravilla-labs/frames — declare
|
|
3
|
-
*
|
|
2
|
+
* @maravilla-labs/frames — declare a timeline of animations and video
|
|
3
|
+
* playback for a page. The same declaration works two ways:
|
|
4
4
|
*
|
|
5
|
-
* In a normal browser
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* them via `window.__mvFrames.applyState(t)` for deterministic output.
|
|
5
|
+
* - In a normal browser, each item plays at its scheduled `at` time.
|
|
6
|
+
* - Inside the Maravilla renderer, the timeline is stepped frame by
|
|
7
|
+
* frame to produce deterministic video output.
|
|
9
8
|
*/
|
|
10
9
|
export function defineTimeline(schema) {
|
|
11
10
|
const rendererPresent = typeof window !== "undefined" && window.__mvFramesRendererPresent === true;
|
|
12
|
-
const
|
|
11
|
+
const slots = schema.instructions.map((i) => {
|
|
13
12
|
const el = document.querySelector(i.selector);
|
|
14
13
|
if (!el)
|
|
15
14
|
throw new Error(`defineTimeline: no element matches ${i.selector}`);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
if (i.kind === "video") {
|
|
16
|
+
if (!(el instanceof HTMLVideoElement)) {
|
|
17
|
+
throw new Error(`defineTimeline: ${i.selector} is not a <video> element`);
|
|
18
|
+
}
|
|
19
|
+
const seek = i.seek ?? 0;
|
|
20
|
+
const naturalMs = isFinite(el.duration) ? el.duration * 1000 : Infinity;
|
|
21
|
+
const duration = i.duration ?? Math.min(naturalMs, schema.duration - i.at);
|
|
22
|
+
el.pause();
|
|
23
|
+
el.currentTime = seek / 1000;
|
|
24
|
+
return { kind: "video", at: i.at, duration, seek, el };
|
|
25
|
+
}
|
|
26
|
+
const duration = i.duration ?? Math.max(0, schema.duration - i.at);
|
|
27
|
+
const anim = el.animate(i.props, { duration, fill: "both" });
|
|
28
|
+
anim.pause();
|
|
29
|
+
return { kind: "animation", at: i.at, duration, anim };
|
|
20
30
|
});
|
|
21
31
|
window.__mvFrames = {
|
|
22
32
|
async applyState(t) {
|
|
23
|
-
for (const
|
|
24
|
-
|
|
33
|
+
for (const s of slots) {
|
|
34
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
35
|
+
if (s.kind === "video") {
|
|
36
|
+
s.el.pause();
|
|
37
|
+
s.el.currentTime = (s.seek + local) / 1000;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
s.anim.currentTime = local;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
25
43
|
await document.fonts.ready;
|
|
26
44
|
},
|
|
27
45
|
};
|
|
46
|
+
if (!rendererPresent) {
|
|
47
|
+
for (const s of slots) {
|
|
48
|
+
window.setTimeout(() => {
|
|
49
|
+
if (s.kind === "video") {
|
|
50
|
+
s.el.currentTime = s.seek / 1000;
|
|
51
|
+
void s.el.play();
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
s.anim.play();
|
|
55
|
+
}
|
|
56
|
+
}, s.at);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function clamp(v, lo, hi) {
|
|
61
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
28
62
|
}
|
|
29
63
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maravilla-labs/frames",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Tiny vanilla helper for declaring Web Animations API timelines that the Maravilla runtime renderer drives frame-accurately.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @maravilla-labs/frames — declare
|
|
3
|
-
*
|
|
2
|
+
* @maravilla-labs/frames — declare a timeline of animations and video
|
|
3
|
+
* playback for a page. The same declaration works two ways:
|
|
4
4
|
*
|
|
5
|
-
* In a normal browser
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* them via `window.__mvFrames.applyState(t)` for deterministic output.
|
|
5
|
+
* - In a normal browser, each item plays at its scheduled `at` time.
|
|
6
|
+
* - Inside the Maravilla renderer, the timeline is stepped frame by
|
|
7
|
+
* frame to produce deterministic video output.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
|
-
export type
|
|
12
|
-
|
|
10
|
+
export type AnimationInstr = {
|
|
11
|
+
/** Optional discriminator. Defaults to `"animation"`. */
|
|
12
|
+
kind?: "animation";
|
|
13
|
+
/** When on the timeline this animation starts, in milliseconds. */
|
|
14
|
+
at: number;
|
|
15
|
+
/** How long the animation runs from `at`, in ms. Defaults to (schema.duration - at). */
|
|
16
|
+
duration?: number;
|
|
17
|
+
/** CSS selector for the element being animated. */
|
|
18
|
+
selector: string;
|
|
19
|
+
/** Web Animations API keyframes, e.g. `[{ opacity: 0 }, { opacity: 1 }]`. */
|
|
20
|
+
props: Keyframe[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type VideoInstr = {
|
|
24
|
+
/** Discriminator — required for video instructions. */
|
|
25
|
+
kind: "video";
|
|
26
|
+
/** When on the timeline the video starts, in milliseconds. */
|
|
27
|
+
at: number;
|
|
28
|
+
/** How long the clip plays, in ms. Defaults to the video's natural duration. */
|
|
29
|
+
duration?: number;
|
|
30
|
+
/** Start offset WITHIN the video file, in ms. Defaults to 0. */
|
|
31
|
+
seek?: number;
|
|
32
|
+
/** CSS selector for the `<video>` element. */
|
|
33
|
+
selector: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type Instr = AnimationInstr | VideoInstr;
|
|
37
|
+
export type TimelineSchema = { duration: number; instructions: Instr[] };
|
|
13
38
|
|
|
14
39
|
declare global {
|
|
15
40
|
interface Window {
|
|
@@ -18,22 +43,65 @@ declare global {
|
|
|
18
43
|
}
|
|
19
44
|
}
|
|
20
45
|
|
|
21
|
-
|
|
46
|
+
type AnimSlot = { kind: "animation"; at: number; duration: number; anim: Animation };
|
|
47
|
+
type VideoSlot = { kind: "video"; at: number; duration: number; seek: number; el: HTMLVideoElement };
|
|
48
|
+
type Slot = AnimSlot | VideoSlot;
|
|
49
|
+
|
|
50
|
+
export function defineTimeline(schema: TimelineSchema): void {
|
|
22
51
|
const rendererPresent =
|
|
23
52
|
typeof window !== "undefined" && window.__mvFramesRendererPresent === true;
|
|
24
53
|
|
|
25
|
-
const
|
|
54
|
+
const slots: Slot[] = schema.instructions.map((i) => {
|
|
26
55
|
const el = document.querySelector(i.selector);
|
|
27
56
|
if (!el) throw new Error(`defineTimeline: no element matches ${i.selector}`);
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
57
|
+
|
|
58
|
+
if (i.kind === "video") {
|
|
59
|
+
if (!(el instanceof HTMLVideoElement)) {
|
|
60
|
+
throw new Error(`defineTimeline: ${i.selector} is not a <video> element`);
|
|
61
|
+
}
|
|
62
|
+
const seek = i.seek ?? 0;
|
|
63
|
+
const naturalMs = isFinite(el.duration) ? el.duration * 1000 : Infinity;
|
|
64
|
+
const duration = i.duration ?? Math.min(naturalMs, schema.duration - i.at);
|
|
65
|
+
el.pause();
|
|
66
|
+
el.currentTime = seek / 1000;
|
|
67
|
+
return { kind: "video", at: i.at, duration, seek, el };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const duration = i.duration ?? Math.max(0, schema.duration - i.at);
|
|
71
|
+
const anim = el.animate(i.props, { duration, fill: "both" });
|
|
72
|
+
anim.pause();
|
|
73
|
+
return { kind: "animation", at: i.at, duration, anim };
|
|
31
74
|
});
|
|
32
75
|
|
|
33
76
|
window.__mvFrames = {
|
|
34
77
|
async applyState(t: number) {
|
|
35
|
-
for (const
|
|
78
|
+
for (const s of slots) {
|
|
79
|
+
const local = clamp(t - s.at, 0, s.duration);
|
|
80
|
+
if (s.kind === "video") {
|
|
81
|
+
s.el.pause();
|
|
82
|
+
s.el.currentTime = (s.seek + local) / 1000;
|
|
83
|
+
} else {
|
|
84
|
+
s.anim.currentTime = local;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
36
87
|
await document.fonts.ready;
|
|
37
88
|
},
|
|
38
89
|
};
|
|
90
|
+
|
|
91
|
+
if (!rendererPresent) {
|
|
92
|
+
for (const s of slots) {
|
|
93
|
+
window.setTimeout(() => {
|
|
94
|
+
if (s.kind === "video") {
|
|
95
|
+
s.el.currentTime = s.seek / 1000;
|
|
96
|
+
void s.el.play();
|
|
97
|
+
} else {
|
|
98
|
+
s.anim.play();
|
|
99
|
+
}
|
|
100
|
+
}, s.at);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function clamp(v: number, lo: number, hi: number): number {
|
|
106
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
39
107
|
}
|