@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,102 @@
1
+ import { frameDriver, onFrameDriverChange } from "#/frame/driver.ts";
2
+
3
+ export interface FrameData {
4
+ /** Milliseconds since the previous frame, clamped so a backgrounded tab does not teleport. */
5
+ readonly delta: number;
6
+ readonly timestamp: number;
7
+ }
8
+
9
+ export type FrameCallback = (data: FrameData) => void;
10
+
11
+ type Schedule = (callback: FrameCallback, keepAlive?: boolean) => FrameCallback;
12
+
13
+ const PHASES = ["read", "update", "render"] as const;
14
+ const IDEAL_DELTA = 1000 / 60;
15
+ const MAX_DELTA = 40;
16
+
17
+ let pending = PHASES.map(() => new Set<FrameCallback>());
18
+ let running: Set<FrameCallback> | undefined;
19
+ let keepAlive = new Set<FrameCallback>();
20
+ let scheduled = false;
21
+ let inFrame = false;
22
+ let data: FrameData = { delta: IDEAL_DELTA, timestamp: 0 };
23
+ let lastTimestamp: number | undefined;
24
+
25
+ function request(): void {
26
+ if (scheduled) return;
27
+ scheduled = true;
28
+ frameDriver().request(tick);
29
+ }
30
+
31
+ function tick(timestamp: number): void {
32
+ scheduled = false;
33
+ const delta =
34
+ lastTimestamp === undefined
35
+ ? IDEAL_DELTA
36
+ : Math.max(1, Math.min(timestamp - lastTimestamp, MAX_DELTA));
37
+ data = { delta, timestamp };
38
+ inFrame = true;
39
+ // Swapping each phase's set as it starts is what makes the scheduling rule
40
+ // hold: a later phase's set is still the one that will run, an earlier or
41
+ // the current phase's set is already the next frame's.
42
+ pending.forEach((_, index) => {
43
+ running = pending[index];
44
+ pending[index] = new Set();
45
+ for (const callback of running) {
46
+ callback(data);
47
+ if (keepAlive.has(callback)) pending[index].add(callback);
48
+ }
49
+ });
50
+ running = undefined;
51
+ inFrame = false;
52
+ const idle = pending.every((set) => set.size === 0);
53
+ lastTimestamp = idle ? undefined : timestamp;
54
+ if (!idle) request();
55
+ }
56
+
57
+ function schedule(phase: number): Schedule {
58
+ return (callback, alive = false) => {
59
+ if (alive) keepAlive.add(callback);
60
+ pending[phase].add(callback);
61
+ request();
62
+ return callback;
63
+ };
64
+ }
65
+
66
+ export const frame: {
67
+ readonly read: Schedule;
68
+ readonly update: Schedule;
69
+ readonly render: Schedule;
70
+ } = { read: schedule(0), update: schedule(1), render: schedule(2) };
71
+
72
+ export function cancelFrame(callback: FrameCallback): void {
73
+ keepAlive.delete(callback);
74
+ running?.delete(callback);
75
+ for (const set of pending) set.delete(callback);
76
+ }
77
+
78
+ export function frameData(): FrameData {
79
+ return data;
80
+ }
81
+
82
+ /** Milliseconds; inside a frame, that frame's timestamp, so every value read in it agrees. */
83
+ export function now(): number {
84
+ return inFrame ? data.timestamp : frameDriver().now();
85
+ }
86
+
87
+ /** For tests: forget every callback and the frame clock. */
88
+ export function resetFrameLoop(): void {
89
+ if (scheduled) frameDriver().cancel();
90
+ pending = PHASES.map(() => new Set<FrameCallback>());
91
+ keepAlive = new Set();
92
+ scheduled = false;
93
+ lastTimestamp = undefined;
94
+ data = { delta: IDEAL_DELTA, timestamp: 0 };
95
+ }
96
+
97
+ onFrameDriverChange((previous) => {
98
+ if (!scheduled) return;
99
+ previous.cancel();
100
+ scheduled = false;
101
+ request();
102
+ });
@@ -0,0 +1,88 @@
1
+ import { spring } from "#/generators/spring.ts";
2
+ import type { Generator, GeneratorState } from "#/generators/types.ts";
3
+
4
+ export interface InertiaOptions {
5
+ readonly from: number;
6
+ /** Units per second, typically a gesture's release velocity. */
7
+ readonly velocity: number;
8
+ /** How far the throw carries: target = from + power × velocity. */
9
+ readonly power?: number;
10
+ /** Seconds; higher glides longer. */
11
+ readonly timeConstant?: number;
12
+ readonly min?: number;
13
+ readonly max?: number;
14
+ readonly bounceStiffness?: number;
15
+ readonly bounceDamping?: number;
16
+ readonly restDelta?: number;
17
+ /** Snap the natural resting point, e.g. to a grid. */
18
+ readonly modifyTarget?: (target: number) => number;
19
+ }
20
+
21
+ /** The bound the value is past or heading past, if any. */
22
+ function boundBeyond(value: number, min?: number, max?: number) {
23
+ if (min !== undefined && value < min) {
24
+ return min;
25
+ }
26
+ if (max !== undefined && value > max) {
27
+ return max;
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ /** A throw decaying exponentially, handing over to a spring when it hits `min` or `max`. */
33
+ export function inertia(options: InertiaOptions): Generator {
34
+ const { from, velocity, min, max } = options;
35
+ const timeConstant = options.timeConstant ?? 0.325;
36
+ const restDelta = options.restDelta ?? 0.5;
37
+ function bounce(start: number, startVelocity: number, bound: number) {
38
+ return spring({
39
+ from: start,
40
+ to: bound,
41
+ velocity: startVelocity,
42
+ stiffness: options.bounceStiffness ?? 500,
43
+ damping: options.bounceDamping ?? 10,
44
+ restDelta,
45
+ });
46
+ }
47
+
48
+ const outside = boundBeyond(from, min, max);
49
+ if (outside !== undefined) {
50
+ return bounce(from, velocity, outside);
51
+ }
52
+
53
+ const ideal = from + (options.power ?? 0.8) * velocity;
54
+ const target = options.modifyTarget?.(ideal) ?? ideal;
55
+ const amplitude = target - from;
56
+ function decay(t: number): GeneratorState {
57
+ const remaining = amplitude * Math.exp(-t / timeConstant);
58
+ return {
59
+ value: target - remaining,
60
+ velocity: remaining / timeConstant,
61
+ done: false,
62
+ };
63
+ }
64
+ const settle =
65
+ Math.abs(amplitude) > restDelta
66
+ ? timeConstant * Math.log(Math.abs(amplitude) / restDelta)
67
+ : 0;
68
+
69
+ const bound = boundBeyond(target, min, max);
70
+ if (bound === undefined) {
71
+ return {
72
+ duration: settle,
73
+ at: (elapsed) =>
74
+ elapsed >= settle
75
+ ? { value: target, velocity: 0, done: true }
76
+ : decay(Math.max(0, elapsed)),
77
+ };
78
+ }
79
+
80
+ const hit = -timeConstant * Math.log((target - bound) / amplitude);
81
+ const atHit = decay(hit);
82
+ const handover = bounce(bound, atHit.velocity, bound);
83
+ return {
84
+ duration: hit + handover.duration,
85
+ at: (elapsed) =>
86
+ elapsed < hit ? decay(Math.max(0, elapsed)) : handover.at(elapsed - hit),
87
+ };
88
+ }
@@ -0,0 +1,117 @@
1
+ import type {
2
+ Generator,
3
+ GeneratorState,
4
+ SpringOptions,
5
+ } from "#/generators/types.ts";
6
+
7
+ export interface SpringPhysics {
8
+ readonly stiffness: number;
9
+ readonly damping: number;
10
+ readonly mass: number;
11
+ }
12
+
13
+ /** A spring that never settles would animate forever: stop looking after this. */
14
+ const MAX_DURATION = 20;
15
+ const SETTLE_STEP = 0.005;
16
+
17
+ /**
18
+ * Duration and bounce to physics, SwiftUI's mapping: the undamped period is
19
+ * the perceived duration, and the damping ratio is `1 - bounce`.
20
+ */
21
+ export function springPhysics(options: SpringOptions = {}): SpringPhysics {
22
+ const { stiffness, damping, mass } = options;
23
+ if (stiffness !== undefined || damping !== undefined || mass !== undefined) {
24
+ return {
25
+ stiffness: stiffness ?? 100,
26
+ damping: damping ?? 10,
27
+ mass: mass ?? 1,
28
+ };
29
+ }
30
+ const duration = Math.max(options.duration ?? 0.3, 0.001);
31
+ const bounce = Math.min(Math.max(options.bounce ?? 0, 0), 0.95);
32
+ return {
33
+ stiffness: ((2 * Math.PI) / duration) ** 2,
34
+ damping: (4 * Math.PI * (1 - bounce)) / duration,
35
+ mass: 1,
36
+ };
37
+ }
38
+
39
+ type Displacement = (t: number) => readonly [number, number];
40
+
41
+ /** The damped oscillator's closed form: displacement from the target and its velocity at `t`. */
42
+ function oscillator(
43
+ physics: SpringPhysics,
44
+ x0: number,
45
+ v0: number,
46
+ ): Displacement {
47
+ const omega = Math.sqrt(physics.stiffness / physics.mass);
48
+ const zeta =
49
+ physics.damping / (2 * Math.sqrt(physics.stiffness * physics.mass));
50
+ if (Math.abs(zeta - 1) < 1e-6) {
51
+ const c = v0 + omega * x0;
52
+ return (t) => {
53
+ const decay = Math.exp(-omega * t);
54
+ return [decay * (x0 + c * t), decay * (c - omega * (x0 + c * t))];
55
+ };
56
+ }
57
+ if (zeta < 1) {
58
+ const omegaD = omega * Math.sqrt(1 - zeta * zeta);
59
+ const b = (v0 + zeta * omega * x0) / omegaD;
60
+ return (t) => {
61
+ const decay = Math.exp(-zeta * omega * t);
62
+ const cos = Math.cos(omegaD * t);
63
+ const sin = Math.sin(omegaD * t);
64
+ const x = x0 * cos + b * sin;
65
+ return [
66
+ decay * x,
67
+ decay * (-zeta * omega * x + omegaD * (b * cos - x0 * sin)),
68
+ ];
69
+ };
70
+ }
71
+ const root = omega * Math.sqrt(zeta * zeta - 1);
72
+ const r1 = -zeta * omega + root;
73
+ const r2 = -zeta * omega - root;
74
+ const c2 = (v0 - r1 * x0) / (r2 - r1);
75
+ const c1 = x0 - c2;
76
+ return (t) => {
77
+ const e1 = c1 * Math.exp(r1 * t);
78
+ const e2 = c2 * Math.exp(r2 * t);
79
+ return [e1 + e2, r1 * e1 + r2 * e2];
80
+ };
81
+ }
82
+
83
+ /** Rest thresholds scale with the motion, so 0→1 and 0→1000 both settle to the eye. */
84
+ function restThresholds(options: SpringOptions, span: number) {
85
+ const restDelta = options.restDelta ?? Math.max(span, 1) * 0.001;
86
+ return { restDelta, restSpeed: options.restSpeed ?? restDelta * 10 };
87
+ }
88
+
89
+ export function spring(
90
+ options: SpringOptions & { from: number; to: number; velocity?: number },
91
+ ): Generator {
92
+ const { from, to } = options;
93
+ const velocity = options.velocity ?? 0;
94
+ const displacement = oscillator(springPhysics(options), from - to, velocity);
95
+ const { restDelta, restSpeed } = restThresholds(options, Math.abs(to - from));
96
+
97
+ function atRest(t: number): boolean {
98
+ const [x, v] = displacement(t);
99
+ return Math.abs(x) <= restDelta && Math.abs(v) <= restSpeed;
100
+ }
101
+
102
+ let duration = 0;
103
+ while (duration < MAX_DURATION && !atRest(duration)) {
104
+ duration += SETTLE_STEP;
105
+ }
106
+
107
+ return {
108
+ duration,
109
+ at(elapsed): GeneratorState {
110
+ if (elapsed >= duration) {
111
+ return { value: to, velocity: 0, done: true };
112
+ }
113
+ const [x, v] = displacement(Math.max(0, elapsed));
114
+ return { value: to + x, velocity: v, done: false };
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,74 @@
1
+ import { easeOut, resolveEasing } from "#/easing/named.ts";
2
+ import type { EasingDefinition } from "#/easing/types.ts";
3
+ import type { Generator, GeneratorState } from "#/generators/types.ts";
4
+
5
+ export interface TweenOptions {
6
+ readonly keyframes: readonly number[];
7
+ /** Seconds, default 0.3. */
8
+ readonly duration?: number;
9
+ /** One easing for every segment, or one per segment. */
10
+ readonly ease?: EasingDefinition | readonly EasingDefinition[];
11
+ /** Each keyframe's offset in 0..1, default evenly spaced. */
12
+ readonly times?: readonly number[];
13
+ /** A tween's shape is fixed by its easing; accepted so options pass through unchanged. */
14
+ readonly velocity?: number;
15
+ }
16
+
17
+ /** Window for the finite difference that reports velocity. */
18
+ const VELOCITY_WINDOW = 0.005;
19
+
20
+ function isEasingList(
21
+ ease: TweenOptions["ease"],
22
+ ): ease is readonly EasingDefinition[] {
23
+ return Array.isArray(ease) && typeof ease[0] !== "number";
24
+ }
25
+
26
+ export function tween(options: TweenOptions): Generator {
27
+ const { keyframes } = options;
28
+ const duration = Math.max(0, options.duration ?? 0.3);
29
+ const last = keyframes.length - 1;
30
+ const times =
31
+ options.times ??
32
+ keyframes.map((_unused, index) => (last === 0 ? 0 : index / last));
33
+ const eases = Array.from({ length: Math.max(last, 1) }, (_unused, index) => {
34
+ const ease = options.ease;
35
+ if (ease === undefined) {
36
+ return easeOut;
37
+ }
38
+ return resolveEasing(
39
+ isEasingList(ease)
40
+ ? (ease[index] ?? ease[ease.length - 1] ?? "easeOut")
41
+ : ease,
42
+ );
43
+ });
44
+
45
+ function valueAt(elapsed: number): number {
46
+ const progress =
47
+ duration === 0 ? 1 : Math.min(1, Math.max(0, elapsed / duration));
48
+ let segment = 0;
49
+ while (segment < last - 1 && progress > (times[segment + 1] ?? 1)) {
50
+ segment += 1;
51
+ }
52
+ const start = times[segment] ?? 0;
53
+ const end = times[segment + 1] ?? 1;
54
+ const from = keyframes[segment] ?? 0;
55
+ const to = keyframes[segment + 1] ?? from;
56
+ const local = end === start ? 1 : (progress - start) / (end - start);
57
+ const eased = (eases[segment] ?? easeOut)(Math.min(1, Math.max(0, local)));
58
+ return from + (to - from) * eased;
59
+ }
60
+
61
+ return {
62
+ duration,
63
+ at(elapsed): GeneratorState {
64
+ if (elapsed >= duration) {
65
+ return { value: keyframes[last] ?? 0, velocity: 0, done: true };
66
+ }
67
+ const value = valueAt(elapsed);
68
+ const before = Math.max(0, elapsed - VELOCITY_WINDOW);
69
+ const after = before === elapsed ? elapsed + VELOCITY_WINDOW : elapsed;
70
+ const velocity = (valueAt(after) - valueAt(before)) / (after - before);
71
+ return { value, velocity, done: false };
72
+ },
73
+ };
74
+ }
@@ -0,0 +1,31 @@
1
+ export interface GeneratorState {
2
+ readonly value: number;
3
+ /** Units per second. */
4
+ readonly velocity: number;
5
+ readonly done: boolean;
6
+ }
7
+
8
+ /** One number moving over time: a spring, a tween or a decay. */
9
+ export interface Generator {
10
+ /** Seconds until it settles. */
11
+ readonly duration: number;
12
+ /** The state `elapsed` seconds after the start. */
13
+ at(elapsed: number): GeneratorState;
14
+ }
15
+
16
+ export interface SpringOptions {
17
+ /**
18
+ * Perceived duration in seconds, SwiftUI's model: how long the motion
19
+ * looks like it takes, not when the last sub-pixel wobble ends. Ignored
20
+ * when `stiffness`, `damping` or `mass` is given.
21
+ */
22
+ readonly duration?: number;
23
+ /** 0 never overshoots; towards 1, springier. */
24
+ readonly bounce?: number;
25
+ readonly stiffness?: number;
26
+ readonly damping?: number;
27
+ readonly mass?: number;
28
+ /** Below this speed (units per second) and `restDelta`, the spring is done. */
29
+ readonly restSpeed?: number;
30
+ readonly restDelta?: number;
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,91 @@
1
+ export {
2
+ onReducedMotionChange,
3
+ prefersReducedMotion,
4
+ type ReducedMotionPolicy,
5
+ reducedMotionPolicy,
6
+ setReducedMotion,
7
+ shouldReduceMotion,
8
+ } from "#/accessibility/reduced-motion.ts";
9
+ export { type Animate, animate, createAnimate } from "#/animate/animate.ts";
10
+ export type {
11
+ AnimationControls,
12
+ AnimationOptions,
13
+ AnimationState,
14
+ DOMKeyframes,
15
+ ElementTarget,
16
+ Keyframes,
17
+ ValueAnimationOptions,
18
+ } from "#/animate/types.ts";
19
+ export { cubicBezier } from "#/easing/cubic-bezier.ts";
20
+ export { linearEasing } from "#/easing/linear-css.ts";
21
+ export {
22
+ anticipate,
23
+ backIn,
24
+ backInOut,
25
+ backOut,
26
+ circIn,
27
+ circInOut,
28
+ circOut,
29
+ easeIn,
30
+ easeInOut,
31
+ easeOut,
32
+ linear,
33
+ resolveEasing,
34
+ } from "#/easing/named.ts";
35
+ export { type SpringEasing, springEasing } from "#/easing/spring.ts";
36
+ export { steps } from "#/easing/steps.ts";
37
+ export type {
38
+ CubicBezier,
39
+ Easing,
40
+ EasingDefinition,
41
+ EasingName,
42
+ } from "#/easing/types.ts";
43
+ export {
44
+ cancelFrame,
45
+ type FrameCallback,
46
+ type FrameData,
47
+ frame,
48
+ frameData,
49
+ } from "#/frame/frame.ts";
50
+ export { type InertiaOptions, inertia } from "#/generators/inertia.ts";
51
+ export { spring, springPhysics } from "#/generators/spring.ts";
52
+ export { type TweenOptions, tween } from "#/generators/tween.ts";
53
+ export type {
54
+ Generator,
55
+ GeneratorState,
56
+ SpringOptions,
57
+ } from "#/generators/types.ts";
58
+ export { mixColor } from "#/interpolate/color.ts";
59
+ export {
60
+ type InterpolateOptions,
61
+ interpolate,
62
+ } from "#/interpolate/interpolate.ts";
63
+ export { type Mixer, mix } from "#/interpolate/mix.ts";
64
+ export { mixPath } from "#/interpolate/path/morph.ts";
65
+ export type {
66
+ Segment,
67
+ SegmentOptions,
68
+ SequenceAt,
69
+ } from "#/sequence/sequence.ts";
70
+ export { type StaggerOptions, stagger } from "#/stagger/stagger.ts";
71
+ export {
72
+ isMotionValue,
73
+ type MotionValue,
74
+ type MotionValueEvent,
75
+ motionValue,
76
+ } from "#/value/motion-value.ts";
77
+ export { transformValue } from "#/value/transform.ts";
78
+ export {
79
+ type InViewOptions,
80
+ inView,
81
+ type OnViewEnd,
82
+ type OnViewStart,
83
+ } from "#/view/in-view.ts";
84
+ export { resize, type Size } from "#/view/resize.ts";
85
+ export {
86
+ type OnScroll,
87
+ type ScrollDrivable,
88
+ type ScrollInfo,
89
+ type ScrollOptions,
90
+ scroll,
91
+ } from "#/view/scroll.ts";
@@ -0,0 +1,13 @@
1
+ import { mix } from "#/interpolate/mix.ts";
2
+
3
+ /** Element by element; arrays of different lengths have no midpoint, so they step. */
4
+ export function mixArray<T>(
5
+ from: readonly T[],
6
+ to: readonly T[],
7
+ ): ((progress: number) => T[]) | undefined {
8
+ if (from.length !== to.length) {
9
+ return undefined;
10
+ }
11
+ const mixers = from.map((value, index) => mix(value, to[index] as T));
12
+ return (progress) => mixers.map((mixer) => mixer(progress));
13
+ }
@@ -0,0 +1,136 @@
1
+ /** A colour in sRGB, every channel 0–1. */
2
+ export interface Rgba {
3
+ readonly r: number;
4
+ readonly g: number;
5
+ readonly b: number;
6
+ readonly alpha: number;
7
+ }
8
+
9
+ type Channels = readonly [number, number, number, number];
10
+ type Parser = (args: readonly string[]) => Rgba | undefined;
11
+
12
+ const FUNCTION = /^(rgba?|hsla?|oklch|oklab)\(\s*([^()]*)\)$/i;
13
+ const HUE_UNITS: Readonly<Record<string, number>> = {
14
+ deg: 1,
15
+ grad: 0.9,
16
+ rad: 180 / Math.PI,
17
+ turn: 360,
18
+ };
19
+
20
+ /** A number, a percentage of `percentScale`, or `none`. */
21
+ function channel(text: string | undefined, percentScale: number): number {
22
+ if (text === undefined || text === "none") {
23
+ return 0;
24
+ }
25
+ return text.endsWith("%")
26
+ ? (Number.parseFloat(text) / 100) * percentScale
27
+ : Number.parseFloat(text);
28
+ }
29
+
30
+ function hue(text: string | undefined): number {
31
+ const unit = text?.match(/[a-z]+$/i)?.[0].toLowerCase() ?? "deg";
32
+ return channel(text, 1) * (HUE_UNITS[unit] ?? 1);
33
+ }
34
+
35
+ function alphaOf(args: readonly string[]): number {
36
+ return args[3] === undefined ? 1 : channel(args[3], 1);
37
+ }
38
+
39
+ function linearToSrgb(value: number): number {
40
+ return value <= 0.0031308
41
+ ? 12.92 * value
42
+ : 1.055 * value ** (1 / 2.4) - 0.055;
43
+ }
44
+
45
+ /** Oklab is mixed in, and also a syntax a page can write colours in. */
46
+ export function oklabToRgba([l, a, b, alpha]: Channels): Rgba {
47
+ const lms = [
48
+ (l + 0.3963377774 * a + 0.2158037573 * b) ** 3,
49
+ (l - 0.1055613458 * a - 0.0638541728 * b) ** 3,
50
+ (l - 0.0894841775 * a - 1.291485548 * b) ** 3,
51
+ ] as const;
52
+ const [x, y, z] = lms;
53
+ return {
54
+ r: linearToSrgb(4.0767416621 * x - 3.3077115913 * y + 0.2309699292 * z),
55
+ g: linearToSrgb(-1.2684380046 * x + 2.6097574011 * y - 0.3413193965 * z),
56
+ b: linearToSrgb(-0.0041960863 * x - 0.7034186147 * y + 1.707614701 * z),
57
+ alpha,
58
+ };
59
+ }
60
+
61
+ function hslToRgba(h: number, s: number, l: number, alpha: number): Rgba {
62
+ const amount = s * Math.min(l, 1 - l);
63
+ function f(n: number) {
64
+ const k = (n + h / 30) % 12;
65
+ return l - amount * Math.max(-1, Math.min(k - 3, 9 - k, 1));
66
+ }
67
+ return { r: f(0), g: f(8), b: f(4), alpha };
68
+ }
69
+
70
+ const PARSERS: Readonly<Record<string, Parser>> = {
71
+ rgb: (args) => ({
72
+ r: channel(args[0], 255) / 255,
73
+ g: channel(args[1], 255) / 255,
74
+ b: channel(args[2], 255) / 255,
75
+ alpha: alphaOf(args),
76
+ }),
77
+ hsl: (args) =>
78
+ hslToRgba(
79
+ ((hue(args[0]) % 360) + 360) % 360,
80
+ channel(args[1], 100) / 100,
81
+ channel(args[2], 100) / 100,
82
+ alphaOf(args),
83
+ ),
84
+ oklab: (args) =>
85
+ oklabToRgba([
86
+ channel(args[0], 1),
87
+ channel(args[1], 0.4),
88
+ channel(args[2], 0.4),
89
+ alphaOf(args),
90
+ ]),
91
+ oklch: (args) => {
92
+ const chroma = channel(args[1], 0.4);
93
+ const angle = (hue(args[2]) * Math.PI) / 180;
94
+ return oklabToRgba([
95
+ channel(args[0], 1),
96
+ chroma * Math.cos(angle),
97
+ chroma * Math.sin(angle),
98
+ alphaOf(args),
99
+ ]);
100
+ },
101
+ };
102
+
103
+ function parseHex(hex: string): Rgba | undefined {
104
+ const digits =
105
+ hex.length <= 4 ? [...hex].map((digit) => digit + digit).join("") : hex;
106
+ if (!/^[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(digits)) {
107
+ return undefined;
108
+ }
109
+ function byte(index: number) {
110
+ return Number.parseInt(digits.slice(index, index + 2), 16) / 255;
111
+ }
112
+ return {
113
+ r: byte(0),
114
+ g: byte(2),
115
+ b: byte(4),
116
+ alpha: digits.length === 8 ? byte(6) : 1,
117
+ };
118
+ }
119
+
120
+ /** A colour written out in full; `undefined` for what only the page can resolve (`var()`, names). */
121
+ export function parseColor(text: string): Rgba | undefined {
122
+ const trimmed = text.trim();
123
+ if (trimmed.toLowerCase() === "transparent") {
124
+ return { r: 0, g: 0, b: 0, alpha: 0 };
125
+ }
126
+ if (trimmed.startsWith("#")) {
127
+ return parseHex(trimmed.slice(1));
128
+ }
129
+ const match = FUNCTION.exec(trimmed);
130
+ if (match === null) {
131
+ return undefined;
132
+ }
133
+ const name = (match[1] as string).toLowerCase().replace(/a$/, "");
134
+ const args = (match[2] as string).split(/[\s,/]+/).filter(Boolean);
135
+ return PARSERS[name]?.(args);
136
+ }