@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.
Files changed (273) hide show
  1. package/README.md +105 -2
  2. package/dist/accessibility/reduced-motion.d.ts +13 -0
  3. package/dist/accessibility/reduced-motion.d.ts.map +1 -0
  4. package/dist/accessibility/reduced-motion.js +56 -0
  5. package/dist/accessibility/reduced-motion.js.map +7 -0
  6. package/dist/animate/animate.d.ts +21 -0
  7. package/dist/animate/animate.d.ts.map +1 -0
  8. package/dist/animate/animate.js +76 -0
  9. package/dist/animate/animate.js.map +7 -0
  10. package/dist/animate/controls.d.ts +16 -0
  11. package/dist/animate/controls.d.ts.map +1 -0
  12. package/dist/animate/controls.js +62 -0
  13. package/dist/animate/controls.js.map +7 -0
  14. package/dist/animate/dom.d.ts +11 -0
  15. package/dist/animate/dom.d.ts.map +1 -0
  16. package/dist/animate/dom.js +112 -0
  17. package/dist/animate/dom.js.map +7 -0
  18. package/dist/animate/js-animation.d.ts +25 -0
  19. package/dist/animate/js-animation.d.ts.map +1 -0
  20. package/dist/animate/js-animation.js +117 -0
  21. package/dist/animate/js-animation.js.map +7 -0
  22. package/dist/animate/plan.d.ts +17 -0
  23. package/dist/animate/plan.d.ts.map +1 -0
  24. package/dist/animate/plan.js +48 -0
  25. package/dist/animate/plan.js.map +7 -0
  26. package/dist/animate/style.d.ts +27 -0
  27. package/dist/animate/style.d.ts.map +1 -0
  28. package/dist/animate/style.js +96 -0
  29. package/dist/animate/style.js.map +7 -0
  30. package/dist/animate/types.d.ts +62 -0
  31. package/dist/animate/types.d.ts.map +1 -0
  32. package/dist/animate/types.js +1 -0
  33. package/dist/animate/types.js.map +7 -0
  34. package/dist/animate/value.d.ts +13 -0
  35. package/dist/animate/value.d.ts.map +1 -0
  36. package/dist/animate/value.js +71 -0
  37. package/dist/animate/value.js.map +7 -0
  38. package/dist/animate/waapi.d.ts +20 -0
  39. package/dist/animate/waapi.d.ts.map +1 -0
  40. package/dist/animate/waapi.js +152 -0
  41. package/dist/animate/waapi.js.map +7 -0
  42. package/dist/dom.d.ts +12 -0
  43. package/dist/dom.d.ts.map +1 -0
  44. package/dist/dom.js +17 -0
  45. package/dist/dom.js.map +7 -0
  46. package/dist/easing/cubic-bezier.d.ts +4 -0
  47. package/dist/easing/cubic-bezier.d.ts.map +1 -0
  48. package/dist/easing/cubic-bezier.js +54 -0
  49. package/dist/easing/cubic-bezier.js.map +7 -0
  50. package/dist/easing/linear-css.d.ts +9 -0
  51. package/dist/easing/linear-css.d.ts.map +1 -0
  52. package/dist/easing/linear-css.js +30 -0
  53. package/dist/easing/linear-css.js.map +7 -0
  54. package/dist/easing/named.d.ts +16 -0
  55. package/dist/easing/named.d.ts.map +1 -0
  56. package/dist/easing/named.js +82 -0
  57. package/dist/easing/named.js.map +7 -0
  58. package/dist/easing/spring.d.ts +11 -0
  59. package/dist/easing/spring.d.ts.map +1 -0
  60. package/dist/easing/spring.js +18 -0
  61. package/dist/easing/spring.js.map +7 -0
  62. package/dist/easing/steps.d.ts +4 -0
  63. package/dist/easing/steps.d.ts.map +1 -0
  64. package/dist/easing/steps.js +14 -0
  65. package/dist/easing/steps.js.map +7 -0
  66. package/dist/easing/types.d.ts +5 -0
  67. package/dist/easing/types.d.ts.map +1 -0
  68. package/dist/easing/types.js +1 -0
  69. package/dist/easing/types.js.map +7 -0
  70. package/dist/frame/driver.d.ts +13 -0
  71. package/dist/frame/driver.d.ts.map +1 -0
  72. package/dist/frame/driver.js +42 -0
  73. package/dist/frame/driver.js.map +7 -0
  74. package/dist/frame/frame.d.ts +20 -0
  75. package/dist/frame/frame.d.ts.map +1 -0
  76. package/dist/frame/frame.js +77 -0
  77. package/dist/frame/frame.js.map +7 -0
  78. package/dist/generators/inertia.d.ts +20 -0
  79. package/dist/generators/inertia.d.ts.map +1 -0
  80. package/dist/generators/inertia.js +59 -0
  81. package/dist/generators/inertia.js.map +7 -0
  82. package/dist/generators/spring.d.ts +17 -0
  83. package/dist/generators/spring.d.ts.map +1 -0
  84. package/dist/generators/spring.js +87 -0
  85. package/dist/generators/spring.js.map +7 -0
  86. package/dist/generators/tween.d.ts +15 -0
  87. package/dist/generators/tween.d.ts.map +1 -0
  88. package/dist/generators/tween.js +51 -0
  89. package/dist/generators/tween.js.map +7 -0
  90. package/dist/generators/types.d.ts +30 -0
  91. package/dist/generators/types.d.ts.map +1 -0
  92. package/dist/generators/types.js +1 -0
  93. package/dist/generators/types.js.map +7 -0
  94. package/dist/index.d.ts +26 -0
  95. package/dist/index.d.ts.map +1 -0
  96. package/dist/index.js +97 -0
  97. package/dist/index.js.map +7 -0
  98. package/dist/interpolate/array.d.ts +3 -0
  99. package/dist/interpolate/array.d.ts.map +1 -0
  100. package/dist/interpolate/array.js +12 -0
  101. package/dist/interpolate/array.js.map +7 -0
  102. package/dist/interpolate/color-parse.d.ts +14 -0
  103. package/dist/interpolate/color-parse.d.ts.map +1 -0
  104. package/dist/interpolate/color-parse.js +111 -0
  105. package/dist/interpolate/color-parse.js.map +7 -0
  106. package/dist/interpolate/color.d.ts +11 -0
  107. package/dist/interpolate/color.d.ts.map +1 -0
  108. package/dist/interpolate/color.js +74 -0
  109. package/dist/interpolate/color.js.map +7 -0
  110. package/dist/interpolate/complex.d.ts +7 -0
  111. package/dist/interpolate/complex.d.ts.map +1 -0
  112. package/dist/interpolate/complex.js +56 -0
  113. package/dist/interpolate/complex.js.map +7 -0
  114. package/dist/interpolate/interpolate.d.ts +16 -0
  115. package/dist/interpolate/interpolate.d.ts.map +1 -0
  116. package/dist/interpolate/interpolate.js +39 -0
  117. package/dist/interpolate/interpolate.js.map +7 -0
  118. package/dist/interpolate/mix.d.ts +8 -0
  119. package/dist/interpolate/mix.d.ts.map +1 -0
  120. package/dist/interpolate/mix.js +48 -0
  121. package/dist/interpolate/mix.js.map +7 -0
  122. package/dist/interpolate/number.d.ts +2 -0
  123. package/dist/interpolate/number.d.ts.map +1 -0
  124. package/dist/interpolate/number.js +7 -0
  125. package/dist/interpolate/number.js.map +7 -0
  126. package/dist/interpolate/object.d.ts +5 -0
  127. package/dist/interpolate/object.d.ts.map +1 -0
  128. package/dist/interpolate/object.js +17 -0
  129. package/dist/interpolate/object.js.map +7 -0
  130. package/dist/interpolate/path/flatten.d.ts +10 -0
  131. package/dist/interpolate/path/flatten.d.ts.map +1 -0
  132. package/dist/interpolate/path/flatten.js +135 -0
  133. package/dist/interpolate/path/flatten.js.map +7 -0
  134. package/dist/interpolate/path/morph.d.ts +9 -0
  135. package/dist/interpolate/path/morph.d.ts.map +1 -0
  136. package/dist/interpolate/path/morph.js +72 -0
  137. package/dist/interpolate/path/morph.js.map +7 -0
  138. package/dist/interpolate/path/parse.d.ts +24 -0
  139. package/dist/interpolate/path/parse.d.ts.map +1 -0
  140. package/dist/interpolate/path/parse.js +125 -0
  141. package/dist/interpolate/path/parse.js.map +7 -0
  142. package/dist/interpolate/path/rings.d.ts +13 -0
  143. package/dist/interpolate/path/rings.d.ts.map +1 -0
  144. package/dist/interpolate/path/rings.js +117 -0
  145. package/dist/interpolate/path/rings.js.map +7 -0
  146. package/dist/react/index.d.ts +7 -0
  147. package/dist/react/index.d.ts.map +1 -0
  148. package/dist/react/index.js +19 -0
  149. package/dist/react/index.js.map +7 -0
  150. package/dist/react/presence.d.ts +27 -0
  151. package/dist/react/presence.d.ts.map +1 -0
  152. package/dist/react/presence.js +135 -0
  153. package/dist/react/presence.js.map +7 -0
  154. package/dist/react/use-animate.d.ts +17 -0
  155. package/dist/react/use-animate.d.ts.map +1 -0
  156. package/dist/react/use-animate.js +34 -0
  157. package/dist/react/use-animate.js.map +7 -0
  158. package/dist/react/use-motion-value.d.ts +4 -0
  159. package/dist/react/use-motion-value.d.ts.map +1 -0
  160. package/dist/react/use-motion-value.js +11 -0
  161. package/dist/react/use-motion-value.js.map +7 -0
  162. package/dist/react/use-reduced-motion.d.ts +7 -0
  163. package/dist/react/use-reduced-motion.d.ts.map +1 -0
  164. package/dist/react/use-reduced-motion.js +19 -0
  165. package/dist/react/use-reduced-motion.js.map +7 -0
  166. package/dist/react/use-spring.d.ts +9 -0
  167. package/dist/react/use-spring.d.ts.map +1 -0
  168. package/dist/react/use-spring.js +27 -0
  169. package/dist/react/use-spring.js.map +7 -0
  170. package/dist/react/use-transform.d.ts +13 -0
  171. package/dist/react/use-transform.d.ts.map +1 -0
  172. package/dist/react/use-transform.js +44 -0
  173. package/dist/react/use-transform.js.map +7 -0
  174. package/dist/sequence/sequence.d.ts +25 -0
  175. package/dist/sequence/sequence.d.ts.map +1 -0
  176. package/dist/sequence/sequence.js +68 -0
  177. package/dist/sequence/sequence.js.map +7 -0
  178. package/dist/stagger/stagger.d.ts +12 -0
  179. package/dist/stagger/stagger.d.ts.map +1 -0
  180. package/dist/stagger/stagger.js +21 -0
  181. package/dist/stagger/stagger.js.map +7 -0
  182. package/dist/testing/manual-frames.d.ts +11 -0
  183. package/dist/testing/manual-frames.d.ts.map +1 -0
  184. package/dist/testing/manual-frames.js +40 -0
  185. package/dist/testing/manual-frames.js.map +7 -0
  186. package/dist/value/motion-value.d.ts +27 -0
  187. package/dist/value/motion-value.d.ts.map +1 -0
  188. package/dist/value/motion-value.js +88 -0
  189. package/dist/value/motion-value.js.map +7 -0
  190. package/dist/value/transform.d.ts +16 -0
  191. package/dist/value/transform.d.ts.map +1 -0
  192. package/dist/value/transform.js +28 -0
  193. package/dist/value/transform.js.map +7 -0
  194. package/dist/view/in-view.d.ts +19 -0
  195. package/dist/view/in-view.d.ts.map +1 -0
  196. package/dist/view/in-view.js +46 -0
  197. package/dist/view/in-view.js.map +7 -0
  198. package/dist/view/resize.d.ts +13 -0
  199. package/dist/view/resize.d.ts.map +1 -0
  200. package/dist/view/resize.js +72 -0
  201. package/dist/view/resize.js.map +7 -0
  202. package/dist/view/resolve-elements.d.ts +3 -0
  203. package/dist/view/resolve-elements.d.ts.map +1 -0
  204. package/dist/view/resolve-elements.js +10 -0
  205. package/dist/view/resolve-elements.js.map +7 -0
  206. package/dist/view/scroll-measure.d.ts +16 -0
  207. package/dist/view/scroll-measure.d.ts.map +1 -0
  208. package/dist/view/scroll-measure.js +51 -0
  209. package/dist/view/scroll-measure.js.map +7 -0
  210. package/dist/view/scroll-offset.d.ts +28 -0
  211. package/dist/view/scroll-offset.d.ts.map +1 -0
  212. package/dist/view/scroll-offset.js +55 -0
  213. package/dist/view/scroll-offset.js.map +7 -0
  214. package/dist/view/scroll.d.ts +34 -0
  215. package/dist/view/scroll.d.ts.map +1 -0
  216. package/dist/view/scroll.js +83 -0
  217. package/dist/view/scroll.js.map +7 -0
  218. package/package.json +57 -5
  219. package/src/accessibility/reduced-motion.ts +75 -0
  220. package/src/animate/animate.ts +155 -0
  221. package/src/animate/controls.ts +76 -0
  222. package/src/animate/dom.ts +166 -0
  223. package/src/animate/js-animation.ts +151 -0
  224. package/src/animate/plan.ts +74 -0
  225. package/src/animate/style.ts +131 -0
  226. package/src/animate/types.ts +69 -0
  227. package/src/animate/value.ts +93 -0
  228. package/src/animate/waapi.ts +212 -0
  229. package/src/dom.ts +35 -0
  230. package/src/easing/cubic-bezier.ts +64 -0
  231. package/src/easing/linear-css.ts +41 -0
  232. package/src/easing/named.ts +88 -0
  233. package/src/easing/spring.ts +25 -0
  234. package/src/easing/steps.ts +16 -0
  235. package/src/easing/types.ts +18 -0
  236. package/src/frame/driver.ts +56 -0
  237. package/src/frame/frame.ts +102 -0
  238. package/src/generators/inertia.ts +88 -0
  239. package/src/generators/spring.ts +117 -0
  240. package/src/generators/tween.ts +74 -0
  241. package/src/generators/types.ts +31 -0
  242. package/src/index.ts +91 -0
  243. package/src/interpolate/array.ts +13 -0
  244. package/src/interpolate/color-parse.ts +136 -0
  245. package/src/interpolate/color.ts +97 -0
  246. package/src/interpolate/complex.ts +80 -0
  247. package/src/interpolate/interpolate.ts +60 -0
  248. package/src/interpolate/mix.ts +74 -0
  249. package/src/interpolate/number.ts +3 -0
  250. package/src/interpolate/object.ts +20 -0
  251. package/src/interpolate/path/flatten.ts +185 -0
  252. package/src/interpolate/path/morph.ts +100 -0
  253. package/src/interpolate/path/parse.ts +162 -0
  254. package/src/interpolate/path/rings.ts +151 -0
  255. package/src/react/index.ts +11 -0
  256. package/src/react/presence.tsx +194 -0
  257. package/src/react/use-animate.ts +46 -0
  258. package/src/react/use-motion-value.ts +9 -0
  259. package/src/react/use-reduced-motion.ts +22 -0
  260. package/src/react/use-spring.ts +35 -0
  261. package/src/react/use-transform.ts +73 -0
  262. package/src/sequence/sequence.ts +129 -0
  263. package/src/stagger/stagger.ts +36 -0
  264. package/src/testing/browser-commands.d.ts +11 -0
  265. package/src/testing/manual-frames.ts +49 -0
  266. package/src/value/motion-value.ts +131 -0
  267. package/src/value/transform.ts +61 -0
  268. package/src/view/in-view.ts +74 -0
  269. package/src/view/resize.ts +101 -0
  270. package/src/view/resolve-elements.ts +11 -0
  271. package/src/view/scroll-measure.ts +83 -0
  272. package/src/view/scroll-offset.ts +100 -0
  273. package/src/view/scroll.ts +143 -0
