@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 +59 -0
- package/dist/index.d.ts +27 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +111 -7
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
|
|
3
|
-
*
|
|
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
|
-
|
|
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[];
|
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;;;;;;;;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
|
|
3
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
3
|
-
*
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
}
|