@maravilla-labs/frames 0.2.0 → 0.3.2

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
@@ -94,6 +94,65 @@ Anything the Web Animations API supports as a keyframe is valid in
94
94
  }
95
95
  ```
96
96
 
97
+ ## CSS-class animations (Animate.css and friends)
98
+
99
+ Use `kind: "css"` to schedule an animation defined as CSS `@keyframes`
100
+ and triggered by a class — for example anything from
101
+ [Animate.css](https://animate.style/):
102
+
103
+ ```ts
104
+ {
105
+ kind: "css",
106
+ at: 1400,
107
+ duration: 1000, // optional — defaults to the longest keyframes duration on the element
108
+ selector: ".badge",
109
+ classes: ["animate__animated", "animate__bounceInDown"],
110
+ }
111
+ ```
112
+
113
+ The helper adds the classes, captures the resulting CSS animation as
114
+ a WAAPI `Animation` object via `Element.getAnimations()`, and drives
115
+ its `currentTime` exactly like every other slot. The visual is the
116
+ class's native CSS animation; the timing is yours to control.
117
+
118
+ The stylesheet that defines the keyframes must be loaded before
119
+ `defineTimeline` runs. The simplest pattern is to `await` the page
120
+ `load` event first inside a `<script type="module">`:
121
+
122
+ ```ts
123
+ await new Promise(r => addEventListener("load", r, { once: true }));
124
+ defineTimeline({ /* ... */ });
125
+ ```
126
+
127
+ This works for any library that exposes its motion as CSS classes
128
+ plus shipped `@keyframes` — Animate.css, Magic, your own custom
129
+ keyframes — not just Animate.css specifically.
130
+
131
+ When you pass an explicit `duration`, the helper writes
132
+ `animation-duration` inline with `!important` so it beats any
133
+ `@media (prefers-reduced-motion: reduce)` rule the library ships with
134
+ (Animate.css collapses to 1 ms otherwise). You opted into this
135
+ timeline; the helper assumes you mean it.
136
+
137
+ ### Pre-hiding the element
138
+
139
+ If you need the element invisible *before* its `at` time, use
140
+ `visibility: hidden` in your baseline CSS — not `opacity: 0`. The
141
+ helper flips `visibility: visible` automatically when the css
142
+ instruction fires.
143
+
144
+ ```css
145
+ .badge { visibility: hidden; }
146
+ ```
147
+
148
+ Why not `opacity: 0`? Animate.css's "*In" keyframes (`bounceInDown`,
149
+ `fadeInUp`, etc.) define opacity at the start and middle of the
150
+ animation but **not at 100%**. CSS fills missing keyframe values with
151
+ the element's underlying cascade value — so a baseline of
152
+ `opacity: 0` makes the animation interpolate `1 → 0` at the tail end
153
+ and the element fades back out as soon as it lands. `visibility` is
154
+ not animated by the keyframes, so it stays cleanly out of the way.
155
+
97
156
  ## Video
98
157
 
99
158
  Use `kind: "video"` to schedule a `<video>` element on the timeline.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
- * @maravilla-labs/frames — declare a timeline of animations and video
3
- * playback for a page. The same declaration works two ways:
2
+ * @maravilla-labs/frames — declare a timeline of animations, video, and
3
+ * CSS-class-driven animations (Animate.css, etc.) for a page. The same
4
+ * declaration works two ways:
4
5
  *
5
6
  * - In a normal browser, each item plays at its scheduled `at` time.
6
7
  * - Inside the Maravilla renderer, the timeline is stepped frame by
@@ -30,7 +31,30 @@ export type VideoInstr = {
30
31
  /** CSS selector for the `<video>` element. */
31
32
  selector: string;
32
33
  };
33
- export type Instr = AnimationInstr | VideoInstr;
34
+ /**
35
+ * Drive a CSS-class-triggered animation (Animate.css, Magic, your own
36
+ * `@keyframes`) through the same timeline as everything else. The
37
+ * classes are added to the element, the resulting CSS animations are
38
+ * captured via `Element.getAnimations()`, and the helper drives their
39
+ * `currentTime` exactly like a WAAPI animation.
40
+ *
41
+ * Make sure the stylesheet that defines the keyframes is loaded *before*
42
+ * `defineTimeline` runs — otherwise the class addition produces no
43
+ * animation to drive.
44
+ */
45
+ export type CssClassInstr = {
46
+ /** Discriminator — required. */
47
+ kind: "css";
48
+ /** When on the timeline the animation starts, in milliseconds. */
49
+ at: number;
50
+ /** How long the animation runs. Defaults to the longest CSS @keyframes duration on the element. */
51
+ duration?: number;
52
+ /** CSS selector for the target element. */
53
+ selector: string;
54
+ /** Class names to add, e.g. `["animate__animated", "animate__bounceInDown"]`. */
55
+ classes: string[];
56
+ };
57
+ export type Instr = AnimationInstr | VideoInstr | CssClassInstr;
34
58
  export type TimelineSchema = {
35
59
  duration: number;
36
60
  instructions: Instr[];
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;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;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,gCAAgC;IAChC,IAAI,EAAE,KAAK,CAAC;IACZ,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG,cAAc,GAAG,UAAU,GAAG,aAAa,CAAC;AAChE,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;AAaD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CA6H3D"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
- * @maravilla-labs/frames — declare a timeline of animations and video
3
- * playback for a page. The same declaration works two ways:
2
+ * @maravilla-labs/frames — declare a timeline of animations, video, and
3
+ * CSS-class-driven animations (Animate.css, etc.) for a page. The same
4
+ * declaration works two ways:
4
5
  *
5
6
  * - In a normal browser, each item plays at its scheduled `at` time.
6
7
  * - Inside the Maravilla renderer, the timeline is stepped frame by
@@ -8,7 +9,7 @@
8
9
  */
9
10
  export function defineTimeline(schema) {
10
11
  const rendererPresent = typeof window !== "undefined" && window.__mvFramesRendererPresent === true;
11
- const slots = schema.instructions.map((i) => {
12
+ const slots = schema.instructions.flatMap((i) => {
12
13
  const el = document.querySelector(i.selector);
13
14
  if (!el)
14
15
  throw new Error(`defineTimeline: no element matches ${i.selector}`);
@@ -21,16 +22,67 @@ export function defineTimeline(schema) {
21
22
  const duration = i.duration ?? Math.min(naturalMs, schema.duration - i.at);
22
23
  el.pause();
23
24
  el.currentTime = seek / 1000;
24
- return { kind: "video", at: i.at, duration, seek, el };
25
+ return [{ kind: "video", at: i.at, duration, seek, el }];
25
26
  }
27
+ if (i.kind === "css") {
28
+ if (!(el instanceof HTMLElement)) {
29
+ throw new Error(`defineTimeline: ${i.selector} must be an HTMLElement for kind: "css"`);
30
+ }
31
+ // In browser preview, defer adding the classes until the scheduled
32
+ // time — the CSS animation then plays naturally from t=0.
33
+ if (!rendererPresent) {
34
+ return [{
35
+ kind: "css-pending",
36
+ at: i.at,
37
+ el,
38
+ classes: i.classes,
39
+ duration: i.duration ?? null,
40
+ }];
41
+ }
42
+ // In renderer mode, add the classes now to materialise the CSS
43
+ // animations as WAAPI Animation objects, then pause and drive them
44
+ // via applyState.
45
+ const before = new Set(el.getAnimations());
46
+ el.classList.add(...i.classes);
47
+ const newAnims = el.getAnimations().filter((a) => !before.has(a));
48
+ if (newAnims.length === 0) {
49
+ throw new Error(`defineTimeline: classes [${i.classes.join(", ")}] produced no animation on ${i.selector}. ` +
50
+ `Is the stylesheet that defines those keyframes loaded before defineTimeline runs?`);
51
+ }
52
+ const natural = newAnims.reduce((max, a) => {
53
+ const t = a.effect?.getTiming();
54
+ const d = typeof t?.duration === "number" ? t.duration : 0;
55
+ return Math.max(max, d);
56
+ }, 0);
57
+ // Always honour the user's explicit `duration` (or fall back to the
58
+ // longest captured CSS animation). Override `animation-duration`
59
+ // inline with !important so a stylesheet's
60
+ // `@media (prefers-reduced-motion: reduce)` rule — which Animate.css
61
+ // ships and which collapses durations to 1ms — can't silently shrink
62
+ // the timeline. Users opt into this helper specifically to drive
63
+ // motion; "reduced motion" applies to involuntary motion, not the
64
+ // timeline they're authoring.
65
+ const duration = i.duration ?? (natural > 0 ? natural : Math.max(0, schema.duration - i.at));
66
+ el.style.setProperty("animation-duration", duration + "ms", "important");
67
+ el.style.visibility = "visible";
68
+ return newAnims.map((anim) => {
69
+ anim.effect?.updateTiming({ duration });
70
+ anim.pause();
71
+ anim.currentTime = 0;
72
+ return { kind: "animation", at: i.at, duration, anim };
73
+ });
74
+ }
75
+ // Standard WAAPI animation.
26
76
  const duration = i.duration ?? Math.max(0, schema.duration - i.at);
27
77
  const anim = el.animate(i.props, { duration, fill: "both" });
28
78
  anim.pause();
29
- return { kind: "animation", at: i.at, duration, anim };
79
+ return [{ kind: "animation", at: i.at, duration, anim }];
30
80
  });
31
81
  window.__mvFrames = {
32
82
  async applyState(t) {
33
83
  for (const s of slots) {
84
+ if (s.kind === "css-pending")
85
+ continue; // only relevant in browser preview
34
86
  const local = clamp(t - s.at, 0, s.duration);
35
87
  if (s.kind === "video") {
36
88
  s.el.pause();
@@ -50,6 +102,26 @@ export function defineTimeline(schema) {
50
102
  s.el.currentTime = s.seek / 1000;
51
103
  void s.el.play();
52
104
  }
105
+ else if (s.kind === "css-pending") {
106
+ // Override `animation-duration` BEFORE adding the classes so
107
+ // the inline !important rule beats Animate.css's
108
+ // `prefers-reduced-motion` media query (which would otherwise
109
+ // collapse the animation to 1ms). If the user didn't pass an
110
+ // explicit duration, leave it alone and let the stylesheet's
111
+ // default apply.
112
+ if (s.duration !== null) {
113
+ s.el.style.setProperty("animation-duration", s.duration + "ms", "important");
114
+ }
115
+ // Flip visibility on so users can keep the element
116
+ // `visibility: hidden` in baseline CSS to hide it before its
117
+ // `at` time. Visibility isn't an animatable property in
118
+ // Animate.css keyframes, so it doesn't conflict with the
119
+ // animation the way `opacity: 0` would (a baseline opacity
120
+ // gets pulled into undefined keyframe slots and can make
121
+ // "*In" animations fade back out — surprising).
122
+ s.el.style.visibility = "visible";
123
+ s.el.classList.add(...s.classes);
124
+ }
53
125
  else {
54
126
  s.anim.play();
55
127
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA0CH,MAAM,UAAU,cAAc,CAAC,MAAsB;IACnD,MAAM,eAAe,GACnB,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,yBAAyB,KAAK,IAAI,CAAC;IAE7E,MAAM,KAAK,GAAW,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAClD,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;QAE7E,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YACvB,IAAI,CAAC,CAAC,EAAE,YAAY,gBAAgB,CAAC,EAAE,CAAC;gBACtC,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,QAAQ,2BAA2B,CAAC,CAAC;YAC5E,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;YACzB,MAAM,SAAS,GAAG,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;YACxE,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;YAC3E,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,EAAE,CAAC,WAAW,GAAG,IAAI,GAAG,IAAI,CAAC;YAC7B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QACzD,CAAC;QAED,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;QACnE,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,UAAU,GAAG;QAClB,KAAK,CAAC,UAAU,CAAC,CAAS;YACxB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;gBACtB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;gBAC7C,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;oBACvB,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;oBACb,CAAC,CAAC,EAAE,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC;gBAC7C,CAAC;qBAAM,CAAC;oBACN,CAAC,CAAC,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;gBAC7B,CAAC;YACH,CAAC;YACD,MAAM,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC;QAC7B,CAAC;KACF,CAAC;IAEF,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;gBACrB,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;oBACvB,CAAC,CAAC,EAAE,CAAC,WAAW,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC;oBACjC,KAAK,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;gBACnB,CAAC;qBAAM,CAAC;oBACN,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;gBAChB,CAAC;YACH,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QACX,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,CAAS,EAAE,EAAU,EAAE,EAAU;IAC9C,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACvC,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.2.0",
3
+ "version": "0.3.2",
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,6 +1,7 @@
1
1
  /**
2
- * @maravilla-labs/frames — declare a timeline of animations and video
3
- * playback for a page. The same declaration works two ways:
2
+ * @maravilla-labs/frames — declare a timeline of animations, video, and
3
+ * CSS-class-driven animations (Animate.css, etc.) for a page. The same
4
+ * declaration works two ways:
4
5
  *
5
6
  * - In a normal browser, each item plays at its scheduled `at` time.
6
7
  * - Inside the Maravilla renderer, the timeline is stepped frame by
@@ -33,7 +34,31 @@ export type VideoInstr = {
33
34
  selector: string;
34
35
  };
35
36
 
36
- export type Instr = AnimationInstr | VideoInstr;
37
+ /**
38
+ * Drive a CSS-class-triggered animation (Animate.css, Magic, your own
39
+ * `@keyframes`) through the same timeline as everything else. The
40
+ * classes are added to the element, the resulting CSS animations are
41
+ * captured via `Element.getAnimations()`, and the helper drives their
42
+ * `currentTime` exactly like a WAAPI animation.
43
+ *
44
+ * Make sure the stylesheet that defines the keyframes is loaded *before*
45
+ * `defineTimeline` runs — otherwise the class addition produces no
46
+ * animation to drive.
47
+ */
48
+ export type CssClassInstr = {
49
+ /** Discriminator — required. */
50
+ kind: "css";
51
+ /** When on the timeline the animation starts, in milliseconds. */
52
+ at: number;
53
+ /** How long the animation runs. Defaults to the longest CSS @keyframes duration on the element. */
54
+ duration?: number;
55
+ /** CSS selector for the target element. */
56
+ selector: string;
57
+ /** Class names to add, e.g. `["animate__animated", "animate__bounceInDown"]`. */
58
+ classes: string[];
59
+ };
60
+
61
+ export type Instr = AnimationInstr | VideoInstr | CssClassInstr;
37
62
  export type TimelineSchema = { duration: number; instructions: Instr[] };
38
63
 
39
64
  declare global {
@@ -45,13 +70,20 @@ declare global {
45
70
 
46
71
  type AnimSlot = { kind: "animation"; at: number; duration: number; anim: Animation };
47
72
  type VideoSlot = { kind: "video"; at: number; duration: number; seek: number; el: HTMLVideoElement };
48
- type Slot = AnimSlot | VideoSlot;
73
+ type CssPendingSlot = {
74
+ kind: "css-pending";
75
+ at: number;
76
+ el: HTMLElement;
77
+ classes: string[];
78
+ duration: number | null;
79
+ };
80
+ type Slot = AnimSlot | VideoSlot | CssPendingSlot;
49
81
 
50
82
  export function defineTimeline(schema: TimelineSchema): void {
51
83
  const rendererPresent =
52
84
  typeof window !== "undefined" && window.__mvFramesRendererPresent === true;
53
85
 
54
- const slots: Slot[] = schema.instructions.map((i) => {
86
+ const slots: Slot[] = schema.instructions.flatMap<Slot>((i) => {
55
87
  const el = document.querySelector(i.selector);
56
88
  if (!el) throw new Error(`defineTimeline: no element matches ${i.selector}`);
57
89
 
@@ -64,18 +96,71 @@ export function defineTimeline(schema: TimelineSchema): void {
64
96
  const duration = i.duration ?? Math.min(naturalMs, schema.duration - i.at);
65
97
  el.pause();
66
98
  el.currentTime = seek / 1000;
67
- return { kind: "video", at: i.at, duration, seek, el };
99
+ return [{ kind: "video", at: i.at, duration, seek, el }];
100
+ }
101
+
102
+ if (i.kind === "css") {
103
+ if (!(el instanceof HTMLElement)) {
104
+ throw new Error(`defineTimeline: ${i.selector} must be an HTMLElement for kind: "css"`);
105
+ }
106
+ // In browser preview, defer adding the classes until the scheduled
107
+ // time — the CSS animation then plays naturally from t=0.
108
+ if (!rendererPresent) {
109
+ return [{
110
+ kind: "css-pending",
111
+ at: i.at,
112
+ el,
113
+ classes: i.classes,
114
+ duration: i.duration ?? null,
115
+ }];
116
+ }
117
+ // In renderer mode, add the classes now to materialise the CSS
118
+ // animations as WAAPI Animation objects, then pause and drive them
119
+ // via applyState.
120
+ const before = new Set(el.getAnimations());
121
+ el.classList.add(...i.classes);
122
+ const newAnims = el.getAnimations().filter((a) => !before.has(a));
123
+ if (newAnims.length === 0) {
124
+ throw new Error(
125
+ `defineTimeline: classes [${i.classes.join(", ")}] produced no animation on ${i.selector}. ` +
126
+ `Is the stylesheet that defines those keyframes loaded before defineTimeline runs?`
127
+ );
128
+ }
129
+ const natural = newAnims.reduce((max, a) => {
130
+ const t = a.effect?.getTiming();
131
+ const d = typeof t?.duration === "number" ? t.duration : 0;
132
+ return Math.max(max, d);
133
+ }, 0);
134
+ // Always honour the user's explicit `duration` (or fall back to the
135
+ // longest captured CSS animation). Override `animation-duration`
136
+ // inline with !important so a stylesheet's
137
+ // `@media (prefers-reduced-motion: reduce)` rule — which Animate.css
138
+ // ships and which collapses durations to 1ms — can't silently shrink
139
+ // the timeline. Users opt into this helper specifically to drive
140
+ // motion; "reduced motion" applies to involuntary motion, not the
141
+ // timeline they're authoring.
142
+ const duration = i.duration ?? (natural > 0 ? natural : Math.max(0, schema.duration - i.at));
143
+ el.style.setProperty("animation-duration", duration + "ms", "important");
144
+ el.style.visibility = "visible";
145
+ return newAnims.map<AnimSlot>((anim) => {
146
+ anim.effect?.updateTiming({ duration });
147
+ anim.pause();
148
+ anim.currentTime = 0;
149
+ return { kind: "animation", at: i.at, duration, anim };
150
+ });
68
151
  }
69
152
 
153
+ // Standard WAAPI animation.
70
154
  const duration = i.duration ?? Math.max(0, schema.duration - i.at);
71
155
  const anim = el.animate(i.props, { duration, fill: "both" });
72
156
  anim.pause();
73
- return { kind: "animation", at: i.at, duration, anim };
157
+ return [{ kind: "animation", at: i.at, duration, anim }];
74
158
  });
75
159
 
76
160
  window.__mvFrames = {
77
161
  async applyState(t: number) {
78
162
  for (const s of slots) {
163
+ if (s.kind === "css-pending") continue; // only relevant in browser preview
79
164
  const local = clamp(t - s.at, 0, s.duration);
80
165
  if (s.kind === "video") {
81
166
  s.el.pause();
@@ -94,6 +179,25 @@ export function defineTimeline(schema: TimelineSchema): void {
94
179
  if (s.kind === "video") {
95
180
  s.el.currentTime = s.seek / 1000;
96
181
  void s.el.play();
182
+ } else if (s.kind === "css-pending") {
183
+ // Override `animation-duration` BEFORE adding the classes so
184
+ // the inline !important rule beats Animate.css's
185
+ // `prefers-reduced-motion` media query (which would otherwise
186
+ // collapse the animation to 1ms). If the user didn't pass an
187
+ // explicit duration, leave it alone and let the stylesheet's
188
+ // default apply.
189
+ if (s.duration !== null) {
190
+ s.el.style.setProperty("animation-duration", s.duration + "ms", "important");
191
+ }
192
+ // Flip visibility on so users can keep the element
193
+ // `visibility: hidden` in baseline CSS to hide it before its
194
+ // `at` time. Visibility isn't an animatable property in
195
+ // Animate.css keyframes, so it doesn't conflict with the
196
+ // animation the way `opacity: 0` would (a baseline opacity
197
+ // gets pulled into undefined keyframe slots and can make
198
+ // "*In" animations fade back out — surprising).
199
+ s.el.style.visibility = "visible";
200
+ s.el.classList.add(...s.classes);
97
201
  } else {
98
202
  s.anim.play();
99
203
  }