@voila.dev/motion 0.0.0-stage → 5.0.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 +105 -2
- package/dist/accessibility/reduced-motion.d.ts +13 -0
- package/dist/accessibility/reduced-motion.d.ts.map +1 -0
- package/dist/accessibility/reduced-motion.js +56 -0
- package/dist/accessibility/reduced-motion.js.map +7 -0
- package/dist/animate/animate.d.ts +21 -0
- package/dist/animate/animate.d.ts.map +1 -0
- package/dist/animate/animate.js +76 -0
- package/dist/animate/animate.js.map +7 -0
- package/dist/animate/controls.d.ts +16 -0
- package/dist/animate/controls.d.ts.map +1 -0
- package/dist/animate/controls.js +62 -0
- package/dist/animate/controls.js.map +7 -0
- package/dist/animate/dom.d.ts +11 -0
- package/dist/animate/dom.d.ts.map +1 -0
- package/dist/animate/dom.js +112 -0
- package/dist/animate/dom.js.map +7 -0
- package/dist/animate/js-animation.d.ts +25 -0
- package/dist/animate/js-animation.d.ts.map +1 -0
- package/dist/animate/js-animation.js +117 -0
- package/dist/animate/js-animation.js.map +7 -0
- package/dist/animate/plan.d.ts +17 -0
- package/dist/animate/plan.d.ts.map +1 -0
- package/dist/animate/plan.js +48 -0
- package/dist/animate/plan.js.map +7 -0
- package/dist/animate/style.d.ts +27 -0
- package/dist/animate/style.d.ts.map +1 -0
- package/dist/animate/style.js +96 -0
- package/dist/animate/style.js.map +7 -0
- package/dist/animate/types.d.ts +62 -0
- package/dist/animate/types.d.ts.map +1 -0
- package/dist/animate/types.js +1 -0
- package/dist/animate/types.js.map +7 -0
- package/dist/animate/value.d.ts +13 -0
- package/dist/animate/value.d.ts.map +1 -0
- package/dist/animate/value.js +71 -0
- package/dist/animate/value.js.map +7 -0
- package/dist/animate/waapi.d.ts +20 -0
- package/dist/animate/waapi.d.ts.map +1 -0
- package/dist/animate/waapi.js +152 -0
- package/dist/animate/waapi.js.map +7 -0
- package/dist/dom.d.ts +12 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +17 -0
- package/dist/dom.js.map +7 -0
- package/dist/easing/cubic-bezier.d.ts +4 -0
- package/dist/easing/cubic-bezier.d.ts.map +1 -0
- package/dist/easing/cubic-bezier.js +54 -0
- package/dist/easing/cubic-bezier.js.map +7 -0
- package/dist/easing/linear-css.d.ts +9 -0
- package/dist/easing/linear-css.d.ts.map +1 -0
- package/dist/easing/linear-css.js +30 -0
- package/dist/easing/linear-css.js.map +7 -0
- package/dist/easing/named.d.ts +16 -0
- package/dist/easing/named.d.ts.map +1 -0
- package/dist/easing/named.js +82 -0
- package/dist/easing/named.js.map +7 -0
- package/dist/easing/spring.d.ts +11 -0
- package/dist/easing/spring.d.ts.map +1 -0
- package/dist/easing/spring.js +18 -0
- package/dist/easing/spring.js.map +7 -0
- package/dist/easing/steps.d.ts +4 -0
- package/dist/easing/steps.d.ts.map +1 -0
- package/dist/easing/steps.js +14 -0
- package/dist/easing/steps.js.map +7 -0
- package/dist/easing/types.d.ts +5 -0
- package/dist/easing/types.d.ts.map +1 -0
- package/dist/easing/types.js +1 -0
- package/dist/easing/types.js.map +7 -0
- package/dist/frame/driver.d.ts +13 -0
- package/dist/frame/driver.d.ts.map +1 -0
- package/dist/frame/driver.js +42 -0
- package/dist/frame/driver.js.map +7 -0
- package/dist/frame/frame.d.ts +20 -0
- package/dist/frame/frame.d.ts.map +1 -0
- package/dist/frame/frame.js +77 -0
- package/dist/frame/frame.js.map +7 -0
- package/dist/generators/inertia.d.ts +20 -0
- package/dist/generators/inertia.d.ts.map +1 -0
- package/dist/generators/inertia.js +59 -0
- package/dist/generators/inertia.js.map +7 -0
- package/dist/generators/spring.d.ts +17 -0
- package/dist/generators/spring.d.ts.map +1 -0
- package/dist/generators/spring.js +87 -0
- package/dist/generators/spring.js.map +7 -0
- package/dist/generators/tween.d.ts +15 -0
- package/dist/generators/tween.d.ts.map +1 -0
- package/dist/generators/tween.js +51 -0
- package/dist/generators/tween.js.map +7 -0
- package/dist/generators/types.d.ts +30 -0
- package/dist/generators/types.d.ts.map +1 -0
- package/dist/generators/types.js +1 -0
- package/dist/generators/types.js.map +7 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +7 -0
- package/dist/interpolate/array.d.ts +3 -0
- package/dist/interpolate/array.d.ts.map +1 -0
- package/dist/interpolate/array.js +12 -0
- package/dist/interpolate/array.js.map +7 -0
- package/dist/interpolate/color-parse.d.ts +14 -0
- package/dist/interpolate/color-parse.d.ts.map +1 -0
- package/dist/interpolate/color-parse.js +111 -0
- package/dist/interpolate/color-parse.js.map +7 -0
- package/dist/interpolate/color.d.ts +11 -0
- package/dist/interpolate/color.d.ts.map +1 -0
- package/dist/interpolate/color.js +74 -0
- package/dist/interpolate/color.js.map +7 -0
- package/dist/interpolate/complex.d.ts +7 -0
- package/dist/interpolate/complex.d.ts.map +1 -0
- package/dist/interpolate/complex.js +56 -0
- package/dist/interpolate/complex.js.map +7 -0
- package/dist/interpolate/interpolate.d.ts +16 -0
- package/dist/interpolate/interpolate.d.ts.map +1 -0
- package/dist/interpolate/interpolate.js +39 -0
- package/dist/interpolate/interpolate.js.map +7 -0
- package/dist/interpolate/mix.d.ts +8 -0
- package/dist/interpolate/mix.d.ts.map +1 -0
- package/dist/interpolate/mix.js +48 -0
- package/dist/interpolate/mix.js.map +7 -0
- package/dist/interpolate/number.d.ts +2 -0
- package/dist/interpolate/number.d.ts.map +1 -0
- package/dist/interpolate/number.js +7 -0
- package/dist/interpolate/number.js.map +7 -0
- package/dist/interpolate/object.d.ts +5 -0
- package/dist/interpolate/object.d.ts.map +1 -0
- package/dist/interpolate/object.js +17 -0
- package/dist/interpolate/object.js.map +7 -0
- package/dist/interpolate/path/flatten.d.ts +10 -0
- package/dist/interpolate/path/flatten.d.ts.map +1 -0
- package/dist/interpolate/path/flatten.js +135 -0
- package/dist/interpolate/path/flatten.js.map +7 -0
- package/dist/interpolate/path/morph.d.ts +9 -0
- package/dist/interpolate/path/morph.d.ts.map +1 -0
- package/dist/interpolate/path/morph.js +72 -0
- package/dist/interpolate/path/morph.js.map +7 -0
- package/dist/interpolate/path/parse.d.ts +24 -0
- package/dist/interpolate/path/parse.d.ts.map +1 -0
- package/dist/interpolate/path/parse.js +125 -0
- package/dist/interpolate/path/parse.js.map +7 -0
- package/dist/interpolate/path/rings.d.ts +13 -0
- package/dist/interpolate/path/rings.d.ts.map +1 -0
- package/dist/interpolate/path/rings.js +117 -0
- package/dist/interpolate/path/rings.js.map +7 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +19 -0
- package/dist/react/index.js.map +7 -0
- package/dist/react/presence.d.ts +27 -0
- package/dist/react/presence.d.ts.map +1 -0
- package/dist/react/presence.js +135 -0
- package/dist/react/presence.js.map +7 -0
- package/dist/react/use-animate.d.ts +17 -0
- package/dist/react/use-animate.d.ts.map +1 -0
- package/dist/react/use-animate.js +34 -0
- package/dist/react/use-animate.js.map +7 -0
- package/dist/react/use-motion-value.d.ts +4 -0
- package/dist/react/use-motion-value.d.ts.map +1 -0
- package/dist/react/use-motion-value.js +11 -0
- package/dist/react/use-motion-value.js.map +7 -0
- package/dist/react/use-reduced-motion.d.ts +7 -0
- package/dist/react/use-reduced-motion.d.ts.map +1 -0
- package/dist/react/use-reduced-motion.js +19 -0
- package/dist/react/use-reduced-motion.js.map +7 -0
- package/dist/react/use-spring.d.ts +9 -0
- package/dist/react/use-spring.d.ts.map +1 -0
- package/dist/react/use-spring.js +27 -0
- package/dist/react/use-spring.js.map +7 -0
- package/dist/react/use-transform.d.ts +13 -0
- package/dist/react/use-transform.d.ts.map +1 -0
- package/dist/react/use-transform.js +44 -0
- package/dist/react/use-transform.js.map +7 -0
- package/dist/sequence/sequence.d.ts +25 -0
- package/dist/sequence/sequence.d.ts.map +1 -0
- package/dist/sequence/sequence.js +68 -0
- package/dist/sequence/sequence.js.map +7 -0
- package/dist/stagger/stagger.d.ts +12 -0
- package/dist/stagger/stagger.d.ts.map +1 -0
- package/dist/stagger/stagger.js +21 -0
- package/dist/stagger/stagger.js.map +7 -0
- package/dist/testing/manual-frames.d.ts +11 -0
- package/dist/testing/manual-frames.d.ts.map +1 -0
- package/dist/testing/manual-frames.js +40 -0
- package/dist/testing/manual-frames.js.map +7 -0
- package/dist/value/motion-value.d.ts +27 -0
- package/dist/value/motion-value.d.ts.map +1 -0
- package/dist/value/motion-value.js +88 -0
- package/dist/value/motion-value.js.map +7 -0
- package/dist/value/transform.d.ts +16 -0
- package/dist/value/transform.d.ts.map +1 -0
- package/dist/value/transform.js +28 -0
- package/dist/value/transform.js.map +7 -0
- package/dist/view/in-view.d.ts +19 -0
- package/dist/view/in-view.d.ts.map +1 -0
- package/dist/view/in-view.js +46 -0
- package/dist/view/in-view.js.map +7 -0
- package/dist/view/resize.d.ts +13 -0
- package/dist/view/resize.d.ts.map +1 -0
- package/dist/view/resize.js +72 -0
- package/dist/view/resize.js.map +7 -0
- package/dist/view/resolve-elements.d.ts +3 -0
- package/dist/view/resolve-elements.d.ts.map +1 -0
- package/dist/view/resolve-elements.js +10 -0
- package/dist/view/resolve-elements.js.map +7 -0
- package/dist/view/scroll-measure.d.ts +16 -0
- package/dist/view/scroll-measure.d.ts.map +1 -0
- package/dist/view/scroll-measure.js +51 -0
- package/dist/view/scroll-measure.js.map +7 -0
- package/dist/view/scroll-offset.d.ts +28 -0
- package/dist/view/scroll-offset.d.ts.map +1 -0
- package/dist/view/scroll-offset.js +55 -0
- package/dist/view/scroll-offset.js.map +7 -0
- package/dist/view/scroll.d.ts +34 -0
- package/dist/view/scroll.d.ts.map +1 -0
- package/dist/view/scroll.js +83 -0
- package/dist/view/scroll.js.map +7 -0
- package/package.json +57 -5
- package/src/accessibility/reduced-motion.ts +75 -0
- package/src/animate/animate.ts +155 -0
- package/src/animate/controls.ts +76 -0
- package/src/animate/dom.ts +166 -0
- package/src/animate/js-animation.ts +151 -0
- package/src/animate/plan.ts +74 -0
- package/src/animate/style.ts +131 -0
- package/src/animate/types.ts +69 -0
- package/src/animate/value.ts +93 -0
- package/src/animate/waapi.ts +212 -0
- package/src/dom.ts +35 -0
- package/src/easing/cubic-bezier.ts +64 -0
- package/src/easing/linear-css.ts +41 -0
- package/src/easing/named.ts +88 -0
- package/src/easing/spring.ts +25 -0
- package/src/easing/steps.ts +16 -0
- package/src/easing/types.ts +18 -0
- package/src/frame/driver.ts +56 -0
- package/src/frame/frame.ts +102 -0
- package/src/generators/inertia.ts +88 -0
- package/src/generators/spring.ts +117 -0
- package/src/generators/tween.ts +74 -0
- package/src/generators/types.ts +31 -0
- package/src/index.ts +91 -0
- package/src/interpolate/array.ts +13 -0
- package/src/interpolate/color-parse.ts +136 -0
- package/src/interpolate/color.ts +97 -0
- package/src/interpolate/complex.ts +80 -0
- package/src/interpolate/interpolate.ts +60 -0
- package/src/interpolate/mix.ts +74 -0
- package/src/interpolate/number.ts +3 -0
- package/src/interpolate/object.ts +20 -0
- package/src/interpolate/path/flatten.ts +185 -0
- package/src/interpolate/path/morph.ts +100 -0
- package/src/interpolate/path/parse.ts +162 -0
- package/src/interpolate/path/rings.ts +151 -0
- package/src/react/index.ts +11 -0
- package/src/react/presence.tsx +194 -0
- package/src/react/use-animate.ts +46 -0
- package/src/react/use-motion-value.ts +9 -0
- package/src/react/use-reduced-motion.ts +22 -0
- package/src/react/use-spring.ts +35 -0
- package/src/react/use-transform.ts +73 -0
- package/src/sequence/sequence.ts +129 -0
- package/src/stagger/stagger.ts +36 -0
- package/src/testing/browser-commands.d.ts +11 -0
- package/src/testing/manual-frames.ts +49 -0
- package/src/value/motion-value.ts +131 -0
- package/src/value/transform.ts +61 -0
- package/src/view/in-view.ts +74 -0
- package/src/view/resize.ts +101 -0
- package/src/view/resolve-elements.ts +11 -0
- package/src/view/scroll-measure.ts +83 -0
- package/src/view/scroll-offset.ts +100 -0
- package/src/view/scroll.ts +143 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { AnimationControls, AnimationState } from "#/animate/types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `finished` and the `then` that makes controls awaitable:
|
|
5
|
+
* `await animate(…)` waits for the end, the way Motion's controls do.
|
|
6
|
+
*/
|
|
7
|
+
export function thenable(
|
|
8
|
+
finished: Promise<void>,
|
|
9
|
+
): Pick<AnimationControls, "finished" | "then"> {
|
|
10
|
+
return {
|
|
11
|
+
finished,
|
|
12
|
+
// biome-ignore lint/suspicious/noThenProperty: controls are awaitable on purpose.
|
|
13
|
+
then: (onFulfilled, onRejected) => finished.then(onFulfilled, onRejected),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One set of controls over many animations: the properties of one element,
|
|
19
|
+
* the elements of a selector, the segments of a sequence. They share a clock
|
|
20
|
+
* (every child's `time` counts from the same start), so scrubbing the group
|
|
21
|
+
* scrubs each child at the same moment.
|
|
22
|
+
*/
|
|
23
|
+
export function groupControls(
|
|
24
|
+
children: readonly AnimationControls[],
|
|
25
|
+
): AnimationControls {
|
|
26
|
+
const finished = Promise.all(children.map((child) => child.finished)).then(
|
|
27
|
+
() => undefined,
|
|
28
|
+
);
|
|
29
|
+
function each(action: (child: AnimationControls) => void) {
|
|
30
|
+
return () => {
|
|
31
|
+
for (const child of children) action(child);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
...thenable(finished),
|
|
36
|
+
get time() {
|
|
37
|
+
return Math.max(0, ...children.map((child) => child.time));
|
|
38
|
+
},
|
|
39
|
+
set time(seconds) {
|
|
40
|
+
for (const child of children) child.time = seconds;
|
|
41
|
+
},
|
|
42
|
+
get speed() {
|
|
43
|
+
return children[0]?.speed ?? 1;
|
|
44
|
+
},
|
|
45
|
+
set speed(rate) {
|
|
46
|
+
for (const child of children) child.speed = rate;
|
|
47
|
+
},
|
|
48
|
+
get duration() {
|
|
49
|
+
return Math.max(0, ...children.map((child) => child.duration));
|
|
50
|
+
},
|
|
51
|
+
get state(): AnimationState {
|
|
52
|
+
const states = new Set(children.map((child) => child.state));
|
|
53
|
+
if (states.has("running")) return "running";
|
|
54
|
+
return states.has("paused") ? "paused" : "finished";
|
|
55
|
+
},
|
|
56
|
+
play: each((child) => child.play()),
|
|
57
|
+
pause: each((child) => child.pause()),
|
|
58
|
+
stop: each((child) => child.stop()),
|
|
59
|
+
cancel: each((child) => child.cancel()),
|
|
60
|
+
complete: each((child) => child.complete()),
|
|
61
|
+
attachTimeline(timeline) {
|
|
62
|
+
const attached = children.filter((child) =>
|
|
63
|
+
child.attachTimeline(timeline),
|
|
64
|
+
);
|
|
65
|
+
if (attached.length === children.length) return true;
|
|
66
|
+
// All or nothing: a half-native group would drift from its JS half.
|
|
67
|
+
for (const child of attached) child.attachTimeline(document.timeline);
|
|
68
|
+
return false;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The controls of an animation that had nothing to animate, or jumped: already finished. */
|
|
74
|
+
export function finishedControls(): AnimationControls {
|
|
75
|
+
return groupControls([]);
|
|
76
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { shouldReduceMotion } from "#/accessibility/reduced-motion.ts";
|
|
2
|
+
import { finishedControls, groupControls } from "#/animate/controls.ts";
|
|
3
|
+
import type { JsAnimation, JsAnimationConfig } from "#/animate/js-animation.ts";
|
|
4
|
+
import {
|
|
5
|
+
beginTransform,
|
|
6
|
+
commitTransform,
|
|
7
|
+
committedTransform,
|
|
8
|
+
cssName,
|
|
9
|
+
isMovement,
|
|
10
|
+
TRANSFORMS,
|
|
11
|
+
withUnit,
|
|
12
|
+
} from "#/animate/style.ts";
|
|
13
|
+
import type {
|
|
14
|
+
AnimationControls,
|
|
15
|
+
AnimationOptions,
|
|
16
|
+
DOMKeyframes,
|
|
17
|
+
KeyframeValue,
|
|
18
|
+
} from "#/animate/types.ts";
|
|
19
|
+
import { waapiAnimation } from "#/animate/waapi.ts";
|
|
20
|
+
|
|
21
|
+
/** Attributes an SVG element animates in JavaScript: `d` morphs, the rest are geometry. */
|
|
22
|
+
const SVG_ATTRIBUTES: Readonly<Record<string, string>> = {
|
|
23
|
+
d: "d",
|
|
24
|
+
points: "points",
|
|
25
|
+
viewBox: "viewBox",
|
|
26
|
+
cx: "cx",
|
|
27
|
+
cy: "cy",
|
|
28
|
+
r: "r",
|
|
29
|
+
rx: "rx",
|
|
30
|
+
ry: "ry",
|
|
31
|
+
x1: "x1",
|
|
32
|
+
y1: "y1",
|
|
33
|
+
x2: "x2",
|
|
34
|
+
y2: "y2",
|
|
35
|
+
attrX: "x",
|
|
36
|
+
attrY: "y",
|
|
37
|
+
attrWidth: "width",
|
|
38
|
+
attrHeight: "height",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Plays what the browser can't (SVG attributes, CSS variables) on the frame
|
|
43
|
+
* loop. Injected, so an entry that only needs the browser's half never loads
|
|
44
|
+
* the interpolators.
|
|
45
|
+
*/
|
|
46
|
+
export type ScriptedChannel = (
|
|
47
|
+
config: JsAnimationConfig<KeyframeValue>,
|
|
48
|
+
) => JsAnimation<KeyframeValue>;
|
|
49
|
+
|
|
50
|
+
const running = new WeakMap<Element, Map<string, JsAnimation<KeyframeValue>>>();
|
|
51
|
+
|
|
52
|
+
interface Channel {
|
|
53
|
+
read(): KeyframeValue;
|
|
54
|
+
write(value: KeyframeValue): void;
|
|
55
|
+
readonly native: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function channelOf(element: Element, key: string): Channel {
|
|
59
|
+
const style = (element as HTMLElement).style;
|
|
60
|
+
const attribute =
|
|
61
|
+
element instanceof SVGElement ? SVG_ATTRIBUTES[key] : undefined;
|
|
62
|
+
if (TRANSFORMS[key]) {
|
|
63
|
+
return {
|
|
64
|
+
read: () => committedTransform(element, key),
|
|
65
|
+
write: (value) => commitTransform(element, key, value),
|
|
66
|
+
native: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (attribute) {
|
|
70
|
+
return {
|
|
71
|
+
read: () => element.getAttribute(attribute) ?? "",
|
|
72
|
+
write: (value) => element.setAttribute(attribute, String(value)),
|
|
73
|
+
native: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const name = cssName(key);
|
|
77
|
+
return {
|
|
78
|
+
read: () => getComputedStyle(element).getPropertyValue(name).trim(),
|
|
79
|
+
write: (value) => style.setProperty(name, withUnit(key, value)),
|
|
80
|
+
// An unregistered custom property only animates discretely in the browser.
|
|
81
|
+
native: !key.startsWith("--"),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function asTarget(read: KeyframeValue, target: KeyframeValue): KeyframeValue {
|
|
86
|
+
return typeof target === "number" && typeof read === "string"
|
|
87
|
+
? Number.parseFloat(read) || 0
|
|
88
|
+
: read;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function animateChannel(
|
|
92
|
+
element: Element,
|
|
93
|
+
key: string,
|
|
94
|
+
value: KeyframeValue | readonly KeyframeValue[],
|
|
95
|
+
options: AnimationOptions,
|
|
96
|
+
delay: number,
|
|
97
|
+
scripted: ScriptedChannel | undefined,
|
|
98
|
+
): AnimationControls {
|
|
99
|
+
const channel = channelOf(element, key);
|
|
100
|
+
const list: readonly KeyframeValue[] = Array.isArray(value) ? value : [value];
|
|
101
|
+
const target = list.at(-1) as KeyframeValue;
|
|
102
|
+
const animations = running.get(element) ?? new Map();
|
|
103
|
+
running.set(element, animations);
|
|
104
|
+
const previous = animations.get(key);
|
|
105
|
+
const now = previous?.current();
|
|
106
|
+
previous?.stop();
|
|
107
|
+
const from = now?.value ?? asTarget(channel.read(), target);
|
|
108
|
+
const keyframes = list.length > 1 ? list : [from, target];
|
|
109
|
+
if (
|
|
110
|
+
(shouldReduceMotion(options.reducedMotion) && isMovement(key)) ||
|
|
111
|
+
(!channel.native && !scripted)
|
|
112
|
+
) {
|
|
113
|
+
channel.write(target);
|
|
114
|
+
return finishedControls();
|
|
115
|
+
}
|
|
116
|
+
const reduced = shouldReduceMotion(options.reducedMotion)
|
|
117
|
+
? { ...options, type: "tween" as const, bounce: 0 }
|
|
118
|
+
: options;
|
|
119
|
+
const config = {
|
|
120
|
+
keyframes,
|
|
121
|
+
options: reduced,
|
|
122
|
+
delay,
|
|
123
|
+
velocity: list.length > 1 ? 0 : (now?.velocity ?? 0),
|
|
124
|
+
};
|
|
125
|
+
const transform = TRANSFORMS[key];
|
|
126
|
+
if (transform) beginTransform(element, key);
|
|
127
|
+
const animation = channel.native
|
|
128
|
+
? waapiAnimation({
|
|
129
|
+
...config,
|
|
130
|
+
element,
|
|
131
|
+
property: transform?.property ?? key,
|
|
132
|
+
format: (frame) =>
|
|
133
|
+
transform
|
|
134
|
+
? transform.alone(withUnit(key, frame))
|
|
135
|
+
: withUnit(key, frame),
|
|
136
|
+
composite: transform ? "add" : "replace",
|
|
137
|
+
commit: channel.write,
|
|
138
|
+
})
|
|
139
|
+
: (scripted as ScriptedChannel)({ ...config, apply: channel.write });
|
|
140
|
+
animations.set(key, animation);
|
|
141
|
+
animation.finished.then(() => {
|
|
142
|
+
if (animations.get(key) === animation) animations.delete(key);
|
|
143
|
+
});
|
|
144
|
+
return animation;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Every key of every element, each its own animation, under one set of controls. */
|
|
148
|
+
export function animateElements(
|
|
149
|
+
elements: readonly Element[],
|
|
150
|
+
keyframes: DOMKeyframes,
|
|
151
|
+
options: AnimationOptions,
|
|
152
|
+
scripted?: ScriptedChannel,
|
|
153
|
+
): AnimationControls {
|
|
154
|
+
const { delay } = options;
|
|
155
|
+
return groupControls(
|
|
156
|
+
elements.flatMap((element, index) => {
|
|
157
|
+
const start =
|
|
158
|
+
typeof delay === "function"
|
|
159
|
+
? delay(index, elements.length)
|
|
160
|
+
: (delay ?? 0);
|
|
161
|
+
return Object.entries(keyframes).map(([key, value]) =>
|
|
162
|
+
animateChannel(element, key, value, options, start, scripted),
|
|
163
|
+
);
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { thenable } from "#/animate/controls.ts";
|
|
2
|
+
import { iterationTime, planGenerator, totalDuration } from "#/animate/plan.ts";
|
|
3
|
+
import type {
|
|
4
|
+
AnimationControls,
|
|
5
|
+
AnimationOptions,
|
|
6
|
+
AnimationState,
|
|
7
|
+
} from "#/animate/types.ts";
|
|
8
|
+
import { cancelFrame, type FrameData, frame, now } from "#/frame/frame.ts";
|
|
9
|
+
import { mix } from "#/interpolate/mix.ts";
|
|
10
|
+
|
|
11
|
+
export interface JsAnimationConfig<T> {
|
|
12
|
+
/** At least two: the caller resolves a lone target against the current value. */
|
|
13
|
+
readonly keyframes: readonly T[];
|
|
14
|
+
readonly options: AnimationOptions;
|
|
15
|
+
/** Seconds. */
|
|
16
|
+
readonly delay: number;
|
|
17
|
+
/** The velocity the value has now, carried into a spring. */
|
|
18
|
+
readonly velocity: number;
|
|
19
|
+
apply(value: T): void;
|
|
20
|
+
onComplete?(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface JsAnimation<T> extends AnimationControls {
|
|
24
|
+
/** The value and velocity at the current time: where an interruption starts from. */
|
|
25
|
+
current(): { readonly value: T; readonly velocity: number };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An animation stepped by the frame loop: plain values, objects, SVG
|
|
30
|
+
* attributes and CSS variables — whatever the Web Animations API can't play.
|
|
31
|
+
*/
|
|
32
|
+
export function jsAnimation<T>(config: JsAnimationConfig<T>): JsAnimation<T> {
|
|
33
|
+
const { keyframes, options, delay, apply } = config;
|
|
34
|
+
const { generator, positional } = planGenerator(
|
|
35
|
+
keyframes,
|
|
36
|
+
options,
|
|
37
|
+
config.velocity,
|
|
38
|
+
);
|
|
39
|
+
const segments = positional
|
|
40
|
+
? keyframes.slice(1).map((to, index) => mix(keyframes[index] as T, to as T))
|
|
41
|
+
: [];
|
|
42
|
+
const active = totalDuration(generator.duration, options);
|
|
43
|
+
const duration = delay + active;
|
|
44
|
+
const endsReversed =
|
|
45
|
+
options.repeatType === "reverse" && (options.repeat ?? 0) % 2 === 1;
|
|
46
|
+
const last = keyframes[endsReversed ? 0 : keyframes.length - 1] as T;
|
|
47
|
+
|
|
48
|
+
let time = 0;
|
|
49
|
+
let speed = 1;
|
|
50
|
+
let state: AnimationState = "running";
|
|
51
|
+
let previous = now();
|
|
52
|
+
let resolve: () => void = () => {};
|
|
53
|
+
const finished = new Promise<void>((done) => {
|
|
54
|
+
resolve = done;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function valueAt(position: number): T {
|
|
58
|
+
if (!positional) return position as T;
|
|
59
|
+
const index = Math.min(
|
|
60
|
+
Math.max(Math.floor(position), 0),
|
|
61
|
+
segments.length - 1,
|
|
62
|
+
);
|
|
63
|
+
return (segments[index] as (progress: number) => T)(position - index);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sample() {
|
|
67
|
+
const elapsed = Math.max(0, time - delay);
|
|
68
|
+
return generator.at(iterationTime(elapsed, generator.duration, options));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function render() {
|
|
72
|
+
if (time < delay) return;
|
|
73
|
+
apply(time >= duration ? last : valueAt(sample().value));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function settle(next: AnimationState) {
|
|
77
|
+
state = next;
|
|
78
|
+
cancelFrame(tick);
|
|
79
|
+
resolve();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function tick(data: FrameData) {
|
|
83
|
+
time = Math.max(0, time + ((data.timestamp - previous) / 1000) * speed);
|
|
84
|
+
previous = data.timestamp;
|
|
85
|
+
if (time < duration) {
|
|
86
|
+
render();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
time = duration;
|
|
90
|
+
render();
|
|
91
|
+
settle("finished");
|
|
92
|
+
config.onComplete?.();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function play() {
|
|
96
|
+
if (state === "running") return;
|
|
97
|
+
previous = now();
|
|
98
|
+
state = "running";
|
|
99
|
+
frame.update(tick, true);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
frame.update(tick, true);
|
|
103
|
+
render();
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
...thenable(finished),
|
|
107
|
+
get time() {
|
|
108
|
+
return time;
|
|
109
|
+
},
|
|
110
|
+
set time(seconds) {
|
|
111
|
+
time = Math.min(Math.max(seconds, 0), duration);
|
|
112
|
+
render();
|
|
113
|
+
},
|
|
114
|
+
get speed() {
|
|
115
|
+
return speed;
|
|
116
|
+
},
|
|
117
|
+
set speed(rate) {
|
|
118
|
+
speed = rate;
|
|
119
|
+
},
|
|
120
|
+
duration,
|
|
121
|
+
get state() {
|
|
122
|
+
return state;
|
|
123
|
+
},
|
|
124
|
+
play,
|
|
125
|
+
pause() {
|
|
126
|
+
if (state !== "running") return;
|
|
127
|
+
state = "paused";
|
|
128
|
+
cancelFrame(tick);
|
|
129
|
+
},
|
|
130
|
+
stop: () => settle("finished"),
|
|
131
|
+
cancel() {
|
|
132
|
+
apply(keyframes[0] as T);
|
|
133
|
+
settle("finished");
|
|
134
|
+
},
|
|
135
|
+
complete() {
|
|
136
|
+
if (state === "finished") return;
|
|
137
|
+
time = duration;
|
|
138
|
+
render();
|
|
139
|
+
settle("finished");
|
|
140
|
+
config.onComplete?.();
|
|
141
|
+
},
|
|
142
|
+
attachTimeline: () => false,
|
|
143
|
+
current() {
|
|
144
|
+
const { value, velocity } = sample();
|
|
145
|
+
return {
|
|
146
|
+
value: time < delay ? (keyframes[0] as T) : valueAt(value),
|
|
147
|
+
velocity: positional || time < delay ? 0 : velocity,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AnimationOptions } from "#/animate/types.ts";
|
|
2
|
+
import { spring } from "#/generators/spring.ts";
|
|
3
|
+
import { tween } from "#/generators/tween.ts";
|
|
4
|
+
import type { Generator } from "#/generators/types.ts";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TWEEN_DURATION = 0.3;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The motion of one channel, as a generator. Two numbers spring in their own
|
|
10
|
+
* units, so the velocity a value already has carries into the new motion.
|
|
11
|
+
* Anything else (colours, paths, several keyframes) moves a position along
|
|
12
|
+
* the keyframe list — 0 at the first, 1 at the second… — and the caller mixes
|
|
13
|
+
* the keyframes at that position.
|
|
14
|
+
*/
|
|
15
|
+
export function planGenerator(
|
|
16
|
+
keyframes: readonly unknown[],
|
|
17
|
+
options: AnimationOptions,
|
|
18
|
+
velocity = 0,
|
|
19
|
+
): { readonly generator: Generator; readonly positional: boolean } {
|
|
20
|
+
const numeric = keyframes.every((frame) => typeof frame === "number");
|
|
21
|
+
const springs =
|
|
22
|
+
options.type !== "tween" && keyframes.length === 2 && !options.ease;
|
|
23
|
+
if (numeric) {
|
|
24
|
+
const values = keyframes as readonly number[];
|
|
25
|
+
const generator = springs
|
|
26
|
+
? spring({
|
|
27
|
+
...options,
|
|
28
|
+
from: values[0] ?? 0,
|
|
29
|
+
to: values[1] ?? 0,
|
|
30
|
+
velocity: options.velocity ?? velocity,
|
|
31
|
+
})
|
|
32
|
+
: tween({
|
|
33
|
+
keyframes: values,
|
|
34
|
+
duration: options.duration ?? DEFAULT_TWEEN_DURATION,
|
|
35
|
+
ease: options.ease,
|
|
36
|
+
times: options.times,
|
|
37
|
+
});
|
|
38
|
+
return { generator, positional: false };
|
|
39
|
+
}
|
|
40
|
+
const positions = keyframes.map((_frame, index) => index);
|
|
41
|
+
const generator = springs
|
|
42
|
+
? spring({ ...options, from: 0, to: 1 })
|
|
43
|
+
: tween({
|
|
44
|
+
keyframes: positions,
|
|
45
|
+
duration: options.duration ?? DEFAULT_TWEEN_DURATION,
|
|
46
|
+
ease: options.ease,
|
|
47
|
+
times: options.times,
|
|
48
|
+
});
|
|
49
|
+
return { generator, positional: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The elapsed time within the current repetition, playing backwards on odd ones when they reverse. */
|
|
53
|
+
export function iterationTime(
|
|
54
|
+
elapsed: number,
|
|
55
|
+
duration: number,
|
|
56
|
+
options: AnimationOptions,
|
|
57
|
+
): number {
|
|
58
|
+
const repeat = options.repeat ?? 0;
|
|
59
|
+
if (repeat === 0 || duration === 0) return Math.min(elapsed, duration);
|
|
60
|
+
const total = duration * (repeat + 1);
|
|
61
|
+
const clamped = Math.min(elapsed, total);
|
|
62
|
+
const iteration = Math.min(Math.floor(clamped / duration), repeat);
|
|
63
|
+
const within = clamped - iteration * duration;
|
|
64
|
+
return options.repeatType === "reverse" && iteration % 2 === 1
|
|
65
|
+
? duration - within
|
|
66
|
+
: within;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function totalDuration(
|
|
70
|
+
duration: number,
|
|
71
|
+
options: AnimationOptions,
|
|
72
|
+
): number {
|
|
73
|
+
return duration * ((options.repeat ?? 0) + 1);
|
|
74
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { KeyframeValue } from "#/animate/types.ts";
|
|
2
|
+
|
|
3
|
+
type TransformProperty = "translate" | "scale" | "rotate";
|
|
4
|
+
|
|
5
|
+
interface TransformChannel {
|
|
6
|
+
readonly property: TransformProperty;
|
|
7
|
+
readonly unit: string;
|
|
8
|
+
readonly identity: number;
|
|
9
|
+
/** The channel alone as a value of its property, the other axes at identity. */
|
|
10
|
+
readonly alone: (value: string) => string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The independent transforms. Each animates its CSS property alone with
|
|
15
|
+
* `composite: "add"`, so `x` and `y` run as two compositor animations that
|
|
16
|
+
* sum, and one can be interrupted without touching the other.
|
|
17
|
+
*/
|
|
18
|
+
export const TRANSFORMS: Readonly<Record<string, TransformChannel>> = {
|
|
19
|
+
x: { property: "translate", unit: "px", identity: 0, alone: (v) => `${v}` },
|
|
20
|
+
y: {
|
|
21
|
+
property: "translate",
|
|
22
|
+
unit: "px",
|
|
23
|
+
identity: 0,
|
|
24
|
+
alone: (v) => `0px ${v}`,
|
|
25
|
+
},
|
|
26
|
+
z: {
|
|
27
|
+
property: "translate",
|
|
28
|
+
unit: "px",
|
|
29
|
+
identity: 0,
|
|
30
|
+
alone: (v) => `0px 0px ${v}`,
|
|
31
|
+
},
|
|
32
|
+
scale: { property: "scale", unit: "", identity: 1, alone: (v) => `${v}` },
|
|
33
|
+
scaleX: { property: "scale", unit: "", identity: 1, alone: (v) => `${v} 1` },
|
|
34
|
+
scaleY: { property: "scale", unit: "", identity: 1, alone: (v) => `1 ${v}` },
|
|
35
|
+
rotate: {
|
|
36
|
+
property: "rotate",
|
|
37
|
+
unit: "deg",
|
|
38
|
+
identity: 0,
|
|
39
|
+
alone: (v) => `${v}`,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const UNITLESS = new Set([
|
|
44
|
+
"opacity",
|
|
45
|
+
"zIndex",
|
|
46
|
+
"fontWeight",
|
|
47
|
+
"lineHeight",
|
|
48
|
+
"flexGrow",
|
|
49
|
+
"flexShrink",
|
|
50
|
+
"order",
|
|
51
|
+
"fillOpacity",
|
|
52
|
+
"strokeOpacity",
|
|
53
|
+
"strokeMiterlimit",
|
|
54
|
+
"aspectRatio",
|
|
55
|
+
"pathLength",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/** A keyframe as CSS: numbers take the property's unit. */
|
|
59
|
+
export function withUnit(key: string, value: KeyframeValue): string {
|
|
60
|
+
if (typeof value === "string") return value;
|
|
61
|
+
const unit =
|
|
62
|
+
TRANSFORMS[key]?.unit ??
|
|
63
|
+
(UNITLESS.has(key) || key.startsWith("--") ? "" : "px");
|
|
64
|
+
return `${value}${unit}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Committed transform values per element, the ones no animation is driving. */
|
|
68
|
+
const committed = new WeakMap<Element, Map<string, KeyframeValue>>();
|
|
69
|
+
const animating = new WeakMap<Element, Set<string>>();
|
|
70
|
+
|
|
71
|
+
function entries<T>(
|
|
72
|
+
map: WeakMap<Element, T>,
|
|
73
|
+
element: Element,
|
|
74
|
+
fresh: () => T,
|
|
75
|
+
) {
|
|
76
|
+
const existing = map.get(element);
|
|
77
|
+
if (existing) return existing;
|
|
78
|
+
const created = fresh();
|
|
79
|
+
map.set(element, created);
|
|
80
|
+
return created;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function committedTransform(element: Element, key: string): number {
|
|
84
|
+
const value = committed.get(element)?.get(key);
|
|
85
|
+
return typeof value === "number"
|
|
86
|
+
? value
|
|
87
|
+
: Number.parseFloat(value ?? "") || (TRANSFORMS[key]?.identity ?? 0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function channel(element: Element, key: string): string {
|
|
91
|
+
const channelDef = TRANSFORMS[key] as TransformChannel;
|
|
92
|
+
if (animating.get(element)?.has(key))
|
|
93
|
+
return `${channelDef.identity}${channelDef.unit}`;
|
|
94
|
+
return withUnit(key, committed.get(element)?.get(key) ?? channelDef.identity);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Writes the committed transforms inline, with every animating channel at its identity. */
|
|
98
|
+
export function writeTransforms(element: Element) {
|
|
99
|
+
const style = (element as HTMLElement).style;
|
|
100
|
+
const at = (key: string) => channel(element, key);
|
|
101
|
+
style.translate = `${at("x")} ${at("y")} ${at("z")}`;
|
|
102
|
+
const uniform = Number.parseFloat(at("scale"));
|
|
103
|
+
style.scale = `${uniform * Number.parseFloat(at("scaleX"))} ${uniform * Number.parseFloat(at("scaleY"))}`;
|
|
104
|
+
style.rotate = at("rotate");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function beginTransform(element: Element, key: string) {
|
|
108
|
+
entries(animating, element, () => new Set()).add(key);
|
|
109
|
+
writeTransforms(element);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function commitTransform(
|
|
113
|
+
element: Element,
|
|
114
|
+
key: string,
|
|
115
|
+
value: KeyframeValue,
|
|
116
|
+
) {
|
|
117
|
+
entries(committed, element, () => new Map()).set(key, value);
|
|
118
|
+
animating.get(element)?.delete(key);
|
|
119
|
+
writeTransforms(element);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function cssName(key: string): string {
|
|
123
|
+
return key.startsWith("--")
|
|
124
|
+
? key
|
|
125
|
+
: key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Moves things on screen, as opposed to fading or tinting them: what reduced motion turns off. */
|
|
129
|
+
export function isMovement(key: string): boolean {
|
|
130
|
+
return !/opacity|color|fill|stroke$|filter/i.test(key);
|
|
131
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ReducedMotionPolicy } from "#/accessibility/reduced-motion.ts";
|
|
2
|
+
import type { EasingDefinition } from "#/easing/types.ts";
|
|
3
|
+
import type { SpringOptions } from "#/generators/types.ts";
|
|
4
|
+
|
|
5
|
+
export type Delay = number | ((index: number, total: number) => number);
|
|
6
|
+
|
|
7
|
+
export interface AnimationOptions extends SpringOptions {
|
|
8
|
+
/**
|
|
9
|
+
* "spring" by default for two numeric keyframes, "tween" otherwise. A tween
|
|
10
|
+
* lasts `duration` seconds (0.3 by default); a spring's `duration` is its
|
|
11
|
+
* perceived duration.
|
|
12
|
+
*/
|
|
13
|
+
readonly type?: "spring" | "tween";
|
|
14
|
+
readonly delay?: Delay;
|
|
15
|
+
readonly ease?: EasingDefinition | readonly EasingDefinition[];
|
|
16
|
+
/** Where each keyframe sits, 0 to 1, for a tween over several keyframes. */
|
|
17
|
+
readonly times?: readonly number[];
|
|
18
|
+
readonly repeat?: number;
|
|
19
|
+
readonly repeatType?: "loop" | "reverse";
|
|
20
|
+
/** Units per second, for a spring. Defaults to the velocity the value already has. */
|
|
21
|
+
readonly velocity?: number;
|
|
22
|
+
/** Overrides the global policy for this animation. */
|
|
23
|
+
readonly reducedMotion?: ReducedMotionPolicy;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ValueAnimationOptions<T> extends AnimationOptions {
|
|
27
|
+
readonly onUpdate?: (value: T) => void;
|
|
28
|
+
readonly onComplete?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type AnimationState = "running" | "paused" | "finished";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What every `animate` call returns, whatever drives it. Awaitable: it
|
|
35
|
+
* resolves when the animation finishes, is stopped or is cancelled.
|
|
36
|
+
*/
|
|
37
|
+
export interface AnimationControls extends PromiseLike<void> {
|
|
38
|
+
readonly finished: Promise<void>;
|
|
39
|
+
/** Seconds since the start, delay included. Settable to scrub. */
|
|
40
|
+
time: number;
|
|
41
|
+
speed: number;
|
|
42
|
+
/** Seconds from the start to the end, delay included. */
|
|
43
|
+
readonly duration: number;
|
|
44
|
+
readonly state: AnimationState;
|
|
45
|
+
play(): void;
|
|
46
|
+
pause(): void;
|
|
47
|
+
/** Stops where it is: the value stays as painted. */
|
|
48
|
+
stop(): void;
|
|
49
|
+
/** Stops and goes back to where it started. */
|
|
50
|
+
cancel(): void;
|
|
51
|
+
/** Jumps to the end. */
|
|
52
|
+
complete(): void;
|
|
53
|
+
/** Lets a native scroll or view timeline drive it; false when part of it runs in JavaScript. */
|
|
54
|
+
attachTimeline(timeline: AnimationTimeline): boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type KeyframeValue = number | string;
|
|
58
|
+
export type Keyframes<T = KeyframeValue> = T | readonly T[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* DOM keyframes: any CSS property (camelCase), a CSS variable (`--name`),
|
|
62
|
+
* the independent transforms (`x`, `y`, `z`, `scale`, `scaleX`, `scaleY`,
|
|
63
|
+
* `rotate`), or an SVG attribute. A single value animates from the current
|
|
64
|
+
* one; an array lists every keyframe. Numbers take the property's unit (px,
|
|
65
|
+
* deg) when it has one.
|
|
66
|
+
*/
|
|
67
|
+
export type DOMKeyframes = Readonly<Record<string, Keyframes>>;
|
|
68
|
+
|
|
69
|
+
export type ElementTarget = Element | string | ArrayLike<Element>;
|