@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 CHANGED
@@ -1,92 +1,135 @@
1
1
  # @maravilla-labs/frames
2
2
 
3
- A tiny, zero-dependency helper that lets a page declare a Web Animations
4
- API timeline once and have it driven two different ways:
3
+ Declare a timeline of animations and video playback for a page. The same
4
+ declaration works two ways:
5
5
 
6
- - **In a normal browser:** the animations auto-play as soon as the page
7
- loads just like any CSS or WAAPI animation.
8
- - **In the Maravilla deterministic renderer:** the animations stay
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
- The whole package is under 50 lines of vanilla TypeScript. No
13
- framework, no scheduler, no `requestAnimationFrame`, no `Date.now()`.
10
+ Zero dependencies. Bring your own animation library if you want one;
11
+ nothing is required.
14
12
 
15
- ## Web Animations API model
13
+ ## Install
16
14
 
17
- Each entry in `TimelineSchema.instructions` is mapped to a single
18
- [`Animation`](https://developer.mozilla.org/en-US/docs/Web/API/Animation)
19
- created by `Element.animate(keyframes, { duration, fill: "both" })`.
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
- ```html
36
- <!doctype html>
37
- <html>
38
- <body>
39
- <h1 id="title">Hello</h1>
40
- <script type="module">
41
- import { defineTimeline } from "@maravilla-labs/frames";
42
-
43
- defineTimeline({
44
- duration: 2000,
45
- instructions: [
46
- { at: 0, selector: "#title", props: [{ opacity: 0 }, { opacity: 1 }] },
47
- ],
48
- });
49
- </script>
50
- </body>
51
- </html>
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
- In Rust the renderer then does, per frame `i`:
58
+ Call `defineTimeline` once, after the elements referenced by every
59
+ `selector` exist in the DOM.
55
60
 
56
- ```js
57
- await window.__mvFrames.applyState(i * frameMs);
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
- followed by a CDP `HeadlessExperimental.beginFrame` call that captures
61
- the rendered pixels — see the runtime plan for the full pipeline.
77
+ ## Animations
62
78
 
63
- ## API
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
- ### `defineTimeline(schema: TimelineSchema): void`
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
- Sets up the per-page animations and installs
68
- `window.__mvFrames.applyState`. Call it once, after the elements
69
- referenced by every `selector` exist in the DOM.
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
- type FrameInstr = {
73
- at: number; // start offset, ms (currently informational)
74
- selector: string; // any querySelector
75
- props: Keyframe[]; // WAAPI keyframes, e.g. [{ opacity: 0 }, { opacity: 1 }]
76
- };
77
- type TimelineSchema = {
78
- duration: number; // ms — total length the renderer will step through
79
- instructions: FrameInstr[];
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
- Throws if any `selector` doesn't match an element. Do not call
84
- `defineTimeline` more than once per page.
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 the same way:
89
- GSAP, Lottie, Three.js. Drive their time cursor inside an
90
- `applyState`-equivalent and the renderer will sample them
91
- deterministically. `defineTimeline` is just the trivial WAAPI case
92
- that ships in the box.
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 Web Animations API timelines that the
3
- * Maravilla runtime renderer can drive frame-accurately.
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 visit, animations auto-play.
6
- * In the renderer (which sets `window.__mvFramesRendererPresent = true`
7
- * before navigating), animations stay paused and the renderer steps
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 FrameInstr = {
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: FrameInstr[];
36
+ instructions: Instr[];
18
37
  };
19
38
  declare global {
20
39
  interface Window {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,UAAU,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,CAAC;AAC7E,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC;AAE9E,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;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,QAkBpD"}
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 Web Animations API timelines that the
3
- * Maravilla runtime renderer can drive frame-accurately.
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 visit, animations auto-play.
6
- * In the renderer (which sets `window.__mvFramesRendererPresent = true`
7
- * before navigating), animations stay paused and the renderer steps
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 animations = schema.instructions.map((i) => {
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
- const anim = el.animate(i.props, { duration: schema.duration, fill: "both" });
17
- if (rendererPresent)
18
- anim.pause();
19
- return { at: i.at, anim };
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 { anim } of animations)
24
- anim.currentTime = t;
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;;;;;;;;GAQG;AAYH,MAAM,UAAU,cAAc,CAAC,MAAsB;IACnD,MAAM,eAAe,GACnB,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,yBAAyB,KAAK,IAAI,CAAC;IAE7E,MAAM,UAAU,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC/C,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC9C,IAAI,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC7E,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QAC9E,IAAI,eAAe;YAAE,IAAI,CAAC,KAAK,EAAE,CAAC;QAClC,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,UAAU,GAAG;QAClB,KAAK,CAAC,UAAU,CAAC,CAAS;YACxB,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,UAAU;gBAAE,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;YACxD,MAAM,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC;QAC7B,CAAC;KACF,CAAC;AACJ,CAAC"}
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.1.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 Web Animations API timelines that the
3
- * Maravilla runtime renderer can drive frame-accurately.
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 visit, animations auto-play.
6
- * In the renderer (which sets `window.__mvFramesRendererPresent = true`
7
- * before navigating), animations stay paused and the renderer steps
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 FrameInstr = { at: number; selector: string; props: Keyframe[] };
12
- export type TimelineSchema = { duration: number; instructions: FrameInstr[] };
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
- export function defineTimeline(schema: TimelineSchema) {
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 animations = schema.instructions.map((i) => {
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
- const anim = el.animate(i.props, { duration: schema.duration, fill: "both" });
29
- if (rendererPresent) anim.pause();
30
- return { at: i.at, anim };
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 { anim } of animations) anim.currentTime = t;
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
  }