@voila.dev/motion 0.0.0-stage → 5.0.113

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,162 @@
1
+ /**
2
+ * A path in four commands, all absolute: H and V become L, S and T their full
3
+ * curves with the reflected control point, so the flattening downstream only
4
+ * ever meets lines, curves, arcs and closes.
5
+ */
6
+ export type PathCommand =
7
+ | { readonly type: "M" | "L"; readonly values: readonly [number, number] }
8
+ | { readonly type: "C"; readonly values: readonly number[] }
9
+ | { readonly type: "Q"; readonly values: readonly number[] }
10
+ | { readonly type: "A"; readonly values: readonly number[] }
11
+ | { readonly type: "Z"; readonly values: readonly [] };
12
+
13
+ interface State {
14
+ x: number;
15
+ y: number;
16
+ startX: number;
17
+ startY: number;
18
+ /** The last curve's second control point, for S and T to reflect. */
19
+ controlX: number;
20
+ controlY: number;
21
+ previous: string;
22
+ }
23
+
24
+ type Handler = (args: number[], state: State) => PathCommand;
25
+
26
+ const ARGUMENTS: Readonly<Record<string, number>> = {
27
+ M: 2,
28
+ L: 2,
29
+ H: 1,
30
+ V: 1,
31
+ C: 6,
32
+ S: 4,
33
+ Q: 4,
34
+ T: 2,
35
+ A: 7,
36
+ Z: 0,
37
+ };
38
+ const NUMBER = /\s*,?\s*(-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)/iy;
39
+ const FLAG = /\s*,?\s*([01])/y;
40
+ const COMMAND = /\s*,?\s*([MLHVCSQTAZ])/iy;
41
+
42
+ function reflect(state: State, curve: string): [number, number] {
43
+ return state.previous === curve
44
+ ? [2 * state.x - state.controlX, 2 * state.y - state.controlY]
45
+ : [state.x, state.y];
46
+ }
47
+
48
+ function moveTo(state: State, x: number, y: number) {
49
+ state.x = x;
50
+ state.y = y;
51
+ }
52
+
53
+ const HANDLERS: Readonly<Record<string, Handler>> = {
54
+ M: ([x, y], state) => {
55
+ moveTo(state, x as number, y as number);
56
+ state.startX = state.x;
57
+ state.startY = state.y;
58
+ return { type: "M", values: [state.x, state.y] };
59
+ },
60
+ L: ([x, y], state) => {
61
+ moveTo(state, x as number, y as number);
62
+ return { type: "L", values: [state.x, state.y] };
63
+ },
64
+ H: ([x], state) => HANDLERS.L?.([x as number, state.y], state) as PathCommand,
65
+ V: ([y], state) => HANDLERS.L?.([state.x, y as number], state) as PathCommand,
66
+ C: (args, state) => {
67
+ [state.controlX, state.controlY] = [args[2] as number, args[3] as number];
68
+ moveTo(state, args[4] as number, args[5] as number);
69
+ return { type: "C", values: args };
70
+ },
71
+ S: (args, state) =>
72
+ HANDLERS.C?.([...reflect(state, "C"), ...args], state) as PathCommand,
73
+ Q: (args, state) => {
74
+ [state.controlX, state.controlY] = [args[0] as number, args[1] as number];
75
+ moveTo(state, args[2] as number, args[3] as number);
76
+ return { type: "Q", values: args };
77
+ },
78
+ T: (args, state) =>
79
+ HANDLERS.Q?.([...reflect(state, "Q"), ...args], state) as PathCommand,
80
+ A: (args, state) => {
81
+ moveTo(state, args[5] as number, args[6] as number);
82
+ return { type: "A", values: args };
83
+ },
84
+ Z: (_args, state) => {
85
+ moveTo(state, state.startX, state.startY);
86
+ return { type: "Z", values: [] };
87
+ },
88
+ };
89
+
90
+ /** Each argument's axis when relative: 0 adds x, 1 adds y, -1 is not a coordinate. */
91
+ const AXES: Readonly<Record<string, readonly number[]>> = {
92
+ M: [0, 1],
93
+ L: [0, 1],
94
+ H: [0],
95
+ V: [1],
96
+ C: [0, 1, 0, 1, 0, 1],
97
+ S: [0, 1, 0, 1],
98
+ Q: [0, 1, 0, 1],
99
+ T: [0, 1],
100
+ A: [-1, -1, -1, -1, -1, 0, 1],
101
+ Z: [],
102
+ };
103
+ const CURVE_FAMILY: Readonly<Record<string, string>> = { S: "C", T: "Q" };
104
+
105
+ function absolute(type: string, args: number[], state: State): number[] {
106
+ const axes = AXES[type] as readonly number[];
107
+ return args.map((value, index) => {
108
+ const axis = axes[index];
109
+ return axis === -1 ? value : value + (axis === 0 ? state.x : state.y);
110
+ });
111
+ }
112
+
113
+ function read(pattern: RegExp, text: string, at: number) {
114
+ pattern.lastIndex = at;
115
+ const match = pattern.exec(text);
116
+ return match === null
117
+ ? undefined
118
+ : { value: match[1] as string, end: pattern.lastIndex };
119
+ }
120
+
121
+ /** Parses `d` to absolute commands; `undefined` when it is not a valid path. */
122
+ export function parsePath(d: string): PathCommand[] | undefined {
123
+ const state: State = {
124
+ x: 0,
125
+ y: 0,
126
+ startX: 0,
127
+ startY: 0,
128
+ controlX: 0,
129
+ controlY: 0,
130
+ previous: "",
131
+ };
132
+ const commands: PathCommand[] = [];
133
+ let at = 0;
134
+ let command = read(COMMAND, d, at);
135
+ if (command === undefined || command.value.toUpperCase() !== "M") {
136
+ return undefined;
137
+ }
138
+ while (command !== undefined) {
139
+ at = command.end;
140
+ let letter = command.value;
141
+ do {
142
+ const type = letter.toUpperCase();
143
+ const args: number[] = [];
144
+ for (let index = 0; index < (ARGUMENTS[type] as number); index += 1) {
145
+ const flag = type === "A" && (index === 3 || index === 4);
146
+ const token = read(flag ? FLAG : NUMBER, d, at);
147
+ if (token === undefined) {
148
+ return undefined;
149
+ }
150
+ args.push(Number(token.value));
151
+ at = token.end;
152
+ }
153
+ const values = letter === type ? args : absolute(type, args, state);
154
+ commands.push((HANDLERS[type] as Handler)(values, state));
155
+ state.previous = CURVE_FAMILY[type] ?? type;
156
+ // After a moveto, further pairs are implicit linetos.
157
+ letter = type === "M" ? (letter === "M" ? "L" : "l") : letter;
158
+ } while (letter.toUpperCase() !== "Z" && read(NUMBER, d, at) !== undefined);
159
+ command = read(COMMAND, d, at);
160
+ }
161
+ return /^\s*$/.test(d.slice(at)) ? commands : undefined;
162
+ }
@@ -0,0 +1,151 @@
1
+ import type { Point, Ring } from "#/interpolate/path/flatten.ts";
2
+
3
+ function distance(a: Point, b: Point): number {
4
+ return Math.hypot(a[0] - b[0], a[1] - b[1]);
5
+ }
6
+
7
+ /** `count` points evenly spaced by arc length; a closed ring includes its closing edge. */
8
+ export function resample(ring: Ring, count: number): Point[] {
9
+ const points = ring.closed
10
+ ? [...ring.points, ring.points[0] as Point]
11
+ : ring.points;
12
+ const lengths = [0];
13
+ for (let index = 1; index < points.length; index += 1) {
14
+ lengths.push(
15
+ (lengths[index - 1] as number) +
16
+ distance(points[index - 1] as Point, points[index] as Point),
17
+ );
18
+ }
19
+ const total = lengths[lengths.length - 1] as number;
20
+ if (points.length < 2 || total === 0) {
21
+ return Array.from({ length: count }, () => points[0] as Point);
22
+ }
23
+ const step = total / (ring.closed ? count : Math.max(1, count - 1));
24
+ const result: Point[] = [];
25
+ let segment = 1;
26
+ for (let index = 0; index < count; index += 1) {
27
+ const along = Math.min(total, index * step);
28
+ while (
29
+ segment < points.length - 1 &&
30
+ (lengths[segment] as number) < along
31
+ ) {
32
+ segment += 1;
33
+ }
34
+ const [a, b] = [points[segment - 1] as Point, points[segment] as Point];
35
+ const span =
36
+ (lengths[segment] as number) - (lengths[segment - 1] as number);
37
+ const t =
38
+ span === 0 ? 0 : (along - (lengths[segment - 1] as number)) / span;
39
+ result.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
40
+ }
41
+ return result;
42
+ }
43
+
44
+ export function area(points: readonly Point[]): number {
45
+ let sum = 0;
46
+ points.forEach((point, index) => {
47
+ const next = points[(index + 1) % points.length] as Point;
48
+ sum += point[0] * next[1] - next[0] * point[1];
49
+ });
50
+ return sum / 2;
51
+ }
52
+
53
+ export function centroid(points: readonly Point[]): Point {
54
+ const sum = points.reduce<[number, number]>(
55
+ (total, [x, y]) => [total[0] + x, total[1] + y],
56
+ [0, 0],
57
+ );
58
+ return [sum[0] / points.length, sum[1] / points.length];
59
+ }
60
+
61
+ function cost(
62
+ a: readonly Point[],
63
+ b: readonly Point[],
64
+ offset: number,
65
+ ): number {
66
+ let sum = 0;
67
+ for (let index = 0; index < a.length; index += 1) {
68
+ const other = b[(index + offset) % b.length] as Point;
69
+ const point = a[index] as Point;
70
+ sum += (point[0] - other[0]) ** 2 + (point[1] - other[1]) ** 2;
71
+ }
72
+ return sum;
73
+ }
74
+
75
+ /**
76
+ * `to` rearranged so each point travels as little as it can to its partner
77
+ * in `from`: a closed ring may start anywhere and run either way round, an
78
+ * open one may only run backwards. Both have the same length.
79
+ */
80
+ export function alignRing(
81
+ from: readonly Point[],
82
+ to: readonly Point[],
83
+ closed: boolean,
84
+ ): Point[] {
85
+ const candidates = [to, [...to].reverse()];
86
+ let best = {
87
+ points: to as readonly Point[],
88
+ offset: 0,
89
+ cost: Number.POSITIVE_INFINITY,
90
+ };
91
+ for (const points of candidates) {
92
+ const offsets = closed ? points.length : 1;
93
+ for (let offset = 0; offset < offsets; offset += 1) {
94
+ const value = cost(from, points, offset);
95
+ if (value < best.cost) {
96
+ best = { points, offset, cost: value };
97
+ }
98
+ }
99
+ }
100
+ return best.points.map(
101
+ (_point, index) =>
102
+ best.points[(index + best.offset) % best.points.length] as Point,
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Pairs each ring of one shape with a ring of the other, nearest centroids and
108
+ * closest areas first. A ring left over pairs with `undefined`: it grows from,
109
+ * or shrinks to, its own centroid.
110
+ */
111
+ function describe(ring: Ring) {
112
+ return { centre: centroid(ring.points), size: Math.abs(area(ring.points)) };
113
+ }
114
+
115
+ export function matchRings(
116
+ from: readonly Ring[],
117
+ to: readonly Ring[],
118
+ ): [Ring | undefined, Ring | undefined][] {
119
+ const starts = from.map(describe);
120
+ const ends = to.map(describe);
121
+ const pairs = starts.flatMap((start, i) =>
122
+ ends.map((end, j) => {
123
+ const scale = Math.sqrt(Math.max(start.size, end.size, 1));
124
+ return {
125
+ i,
126
+ j,
127
+ score:
128
+ distance(start.centre, end.centre) / scale +
129
+ Math.abs(Math.log((start.size + 1) / (end.size + 1))),
130
+ };
131
+ }),
132
+ );
133
+ pairs.sort((a, b) => a.score - b.score);
134
+ const usedFrom = new Set<number>();
135
+ const usedTo = new Set<number>();
136
+ const result: [Ring | undefined, Ring | undefined][] = [];
137
+ for (const { i, j } of pairs) {
138
+ if (!usedFrom.has(i) && !usedTo.has(j)) {
139
+ usedFrom.add(i);
140
+ usedTo.add(j);
141
+ result.push([from[i], to[j]]);
142
+ }
143
+ }
144
+ for (const [i, ring] of from.entries()) {
145
+ if (!usedFrom.has(i)) result.push([ring, undefined]);
146
+ }
147
+ for (const [j, ring] of to.entries()) {
148
+ if (!usedTo.has(j)) result.push([undefined, ring]);
149
+ }
150
+ return result;
151
+ }
@@ -0,0 +1,11 @@
1
+ export {
2
+ type Exit,
3
+ Presence,
4
+ type PresenceProps,
5
+ usePresence,
6
+ } from "#/react/presence.tsx";
7
+ export { type AnimationScope, useAnimate } from "#/react/use-animate.ts";
8
+ export { useMotionValue } from "#/react/use-motion-value.ts";
9
+ export { useReducedMotion } from "#/react/use-reduced-motion.ts";
10
+ export { useSpring } from "#/react/use-spring.ts";
11
+ export { useTransform } from "#/react/use-transform.ts";
@@ -0,0 +1,194 @@
1
+ import * as React from "react";
2
+
3
+ /** Plays a child's way out; Presence removes it once the returned promise settles. */
4
+ export type Exit = (element: Element) => PromiseLike<unknown> | undefined;
5
+
6
+ declare module "react" {
7
+ interface Attributes {
8
+ /** Inside a `<Presence>`: plays this child's way out before it unmounts. */
9
+ exit?: Exit;
10
+ }
11
+ }
12
+
13
+ interface PresenceState {
14
+ readonly isPresent: boolean;
15
+ /** For a child that animates its own exit: tells Presence it may go. */
16
+ readonly safeToRemove: () => void;
17
+ /** Called by `usePresence`: Presence then waits for `safeToRemove`. */
18
+ readonly register: () => () => void;
19
+ }
20
+
21
+ const PresenceContext = React.createContext<PresenceState | null>(null);
22
+
23
+ /**
24
+ * Whether this component is still wanted, and the function that lets it go
25
+ * once its own exit has played. Outside a Presence it is always present.
26
+ */
27
+ export function usePresence(): readonly [boolean, () => void] {
28
+ const state = React.useContext(PresenceContext);
29
+ const register = state?.register;
30
+ React.useLayoutEffect(() => register?.(), [register]);
31
+ return [state?.isPresent ?? true, state?.safeToRemove ?? noop];
32
+ }
33
+
34
+ function noop() {}
35
+
36
+ type Keyed = React.ReactElement<{ exit?: Exit; ref?: React.Ref<Element> }>;
37
+
38
+ function keyedChildren(children: React.ReactNode): Keyed[] {
39
+ return React.Children.toArray(children).filter((child): child is Keyed =>
40
+ React.isValidElement(child),
41
+ );
42
+ }
43
+
44
+ /** The new children, with the ones that left kept where they were. */
45
+ function merge(previous: readonly Keyed[], current: readonly Keyed[]): Keyed[] {
46
+ const keys = new Set(current.map((child) => child.key));
47
+ const merged = [...current];
48
+ for (const [index, child] of previous.entries()) {
49
+ if (keys.has(child.key)) continue;
50
+ const before = previous[index - 1]?.key;
51
+ const at = merged.findIndex((other) => other.key === before);
52
+ merged.splice(at + 1, 0, child);
53
+ }
54
+ return merged;
55
+ }
56
+
57
+ interface Slot {
58
+ ownRef: React.Ref<Element> | undefined;
59
+ readonly ref: (element: Element | null) => void;
60
+ readonly present: PresenceState;
61
+ readonly absent: PresenceState;
62
+ }
63
+
64
+ export interface PresenceProps {
65
+ readonly children?: React.ReactNode;
66
+ /** Called each time the last exiting child is gone. */
67
+ readonly onExitComplete?: () => void;
68
+ }
69
+
70
+ /**
71
+ * Keeps a removed child on screen until its exit has played: the `exit`
72
+ * prop it carries (given the child's element, through its ref), or the
73
+ * child's own `usePresence`. Children need a stable `key`. An exiting child
74
+ * is inert: it can't take focus or clicks on its way out.
75
+ */
76
+ export function Presence({ children, onExitComplete }: PresenceProps) {
77
+ const current = keyedChildren(children);
78
+ const rendered = React.useRef<Keyed[]>(current);
79
+ const gone = React.useRef(new Set<string | null>());
80
+ const elements = React.useRef(new Map<string | null, Element>());
81
+ const registered = React.useRef(new Map<string | null, number>());
82
+ const [, rerender] = React.useReducer((count: number) => count + 1, 0);
83
+
84
+ const currentKeys = new Set(current.map((child) => child.key));
85
+ const present = React.useRef(currentKeys);
86
+ present.current = currentKeys;
87
+ /** Exits under way, so a later exit doesn't restart them, and a return can undo them. */
88
+ const leaving = React.useRef(
89
+ new Map<string | null, PromiseLike<unknown> | undefined>(),
90
+ );
91
+ const shown = merge(
92
+ rendered.current.filter((child) => !gone.current.has(child.key)),
93
+ current,
94
+ );
95
+ for (const key of currentKeys) gone.current.delete(key);
96
+ rendered.current = shown;
97
+ const exiting = shown.filter((child) => !currentKeys.has(child.key));
98
+
99
+ const remove = React.useCallback(
100
+ (key: string | null) => {
101
+ // Gone already, or back before its exit finished.
102
+ if (gone.current.has(key) || present.current.has(key)) return;
103
+ gone.current.add(key);
104
+ leaving.current.delete(key);
105
+ rerender();
106
+ const left = rendered.current.some(
107
+ (child) =>
108
+ !gone.current.has(child.key) && !present.current.has(child.key),
109
+ );
110
+ if (!left) onExitComplete?.();
111
+ },
112
+ [onExitComplete],
113
+ );
114
+
115
+ const exitingKeys = exiting.map((child) => child.key).join("\u0000");
116
+ // biome-ignore lint/correctness/useExhaustiveDependencies: keyed on the set of exiting children.
117
+ React.useLayoutEffect(() => {
118
+ for (const [key, played] of leaving.current) {
119
+ if (!present.current.has(key)) continue;
120
+ leaving.current.delete(key);
121
+ elements.current.get(key)?.removeAttribute("inert");
122
+ (played as { cancel?: () => void } | undefined)?.cancel?.();
123
+ }
124
+ for (const child of exiting) {
125
+ if (leaving.current.has(child.key)) continue;
126
+ const element = elements.current.get(child.key);
127
+ element?.setAttribute("inert", "");
128
+ const played = element && child.props.exit?.(element);
129
+ leaving.current.set(child.key, played);
130
+ if (played) {
131
+ Promise.resolve(played).then(
132
+ () => remove(child.key),
133
+ () => remove(child.key),
134
+ );
135
+ } else if (!registered.current.get(child.key)) {
136
+ remove(child.key);
137
+ }
138
+ }
139
+ }, [exitingKeys]);
140
+
141
+ const removeRef = React.useRef(remove);
142
+ removeRef.current = remove;
143
+ const slots = React.useRef(new Map<string | null, Slot>());
144
+
145
+ /**
146
+ * Stable per key: the ref React sees and the context values never change
147
+ * identity between renders, so a child's own callback ref isn't detached
148
+ * and reattached each time, and `usePresence` consumers only re-render
149
+ * when their presence flips.
150
+ */
151
+ function slotFor(key: string | null): Slot {
152
+ const existing = slots.current.get(key);
153
+ if (existing) return existing;
154
+ function register() {
155
+ registered.current.set(key, (registered.current.get(key) ?? 0) + 1);
156
+ return () => {
157
+ registered.current.set(key, (registered.current.get(key) ?? 1) - 1);
158
+ };
159
+ }
160
+ function safeToRemove() {
161
+ removeRef.current(key);
162
+ }
163
+ const slot: Slot = {
164
+ ownRef: undefined,
165
+ ref(element) {
166
+ if (element) elements.current.set(key, element);
167
+ else elements.current.delete(key);
168
+ const ownRef = slot.ownRef;
169
+ if (typeof ownRef === "function") ownRef(element);
170
+ else if (ownRef)
171
+ (ownRef as React.RefObject<Element | null>).current = element;
172
+ },
173
+ present: { isPresent: true, safeToRemove, register },
174
+ absent: { isPresent: false, safeToRemove, register },
175
+ };
176
+ slots.current.set(key, slot);
177
+ return slot;
178
+ }
179
+
180
+ return shown.map((child) => {
181
+ const key = child.key;
182
+ const { exit: _exit, ref: ownRef, ...props } = child.props;
183
+ const slot = slotFor(key);
184
+ slot.ownRef = ownRef;
185
+ return (
186
+ <PresenceContext.Provider
187
+ key={key}
188
+ value={currentKeys.has(key) ? slot.present : slot.absent}
189
+ >
190
+ {React.createElement(child.type, { ...props, ref: slot.ref })}
191
+ </PresenceContext.Provider>
192
+ );
193
+ });
194
+ }
@@ -0,0 +1,46 @@
1
+ import * as React from "react";
2
+ import { type Animate, createAnimate } from "#/animate/animate.ts";
3
+ import type { AnimationControls } from "#/animate/types.ts";
4
+
5
+ export interface AnimationScope<T extends Element = Element> {
6
+ current: T | null;
7
+ /** The animations started through this scope and still running. */
8
+ readonly animations: AnimationControls[];
9
+ }
10
+
11
+ /**
12
+ * A ref to put on an element, and an `animate` whose selectors only match
13
+ * inside it. Every animation started through it stops on unmount, so a
14
+ * component never animates a node React already removed.
15
+ */
16
+ export function useAnimate<T extends Element = HTMLElement>(): readonly [
17
+ AnimationScope<T>,
18
+ Animate,
19
+ ] {
20
+ const [scope] = React.useState<AnimationScope<T>>(() => ({
21
+ current: null,
22
+ animations: [],
23
+ }));
24
+ const [scoped] = React.useState(() => {
25
+ const base = createAnimate(() => scope.current);
26
+ function tracked(...args: Parameters<typeof base>): AnimationControls {
27
+ const controls = (base as (...a: unknown[]) => AnimationControls)(
28
+ ...args,
29
+ );
30
+ scope.animations.push(controls);
31
+ controls.finished.then(() => {
32
+ const index = scope.animations.indexOf(controls);
33
+ if (index !== -1) scope.animations.splice(index, 1);
34
+ });
35
+ return controls;
36
+ }
37
+ return tracked as Animate;
38
+ });
39
+ React.useEffect(
40
+ () => () => {
41
+ for (const controls of scope.animations.splice(0)) controls.stop();
42
+ },
43
+ [scope],
44
+ );
45
+ return [scope, scoped] as const;
46
+ }
@@ -0,0 +1,9 @@
1
+ import * as React from "react";
2
+ import { type MotionValue, motionValue } from "#/value/motion-value.ts";
3
+
4
+ /** A motion value that lives as long as the component, created once. */
5
+ export function useMotionValue<T>(initial: T): MotionValue<T> {
6
+ const [value] = React.useState(() => motionValue(initial));
7
+ React.useEffect(() => () => value.stop(), [value]);
8
+ return value;
9
+ }
@@ -0,0 +1,22 @@
1
+ import * as React from "react";
2
+ import {
3
+ onReducedMotionChange,
4
+ shouldReduceMotion,
5
+ } from "#/accessibility/reduced-motion.ts";
6
+
7
+ function serverSnapshot(): boolean {
8
+ return false;
9
+ }
10
+
11
+ /**
12
+ * Whether motion should be reduced, following the global policy and the
13
+ * reader's setting as either changes. False on the server: the first client
14
+ * render matches, then corrects itself.
15
+ */
16
+ export function useReducedMotion(): boolean {
17
+ return React.useSyncExternalStore(
18
+ onReducedMotionChange,
19
+ () => shouldReduceMotion(),
20
+ serverSnapshot,
21
+ );
22
+ }
@@ -0,0 +1,35 @@
1
+ import * as React from "react";
2
+ import { animate } from "#/animate/animate.ts";
3
+ import type { AnimationOptions } from "#/animate/types.ts";
4
+ import { useMotionValue } from "#/react/use-motion-value.ts";
5
+ import { isMotionValue, type MotionValue } from "#/value/motion-value.ts";
6
+
7
+ /**
8
+ * A value that springs towards `target` whenever it changes: a number from
9
+ * props, or another motion value it follows. A new target mid-flight keeps
10
+ * the speed the value has.
11
+ */
12
+ export function useSpring(
13
+ target: number | MotionValue<number>,
14
+ options: AnimationOptions = {},
15
+ ): MotionValue<number> {
16
+ const value = useMotionValue(
17
+ isMotionValue(target) ? (target as MotionValue<number>).get() : target,
18
+ );
19
+ const optionsRef = React.useRef(options);
20
+ optionsRef.current = options;
21
+
22
+ React.useEffect(() => {
23
+ function follow(next: number) {
24
+ animate(value, next, { ...optionsRef.current, type: "spring" });
25
+ }
26
+ if (!isMotionValue(target)) {
27
+ if (value.get() !== target) follow(target);
28
+ return;
29
+ }
30
+ const source = target as MotionValue<number>;
31
+ return source.on("change", follow);
32
+ }, [target, value]);
33
+
34
+ return value;
35
+ }