@@ -0,0 +1,93 @@
1
+ import { shouldReduceMotion } from "#/accessibility/reduced-motion.ts";
2
+ import { finishedControls, groupControls } from "#/animate/controls.ts";
3
+ import { jsAnimation } from "#/animate/js-animation.ts";
4
+ import type {
5
+ AnimationControls,
6
+ AnimationOptions,
7
+ Keyframes,
8
+ ValueAnimationOptions,
9
+ } from "#/animate/types.ts";
10
+ import { cancelFrame, frame } from "#/frame/frame.ts";
11
+ import type { MotionValue } from "#/value/motion-value.ts";
12
+
13
+ function startDelay(options: AnimationOptions): number {
14
+ return typeof options.delay === "function"
15
+ ? options.delay(0, 1)
16
+ : (options.delay ?? 0);
17
+ }
18
+
19
+ /**
20
+ * A motion value towards a target on the frame loop. Whatever the value is
21
+ * doing now — mid-spring, dragged — is where the new animation starts, at
22
+ * the same speed.
23
+ */
24
+ export function animateMotionValue<T>(
25
+ value: MotionValue<T>,
26
+ target: Keyframes<T>,
27
+ options: ValueAnimationOptions<T> = {},
28
+ ): AnimationControls {
29
+ const list: readonly T[] = Array.isArray(target) ? target : [target as T];
30
+ const keyframes = list.length > 1 ? list : [value.get(), list[0] as T];
31
+ const velocity = value.getVelocity();
32
+ value.stop();
33
+ if (shouldReduceMotion(options.reducedMotion)) {
34
+ value.jump(keyframes.at(-1) as T);
35
+ options.onUpdate?.(value.get());
36
+ options.onComplete?.();
37
+ return finishedControls();
38
+ }
39
+ const animation = jsAnimation({
40
+ keyframes,
41
+ options,
42
+ delay: startDelay(options),
43
+ velocity: list.length > 1 ? 0 : velocity,
44
+ apply(next: T) {
45
+ value.set(next);
46
+ options.onUpdate?.(next);
47
+ },
48
+ onComplete: options.onComplete,
49
+ });
50
+ value.start(animation);
51
+ return animation;
52
+ }
53
+
54
+ /** Each numeric or mixable key of a plain object, written back into the object. */
55
+ export function animateObject<T extends object>(
56
+ object: T,
57
+ keyframes: { readonly [K in keyof T]?: Keyframes<T[K]> },
58
+ options: ValueAnimationOptions<T> = {},
59
+ ): AnimationControls {
60
+ const record = object as Record<string, unknown>;
61
+ const reduce = shouldReduceMotion(options.reducedMotion);
62
+ const controls = Object.entries(keyframes).map(([key, target]) => {
63
+ const list: readonly unknown[] = Array.isArray(target) ? target : [target];
64
+ const frames = list.length > 1 ? list : [record[key], list[0]];
65
+ if (reduce) {
66
+ record[key] = frames.at(-1);
67
+ return finishedControls();
68
+ }
69
+ return jsAnimation({
70
+ keyframes: frames,
71
+ options,
72
+ delay: startDelay(options),
73
+ velocity: 0,
74
+ apply(next) {
75
+ record[key] = next;
76
+ },
77
+ });
78
+ });
79
+ if (reduce) options.onUpdate?.(object);
80
+ // One update per frame for the whole object, after every key has moved.
81
+ const group = groupControls(controls);
82
+ const notify = options.onUpdate;
83
+ if (notify && !reduce) {
84
+ const each = () => notify(object);
85
+ frame.render(each, true);
86
+ group.finished.then(() => {
87
+ cancelFrame(each);
88
+ notify(object);
89
+ });
90
+ }
91
+ group.finished.then(() => options.onComplete?.());
92
+ return group;
93
+ }
@@ -0,0 +1,212 @@
1
+ import { thenable } from "#/animate/controls.ts";
2
+ import type { JsAnimation } from "#/animate/js-animation.ts";
3
+ import { iterationTime, planGenerator } from "#/animate/plan.ts";
4
+ import { cssName } from "#/animate/style.ts";
5
+ import type {
6
+ AnimationOptions,
7
+ AnimationState,
8
+ KeyframeValue,
9
+ } from "#/animate/types.ts";
10
+ import { linearEasing, supportsLinearEasing } from "#/easing/linear-css.ts";
11
+ import { easingToCss, resolveEasing } from "#/easing/named.ts";
12
+ import type { EasingDefinition } from "#/easing/types.ts";
13
+ import type { Generator } from "#/generators/types.ts";
14
+
15
+ export interface WaapiConfig {
16
+ readonly element: Element;
17
+ /** The CSS property the browser animates. */
18
+ readonly property: string;
19
+ readonly keyframes: readonly KeyframeValue[];
20
+ /** A keyframe as the property's CSS value. */
21
+ format(value: KeyframeValue): string;
22
+ readonly options: AnimationOptions;
23
+ readonly delay: number;
24
+ readonly velocity: number;
25
+ readonly composite: CompositeOperation;
26
+ /** Writes the value that stays once the animation is gone. */
27
+ commit(value: KeyframeValue): void;
28
+ onComplete?(): void;
29
+ }
30
+
31
+ const SAMPLE_STEP = 1 / 60;
32
+
33
+ function cssEasing(definition: EasingDefinition, duration: number): string {
34
+ return (
35
+ easingToCss(definition) ?? linearEasing(resolveEasing(definition), duration)
36
+ );
37
+ }
38
+
39
+ /**
40
+ * The browser keyframes and easing. Two keyframes on a spring become one
41
+ * `linear()` easing sampled from the spring, so the compositor plays the
42
+ * exact curve. Without `linear()`, or when the spring starts and ends at the
43
+ * same value (a flick of velocity), the spring is sampled into keyframes.
44
+ */
45
+ function browserKeyframes(
46
+ config: WaapiConfig,
47
+ generator: Generator,
48
+ positional: boolean,
49
+ ): { keyframes: Keyframe[]; easing: string } {
50
+ const { keyframes, format, property, options } = config;
51
+ const duration = generator.duration;
52
+ const [from, to] = keyframes;
53
+ const springs =
54
+ options.type !== "tween" && keyframes.length === 2 && !options.ease;
55
+ const span =
56
+ typeof from === "number" && typeof to === "number" ? to - from : 1;
57
+ if (springs && span !== 0 && supportsLinearEasing()) {
58
+ const progress = (t: number) => {
59
+ const { value } = generator.at(t * duration);
60
+ return positional ? value : (value - (from as number)) / span;
61
+ };
62
+ return {
63
+ keyframes: keyframes.map((frame) => ({ [property]: format(frame) })),
64
+ easing: linearEasing(progress, duration),
65
+ };
66
+ }
67
+ if (springs && !positional) {
68
+ const count = Math.max(2, Math.ceil(duration / SAMPLE_STEP) + 1);
69
+ return {
70
+ keyframes: Array.from({ length: count }, (_unused, index) => {
71
+ const offset = index / (count - 1);
72
+ return {
73
+ [property]: format(generator.at(offset * duration).value),
74
+ offset,
75
+ };
76
+ }),
77
+ easing: "linear",
78
+ };
79
+ }
80
+ const ease = options.ease ?? "easeOut";
81
+ return {
82
+ keyframes: keyframes.map((frame, index) => ({
83
+ [property]: format(frame),
84
+ offset: options.times?.[index] ?? index / (keyframes.length - 1),
85
+ easing: cssEasing(
86
+ Array.isArray(ease) && typeof ease[0] !== "number"
87
+ ? ((ease as readonly EasingDefinition[])[index] ?? "linear")
88
+ : (ease as EasingDefinition),
89
+ duration,
90
+ ),
91
+ })),
92
+ easing: "linear",
93
+ };
94
+ }
95
+
96
+ const STATES: Record<AnimationPlayState, AnimationState> = {
97
+ running: "running",
98
+ paused: "paused",
99
+ finished: "finished",
100
+ idle: "finished",
101
+ };
102
+
103
+ /** One CSS property of one element, played by the Web Animations API. */
104
+ export function waapiAnimation(
105
+ config: WaapiConfig,
106
+ ): JsAnimation<KeyframeValue> {
107
+ const { element, keyframes, options, delay, commit } = config;
108
+ const { generator, positional } = planGenerator(
109
+ keyframes,
110
+ options,
111
+ config.velocity,
112
+ );
113
+ const iterations = (options.repeat ?? 0) + 1;
114
+ const { keyframes: frames, easing } = browserKeyframes(
115
+ config,
116
+ generator,
117
+ positional,
118
+ );
119
+ const animation = element.animate(frames, {
120
+ duration: generator.duration * 1000,
121
+ delay: delay * 1000,
122
+ iterations,
123
+ direction: options.repeatType === "reverse" ? "alternate" : "normal",
124
+ easing,
125
+ fill: "both",
126
+ composite: config.composite,
127
+ });
128
+ const last = keyframes[
129
+ options.repeatType === "reverse" && iterations % 2 === 0
130
+ ? 0
131
+ : keyframes.length - 1
132
+ ] as KeyframeValue;
133
+
134
+ let settled = false;
135
+ /** What stayed on screen when it settled: where a following animation starts. */
136
+ let held: KeyframeValue = keyframes[0] as KeyframeValue;
137
+ let resolve: () => void = () => {};
138
+ const finished = new Promise<void>((done) => {
139
+ resolve = done;
140
+ });
141
+
142
+ function current() {
143
+ if (settled) return { value: held, velocity: 0 };
144
+ const elapsed = (Number(animation.currentTime) || 0) / 1000 - delay;
145
+ if (elapsed <= 0)
146
+ return { value: keyframes[0] as KeyframeValue, velocity: 0 };
147
+ const state = generator.at(
148
+ iterationTime(elapsed, generator.duration, options),
149
+ );
150
+ if (!positional) return state;
151
+ // The browser mixes colours and strings itself: ask it what it painted.
152
+ const painted =
153
+ config.composite === "replace"
154
+ ? getComputedStyle(element).getPropertyValue(cssName(config.property))
155
+ : "";
156
+ return { value: painted || last, velocity: 0 };
157
+ }
158
+
159
+ function settle(value: KeyframeValue | undefined) {
160
+ if (settled) return;
161
+ settled = true;
162
+ if (value !== undefined) {
163
+ held = value;
164
+ commit(value);
165
+ }
166
+ animation.cancel();
167
+ resolve();
168
+ }
169
+
170
+ animation.finished.then(
171
+ () => {
172
+ if (settled) return;
173
+ settle(last);
174
+ config.onComplete?.();
175
+ },
176
+ () => settle(undefined),
177
+ );
178
+
179
+ return {
180
+ ...thenable(finished),
181
+ get time() {
182
+ return (Number(animation.currentTime) || 0) / 1000;
183
+ },
184
+ set time(seconds) {
185
+ animation.currentTime = seconds * 1000;
186
+ },
187
+ get speed() {
188
+ return animation.playbackRate;
189
+ },
190
+ set speed(rate) {
191
+ animation.playbackRate = rate;
192
+ },
193
+ duration: delay + generator.duration * iterations,
194
+ get state() {
195
+ return settled ? "finished" : STATES[animation.playState];
196
+ },
197
+ play: () => animation.play(),
198
+ pause: () => animation.pause(),
199
+ stop: () => settle(current().value),
200
+ cancel: () => settle(keyframes[0]),
201
+ // The browser refuses to finish an endless animation: settle on its last frame instead.
202
+ complete: () =>
203
+ iterations === Number.POSITIVE_INFINITY
204
+ ? settle(last)
205
+ : animation.finish(),
206
+ attachTimeline(timeline) {
207
+ animation.timeline = timeline;
208
+ return true;
209
+ },
210
+ current,
211
+ };
212
+ }
package/src/dom.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { animateElements } from "#/animate/dom.ts";
2
+ import type {
3
+ AnimationControls,
4
+ AnimationOptions,
5
+ DOMKeyframes,
6
+ ElementTarget,
7
+ } from "#/animate/types.ts";
8
+ import { resolveElements } from "#/view/resolve-elements.ts";
9
+
10
+ export type {
11
+ AnimationControls,
12
+ AnimationOptions,
13
+ DOMKeyframes,
14
+ ElementTarget,
15
+ } from "#/animate/types.ts";
16
+ export { spring } from "#/generators/spring.ts";
17
+ export { stagger } from "#/stagger/stagger.ts";
18
+
19
+ /**
20
+ * `animate` for elements only, every property played by the browser: CSS
21
+ * properties and the independent transforms, with springs as `linear()`.
22
+ * What it can't hand to the browser (SVG attributes, CSS variables) jumps to
23
+ * its last keyframe: use the full `animate` for those.
24
+ */
25
+ export function animate(
26
+ target: ElementTarget,
27
+ keyframes: DOMKeyframes,
28
+ options: AnimationOptions = {},
29
+ ): AnimationControls {
30
+ return animateElements(
31
+ resolveElements(target as Element | string | readonly Element[]),
32
+ keyframes,
33
+ options,
34
+ );
35
+ }
@@ -0,0 +1,64 @@
1
+ import type { Easing } from "#/easing/types.ts";
2
+
3
+ function linear(progress: number): number {
4
+ return progress;
5
+ }
6
+
7
+ /** One coordinate of the curve at parameter `t`, with P0 = 0 and P3 = 1. */
8
+ function coordinate(t: number, p1: number, p2: number): number {
9
+ return ((1 - 3 * p2 + 3 * p1) * t + (3 * p2 - 6 * p1)) * t * t + 3 * p1 * t;
10
+ }
11
+
12
+ function slope(t: number, p1: number, p2: number): number {
13
+ return 3 * (1 - 3 * p2 + 3 * p1) * t * t + 2 * (3 * p2 - 6 * p1) * t + 3 * p1;
14
+ }
15
+
16
+ /** The curve's parameter whose x is `x`: Newton first, bisection when the slope flattens. */
17
+ function solveT(x: number, x1: number, x2: number): number {
18
+ let t = x;
19
+ for (let iteration = 0; iteration < 8; iteration += 1) {
20
+ const error = coordinate(t, x1, x2) - x;
21
+ if (Math.abs(error) < 1e-7) {
22
+ return t;
23
+ }
24
+ const derivative = slope(t, x1, x2);
25
+ if (Math.abs(derivative) < 1e-6) {
26
+ break;
27
+ }
28
+ t -= error / derivative;
29
+ }
30
+ let low = 0;
31
+ let high = 1;
32
+ t = x;
33
+ for (let iteration = 0; iteration < 24; iteration += 1) {
34
+ const value = coordinate(t, x1, x2);
35
+ if (Math.abs(value - x) < 1e-7) {
36
+ return t;
37
+ }
38
+ if (value < x) {
39
+ low = t;
40
+ } else {
41
+ high = t;
42
+ }
43
+ t = (low + high) / 2;
44
+ }
45
+ return t;
46
+ }
47
+
48
+ /** CSS `cubic-bezier(x1, y1, x2, y2)` as a function. */
49
+ export function cubicBezier(
50
+ x1: number,
51
+ y1: number,
52
+ x2: number,
53
+ y2: number,
54
+ ): Easing {
55
+ if (x1 === y1 && x2 === y2) {
56
+ return linear;
57
+ }
58
+ return function bezier(progress) {
59
+ if (progress <= 0 || progress >= 1) {
60
+ return progress <= 0 ? 0 : 1;
61
+ }
62
+ return coordinate(solveT(progress, x1, x2), y1, y2);
63
+ };
64
+ }
@@ -0,0 +1,41 @@
1
+ import type { Easing } from "#/easing/types.ts";
2
+
3
+ /** Past this many points the string grows with no visible gain. */
4
+ const MAX_POINTS = 200;
5
+
6
+ function round(value: number): string {
7
+ return String(Math.round(value * 10_000) / 10_000);
8
+ }
9
+
10
+ /**
11
+ * Any easing as a CSS `linear()` string, so the browser plays a spring or a
12
+ * custom curve off the main thread. `duration` and `step` are in seconds:
13
+ * longer motions need more points to stay smooth.
14
+ */
15
+ export function linearEasing(
16
+ sample: Easing,
17
+ duration: number,
18
+ step = 0.01,
19
+ ): string {
20
+ const intervals = Math.min(
21
+ MAX_POINTS - 1,
22
+ Math.max(1, Math.ceil(duration / step)),
23
+ );
24
+ const points: string[] = [];
25
+ for (let index = 0; index <= intervals; index += 1) {
26
+ points.push(round(sample(index / intervals)));
27
+ }
28
+ return `linear(${points.join(", ")})`;
29
+ }
30
+
31
+ let supported: boolean | undefined;
32
+
33
+ export function supportsLinearEasing(): boolean {
34
+ if (supported === undefined) {
35
+ if (typeof CSS === "undefined" || typeof CSS.supports !== "function") {
36
+ return false;
37
+ }
38
+ supported = CSS.supports("animation-timing-function", "linear(0, 1)");
39
+ }
40
+ return supported;
41
+ }
@@ -0,0 +1,88 @@
1
+ import { cubicBezier } from "#/easing/cubic-bezier.ts";
2
+ import type {
3
+ CubicBezier,
4
+ Easing,
5
+ EasingDefinition,
6
+ EasingName,
7
+ } from "#/easing/types.ts";
8
+
9
+ function reverse(easing: Easing): Easing {
10
+ return (progress) => 1 - easing(1 - progress);
11
+ }
12
+
13
+ function mirror(easing: Easing): Easing {
14
+ return (progress) =>
15
+ progress <= 0.5
16
+ ? easing(2 * progress) / 2
17
+ : (2 - easing(2 * (1 - progress))) / 2;
18
+ }
19
+
20
+ /** The named easings that are plain CSS curves, so WAAPI plays them natively. */
21
+ const BEZIERS: Partial<Record<EasingName, CubicBezier>> = {
22
+ easeIn: [0.42, 0, 1, 1],
23
+ easeOut: [0, 0, 0.58, 1],
24
+ easeInOut: [0.42, 0, 0.58, 1],
25
+ backIn: [0.31, 0.01, 0.66, -0.59],
26
+ backOut: [0.33, 1.53, 0.69, 0.99],
27
+ };
28
+
29
+ export const linear: Easing = (progress) => progress;
30
+ export const easeIn = cubicBezier(0.42, 0, 1, 1);
31
+ export const easeOut = cubicBezier(0, 0, 0.58, 1);
32
+ export const easeInOut = cubicBezier(0.42, 0, 0.58, 1);
33
+ export const circIn: Easing = (progress) =>
34
+ 1 - Math.sin(Math.acos(Math.min(1, Math.max(0, progress))));
35
+ export const circOut = reverse(circIn);
36
+ export const circInOut = mirror(circIn);
37
+ export const backIn = cubicBezier(0.31, 0.01, 0.66, -0.59);
38
+ export const backOut = cubicBezier(0.33, 1.53, 0.69, 0.99);
39
+ export const backInOut = mirror(backIn);
40
+ export const anticipate: Easing = (progress) => {
41
+ const doubled = progress * 2;
42
+ return doubled < 1
43
+ ? 0.5 * backIn(doubled)
44
+ : 0.5 * (2 - 2 ** (-10 * (doubled - 1)));
45
+ };
46
+
47
+ const NAMED: Record<EasingName, Easing> = {
48
+ linear,
49
+ easeIn,
50
+ easeOut,
51
+ easeInOut,
52
+ circIn,
53
+ circOut,
54
+ circInOut,
55
+ backIn,
56
+ backOut,
57
+ backInOut,
58
+ anticipate,
59
+ };
60
+
61
+ export function resolveEasing(definition: EasingDefinition): Easing {
62
+ if (typeof definition === "function") {
63
+ return definition;
64
+ }
65
+ if (typeof definition === "string") {
66
+ return NAMED[definition];
67
+ }
68
+ return cubicBezier(...definition);
69
+ }
70
+
71
+ function bezierCss([x1, y1, x2, y2]: CubicBezier): string {
72
+ return `cubic-bezier(${x1}, ${y1}, ${x2}, ${y2})`;
73
+ }
74
+
75
+ /** A CSS `<easing-function>` for the definition, or undefined when only sampling can express it. */
76
+ export function easingToCss(definition: EasingDefinition): string | undefined {
77
+ if (typeof definition === "function") {
78
+ return undefined;
79
+ }
80
+ if (typeof definition !== "string") {
81
+ return bezierCss(definition);
82
+ }
83
+ if (definition === "linear") {
84
+ return "linear";
85
+ }
86
+ const bezier = BEZIERS[definition];
87
+ return bezier === undefined ? undefined : bezierCss(bezier);
88
+ }
@@ -0,0 +1,25 @@
1
+ import { linearEasing } from "#/easing/linear-css.ts";
2
+ import type { Easing } from "#/easing/types.ts";
3
+ import { spring } from "#/generators/spring.ts";
4
+ import type { SpringOptions } from "#/generators/types.ts";
5
+
6
+ export interface SpringEasing {
7
+ /** Seconds until the spring settles: the WAAPI duration to play `easing` over. */
8
+ readonly duration: number;
9
+ readonly easing: string;
10
+ readonly at: Easing;
11
+ }
12
+
13
+ /** A spring from 0 to 1 as an easing: what WAAPI plays for a DOM spring. */
14
+ export function springEasing(options: SpringOptions = {}): SpringEasing {
15
+ const generator = spring({ ...options, from: 0, to: 1 });
16
+ const duration = generator.duration;
17
+ function at(progress: number): number {
18
+ return generator.at(progress * duration).value;
19
+ }
20
+ return {
21
+ duration,
22
+ easing: duration > 0 ? linearEasing(at, duration) : "linear(0, 1)",
23
+ at,
24
+ };
25
+ }
@@ -0,0 +1,16 @@
1
+ import type { Easing } from "#/easing/types.ts";
2
+
3
+ /** CSS `steps(count, jump-start | jump-end)`. */
4
+ export function steps(
5
+ count: number,
6
+ direction: "start" | "end" = "end",
7
+ ): Easing {
8
+ const jump = direction === "start" ? 1 : 0;
9
+ return function stepped(progress) {
10
+ if (progress >= 1) {
11
+ return 1;
12
+ }
13
+ const clamped = Math.max(0, progress);
14
+ return Math.min(1, (Math.floor(clamped * count) + jump) / count);
15
+ };
16
+ }
@@ -0,0 +1,18 @@
1
+ export type Easing = (progress: number) => number;
2
+
3
+ export type CubicBezier = readonly [number, number, number, number];
4
+
5
+ export type EasingName =
6
+ | "linear"
7
+ | "easeIn"
8
+ | "easeOut"
9
+ | "easeInOut"
10
+ | "circIn"
11
+ | "circOut"
12
+ | "circInOut"
13
+ | "backIn"
14
+ | "backOut"
15
+ | "backInOut"
16
+ | "anticipate";
17
+
18
+ export type EasingDefinition = EasingName | CubicBezier | Easing;
@@ -0,0 +1,56 @@
1
+ /** What ticks the frame loop: the browser's frames by default, a test clock when injected. */
2
+ export interface FrameDriver {
3
+ request(tick: (timestamp: number) => void): void;
4
+ cancel(): void;
5
+ /** Milliseconds, on the same clock as the timestamps handed to `tick`. */
6
+ now(): number;
7
+ }
8
+
9
+ function clock(): number {
10
+ return typeof performance === "undefined" ? Date.now() : performance.now();
11
+ }
12
+
13
+ /** Read lazily so importing on the server never touches the browser globals. */
14
+ function browserDriver(): FrameDriver {
15
+ let handle: number | ReturnType<typeof setTimeout> | undefined;
16
+ const hasAnimationFrame = typeof requestAnimationFrame === "function";
17
+ return {
18
+ request(tick) {
19
+ handle = hasAnimationFrame
20
+ ? requestAnimationFrame(tick)
21
+ : setTimeout(() => tick(clock()), 1000 / 60);
22
+ },
23
+ cancel() {
24
+ if (handle === undefined) return;
25
+ if (hasAnimationFrame) cancelAnimationFrame(handle as number);
26
+ else clearTimeout(handle);
27
+ handle = undefined;
28
+ },
29
+ now: clock,
30
+ };
31
+ }
32
+
33
+ let injected: FrameDriver | undefined;
34
+ let fallback: FrameDriver | undefined;
35
+ const driverListeners = new Set<(previous: FrameDriver) => void>();
36
+
37
+ export function frameDriver(): FrameDriver {
38
+ if (injected) return injected;
39
+ fallback ??= browserDriver();
40
+ return fallback;
41
+ }
42
+
43
+ /** `undefined` restores the browser's frames. */
44
+ export function setFrameDriver(driver: FrameDriver | undefined): void {
45
+ const previous = frameDriver();
46
+ injected = driver;
47
+ for (const listener of driverListeners) listener(previous);
48
+ }
49
+
50
+ /** The loop listens so a driver swapped mid-animation carries the pending frame over. */
51
+ export function onFrameDriverChange(
52
+ listener: (previous: FrameDriver) => void,
53
+ ): () => void {
54
+ driverListeners.add(listener);
55
+ return () => driverListeners.delete(listener);
56
+ }