@rafaelaugusto/jolt 0.1.0-beta.0 → 0.1.0-beta.10

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 (307) hide show
  1. package/README.md +441 -25
  2. package/dist/animate.d.ts +21 -5
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +205 -25
  5. package/dist/animate.js.map +1 -1
  6. package/dist/color.d.ts +11 -0
  7. package/dist/color.d.ts.map +1 -0
  8. package/dist/color.js +73 -0
  9. package/dist/color.js.map +1 -0
  10. package/dist/config.d.ts +31 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +73 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/decay.d.ts +11 -0
  15. package/dist/decay.d.ts.map +1 -0
  16. package/dist/decay.js +22 -0
  17. package/dist/decay.js.map +1 -0
  18. package/dist/decoration.d.ts +18 -0
  19. package/dist/decoration.d.ts.map +1 -0
  20. package/dist/decoration.js +155 -0
  21. package/dist/decoration.js.map +1 -0
  22. package/dist/drag-input.d.ts +15 -0
  23. package/dist/drag-input.d.ts.map +1 -0
  24. package/dist/drag-input.js +66 -0
  25. package/dist/drag-input.js.map +1 -0
  26. package/dist/drag-physics.d.ts +20 -0
  27. package/dist/drag-physics.d.ts.map +1 -0
  28. package/dist/drag-physics.js +75 -0
  29. package/dist/drag-physics.js.map +1 -0
  30. package/dist/drag.d.ts +58 -0
  31. package/dist/drag.d.ts.map +1 -0
  32. package/dist/drag.js +324 -0
  33. package/dist/drag.js.map +1 -0
  34. package/dist/driver/android-color.d.ts +20 -0
  35. package/dist/driver/android-color.d.ts.map +1 -0
  36. package/dist/driver/android-color.js +136 -0
  37. package/dist/driver/android-color.js.map +1 -0
  38. package/dist/driver/android-decoration.d.ts +22 -0
  39. package/dist/driver/android-decoration.d.ts.map +1 -0
  40. package/dist/driver/android-decoration.js +139 -0
  41. package/dist/driver/android-decoration.js.map +1 -0
  42. package/dist/driver/android-playback.d.ts +13 -0
  43. package/dist/driver/android-playback.d.ts.map +1 -0
  44. package/dist/driver/android-playback.js +141 -0
  45. package/dist/driver/android-playback.js.map +1 -0
  46. package/dist/driver/android.d.ts +1 -1
  47. package/dist/driver/android.d.ts.map +1 -1
  48. package/dist/driver/android.js +165 -13
  49. package/dist/driver/android.js.map +1 -1
  50. package/dist/driver/index.d.ts +25 -2
  51. package/dist/driver/index.d.ts.map +1 -1
  52. package/dist/driver/index.js.map +1 -1
  53. package/dist/driver/ios-color.d.ts +16 -0
  54. package/dist/driver/ios-color.d.ts.map +1 -0
  55. package/dist/driver/ios-color.js +392 -0
  56. package/dist/driver/ios-color.js.map +1 -0
  57. package/dist/driver/ios-decoration.d.ts +17 -0
  58. package/dist/driver/ios-decoration.d.ts.map +1 -0
  59. package/dist/driver/ios-decoration.js +271 -0
  60. package/dist/driver/ios-decoration.js.map +1 -0
  61. package/dist/driver/ios-layout.d.ts +9 -0
  62. package/dist/driver/ios-layout.d.ts.map +1 -0
  63. package/dist/driver/ios-layout.js +52 -0
  64. package/dist/driver/ios-layout.js.map +1 -0
  65. package/dist/driver/ios-playback.d.ts +10 -0
  66. package/dist/driver/ios-playback.d.ts.map +1 -0
  67. package/dist/driver/ios-playback.js +47 -0
  68. package/dist/driver/ios-playback.js.map +1 -0
  69. package/dist/driver/ios-shadow.d.ts +6 -0
  70. package/dist/driver/ios-shadow.d.ts.map +1 -0
  71. package/dist/driver/ios-shadow.js +52 -0
  72. package/dist/driver/ios-shadow.js.map +1 -0
  73. package/dist/driver/ios.d.ts.map +1 -1
  74. package/dist/driver/ios.js +164 -20
  75. package/dist/driver/ios.js.map +1 -1
  76. package/dist/engine.d.ts +76 -5
  77. package/dist/engine.d.ts.map +1 -1
  78. package/dist/engine.js +303 -23
  79. package/dist/engine.js.map +1 -1
  80. package/dist/haptics.d.ts +21 -0
  81. package/dist/haptics.d.ts.map +1 -0
  82. package/dist/haptics.js +120 -0
  83. package/dist/haptics.js.map +1 -0
  84. package/dist/image-effect-native.d.ts +26 -0
  85. package/dist/image-effect-native.d.ts.map +1 -0
  86. package/dist/image-effect-native.js +73 -0
  87. package/dist/image-effect-native.js.map +1 -0
  88. package/dist/image-effect.d.ts +50 -0
  89. package/dist/image-effect.d.ts.map +1 -0
  90. package/dist/image-effect.js +290 -0
  91. package/dist/image-effect.js.map +1 -0
  92. package/dist/image-owner.d.ts +3 -0
  93. package/dist/image-owner.d.ts.map +1 -0
  94. package/dist/image-owner.js +11 -0
  95. package/dist/image-owner.js.map +1 -0
  96. package/dist/index.d.ts +45 -4
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +21 -2
  99. package/dist/index.js.map +1 -1
  100. package/dist/interaction-surface.d.ts +12 -0
  101. package/dist/interaction-surface.d.ts.map +1 -0
  102. package/dist/interaction-surface.js +36 -0
  103. package/dist/interaction-surface.js.map +1 -0
  104. package/dist/items.d.ts +20 -0
  105. package/dist/items.d.ts.map +1 -0
  106. package/dist/items.js +109 -0
  107. package/dist/items.js.map +1 -0
  108. package/dist/keyboard.d.ts +41 -0
  109. package/dist/keyboard.d.ts.map +1 -0
  110. package/dist/keyboard.js +256 -0
  111. package/dist/keyboard.js.map +1 -0
  112. package/dist/label.d.ts +7 -0
  113. package/dist/label.d.ts.map +1 -0
  114. package/dist/label.js +60 -0
  115. package/dist/label.js.map +1 -0
  116. package/dist/layout.d.ts +13 -0
  117. package/dist/layout.d.ts.map +1 -0
  118. package/dist/layout.js +348 -0
  119. package/dist/layout.js.map +1 -0
  120. package/dist/liquid-grid.d.ts +3 -0
  121. package/dist/liquid-grid.d.ts.map +1 -0
  122. package/dist/liquid-grid.js +24 -0
  123. package/dist/liquid-grid.js.map +1 -0
  124. package/dist/liquid.d.ts +18 -0
  125. package/dist/liquid.d.ts.map +1 -0
  126. package/dist/liquid.js +234 -0
  127. package/dist/liquid.js.map +1 -0
  128. package/dist/loader.d.ts +19 -0
  129. package/dist/loader.d.ts.map +1 -0
  130. package/dist/loader.js +173 -0
  131. package/dist/loader.js.map +1 -0
  132. package/dist/motion-scope.d.ts +39 -0
  133. package/dist/motion-scope.d.ts.map +1 -0
  134. package/dist/motion-scope.js +238 -0
  135. package/dist/motion-scope.js.map +1 -0
  136. package/dist/motion-states.d.ts +36 -0
  137. package/dist/motion-states.d.ts.map +1 -0
  138. package/dist/motion-states.js +165 -0
  139. package/dist/motion-states.js.map +1 -0
  140. package/dist/particles.d.ts +39 -0
  141. package/dist/particles.d.ts.map +1 -0
  142. package/dist/particles.js +265 -0
  143. package/dist/particles.js.map +1 -0
  144. package/dist/path.d.ts +12 -0
  145. package/dist/path.d.ts.map +1 -0
  146. package/dist/path.js +90 -0
  147. package/dist/path.js.map +1 -0
  148. package/dist/playback-group.d.ts +6 -0
  149. package/dist/playback-group.d.ts.map +1 -0
  150. package/dist/playback-group.js +141 -0
  151. package/dist/playback-group.js.map +1 -0
  152. package/dist/playback.d.ts +25 -0
  153. package/dist/playback.d.ts.map +1 -0
  154. package/dist/playback.js +35 -0
  155. package/dist/playback.js.map +1 -0
  156. package/dist/presets.d.ts +16 -1
  157. package/dist/presets.d.ts.map +1 -1
  158. package/dist/presets.js +24 -6
  159. package/dist/presets.js.map +1 -1
  160. package/dist/press.d.ts.map +1 -1
  161. package/dist/press.js +25 -7
  162. package/dist/press.js.map +1 -1
  163. package/dist/properties.d.ts +6 -3
  164. package/dist/properties.d.ts.map +1 -1
  165. package/dist/properties.js +26 -2
  166. package/dist/properties.js.map +1 -1
  167. package/dist/reveal.d.ts +25 -0
  168. package/dist/reveal.d.ts.map +1 -0
  169. package/dist/reveal.js +294 -0
  170. package/dist/reveal.js.map +1 -0
  171. package/dist/roll-diff.d.ts +22 -0
  172. package/dist/roll-diff.d.ts.map +1 -0
  173. package/dist/roll-diff.js +36 -0
  174. package/dist/roll-diff.js.map +1 -0
  175. package/dist/roll.d.ts +29 -0
  176. package/dist/roll.d.ts.map +1 -0
  177. package/dist/roll.js +442 -0
  178. package/dist/roll.js.map +1 -0
  179. package/dist/scroll-linked.d.ts +10 -0
  180. package/dist/scroll-linked.d.ts.map +1 -0
  181. package/dist/scroll-linked.js +58 -0
  182. package/dist/scroll-linked.js.map +1 -0
  183. package/dist/sequence.d.ts +13 -7
  184. package/dist/sequence.d.ts.map +1 -1
  185. package/dist/sequence.js +214 -19
  186. package/dist/sequence.js.map +1 -1
  187. package/dist/shimmer.d.ts +12 -0
  188. package/dist/shimmer.d.ts.map +1 -0
  189. package/dist/shimmer.js +161 -0
  190. package/dist/shimmer.js.map +1 -0
  191. package/dist/svg-path.d.ts +14 -0
  192. package/dist/svg-path.d.ts.map +1 -0
  193. package/dist/svg-path.js +281 -0
  194. package/dist/svg-path.js.map +1 -0
  195. package/dist/symbol-effect.d.ts +23 -0
  196. package/dist/symbol-effect.d.ts.map +1 -0
  197. package/dist/symbol-effect.js +187 -0
  198. package/dist/symbol-effect.js.map +1 -0
  199. package/dist/text-layout.d.ts +30 -0
  200. package/dist/text-layout.d.ts.map +1 -0
  201. package/dist/text-layout.js +185 -0
  202. package/dist/text-layout.js.map +1 -0
  203. package/dist/text.d.ts +17 -0
  204. package/dist/text.d.ts.map +1 -0
  205. package/dist/text.js +263 -0
  206. package/dist/text.js.map +1 -0
  207. package/dist/track.d.ts +3 -1
  208. package/dist/track.d.ts.map +1 -1
  209. package/dist/track.js +266 -13
  210. package/dist/track.js.map +1 -1
  211. package/dist/transition.d.ts +17 -1
  212. package/dist/transition.d.ts.map +1 -1
  213. package/dist/transition.js +41 -6
  214. package/dist/transition.js.map +1 -1
  215. package/dist/types.d.ts +50 -1
  216. package/dist/types.d.ts.map +1 -1
  217. package/dist/vue/index.d.ts +95 -0
  218. package/dist/vue/index.d.ts.map +1 -0
  219. package/dist/vue/index.js +163 -0
  220. package/dist/vue/index.js.map +1 -0
  221. package/dist/zoom-native.d.ts +30 -0
  222. package/dist/zoom-native.d.ts.map +1 -0
  223. package/dist/zoom-native.js +56 -0
  224. package/dist/zoom-native.js.map +1 -0
  225. package/dist/zoomable.d.ts +48 -0
  226. package/dist/zoomable.d.ts.map +1 -0
  227. package/dist/zoomable.js +249 -0
  228. package/dist/zoomable.js.map +1 -0
  229. package/package.json +33 -5
  230. package/platforms/android/include.gradle +7 -0
  231. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  232. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  233. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  234. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  235. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
  236. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  237. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  238. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  239. package/platforms/android/proguard-rules.pro +19 -0
  240. package/platforms/ios/build.xcconfig +1 -0
  241. package/platforms/ios/src/JoltImageEffect.h +25 -0
  242. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  243. package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
  244. package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
  245. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  246. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  247. package/platforms/ios/src/module.modulemap +6 -0
  248. package/src/android-plugin.d.ts +155 -0
  249. package/src/animate.ts +278 -38
  250. package/src/color.ts +101 -0
  251. package/src/config.ts +84 -0
  252. package/src/decay.ts +45 -0
  253. package/src/decoration.ts +180 -0
  254. package/src/drag-input.ts +74 -0
  255. package/src/drag-physics.ts +110 -0
  256. package/src/drag.ts +395 -0
  257. package/src/driver/android-color.ts +170 -0
  258. package/src/driver/android-decoration.ts +182 -0
  259. package/src/driver/android-playback.ts +157 -0
  260. package/src/driver/android.ts +208 -20
  261. package/src/driver/index.ts +37 -2
  262. package/src/driver/ios-color.ts +441 -0
  263. package/src/driver/ios-decoration.ts +332 -0
  264. package/src/driver/ios-layout.ts +63 -0
  265. package/src/driver/ios-playback.ts +51 -0
  266. package/src/driver/ios-shadow.ts +50 -0
  267. package/src/driver/ios.ts +194 -25
  268. package/src/engine.ts +329 -27
  269. package/src/haptics.ts +148 -0
  270. package/src/image-effect-native.ts +129 -0
  271. package/src/image-effect.ts +336 -0
  272. package/src/image-owner.ts +12 -0
  273. package/src/index.ts +61 -4
  274. package/src/interaction-surface.ts +41 -0
  275. package/src/ios-plugin.d.ts +81 -0
  276. package/src/items.ts +140 -0
  277. package/src/keyboard.ts +291 -0
  278. package/src/label.ts +76 -0
  279. package/src/layout.ts +408 -0
  280. package/src/liquid-grid.ts +28 -0
  281. package/src/liquid.ts +287 -0
  282. package/src/loader.ts +202 -0
  283. package/src/motion-scope.ts +279 -0
  284. package/src/motion-states.ts +215 -0
  285. package/src/particles.ts +323 -0
  286. package/src/path.ts +124 -0
  287. package/src/playback-group.ts +143 -0
  288. package/src/playback.ts +62 -0
  289. package/src/presets.ts +31 -7
  290. package/src/press.ts +32 -10
  291. package/src/properties.ts +32 -6
  292. package/src/reveal.ts +374 -0
  293. package/src/roll-diff.ts +57 -0
  294. package/src/roll.ts +515 -0
  295. package/src/scroll-linked.ts +73 -0
  296. package/src/sequence.ts +212 -19
  297. package/src/shimmer.ts +180 -0
  298. package/src/svg-path.ts +355 -0
  299. package/src/symbol-effect.ts +226 -0
  300. package/src/text-layout.ts +228 -0
  301. package/src/text.ts +279 -0
  302. package/src/track.ts +347 -12
  303. package/src/transition.ts +63 -6
  304. package/src/types.ts +51 -1
  305. package/src/vue/index.ts +231 -0
  306. package/src/zoom-native.ts +124 -0
  307. package/src/zoomable.ts +330 -0
@@ -0,0 +1,279 @@
1
+ import { animate, decay } from './animate.js'
2
+ import { press } from './press.js'
3
+ import { drag } from './drag.js'
4
+ import { animateLayout, type LayoutOptions } from './layout.js'
5
+ import type { SpringName } from './transition.js'
6
+ import { sequence } from './sequence.js'
7
+ import {
8
+ motionStates,
9
+ type MotionState,
10
+ type MotionDefinitions,
11
+ type MotionStates,
12
+ type MotionStatesOptions,
13
+ } from './motion-states.js'
14
+ import type { Target } from './types.js'
15
+
16
+ /** A cleanup function or a handle owned exclusively by one scope. */
17
+ export type MotionResource = (() => void) | { stop(): void } | { dispose(): void }
18
+ type Controlled = {
19
+ pause?: () => void
20
+ play?: () => void
21
+ readonly paused?: boolean
22
+ readonly finished?: Promise<unknown>
23
+ }
24
+ interface Entry {
25
+ resource?: MotionResource
26
+ setup?: () => MotionResource
27
+ resume: boolean
28
+ }
29
+ const owners = new WeakMap<object, object>()
30
+
31
+ export interface MotionScope {
32
+ readonly disposed: boolean
33
+ readonly suspended: boolean
34
+ /** Owned handles/subscriptions, including suspended subscription factories. */
35
+ readonly size: number
36
+ animateLayout: typeof animateLayout
37
+ animate: typeof animate
38
+ decay: typeof decay
39
+ sequence: typeof sequence
40
+ press: typeof press
41
+ drag: typeof drag
42
+ states<
43
+ T extends Record<string, Target>,
44
+ const S extends Record<string, MotionState<keyof T>>,
45
+ >(
46
+ targets: T,
47
+ definitions: MotionDefinitions<T, S>,
48
+ options?: Omit<MotionStatesOptions, 'scope'>,
49
+ ): MotionStates<Extract<keyof S, string>>
50
+ /** Adopt an existing handle. Completed animations are released automatically. */
51
+ add<T extends MotionResource>(resource: T): T
52
+ /** Attach a subscription, detach on suspension, and recreate on resume. */
53
+ use(setup: () => MotionResource): () => void
54
+ /** Pause playable handles; cancel non-playable ones; detach subscriptions. */
55
+ suspend(): void
56
+ resume(): void
57
+ /** Cancel all owned work without destroying the scope, e.g. when recycling a cell. */
58
+ reset(): void
59
+ dispose(): void
60
+ }
61
+
62
+ /** Framework-independent ownership. Native unload never permanently destroys a scope. */
63
+ export function motionScope(): MotionScope {
64
+ const entries = new Set<Entry>()
65
+ const adopted = new Map<MotionResource, Entry>()
66
+ const identity = {}
67
+ let disposed = false
68
+ let suspended = false
69
+ let clearing = false
70
+ const assertAlive = () => {
71
+ if (disposed) throw new Error('Motion scope is disposed.')
72
+ if (clearing) throw new Error('Motion scope is resetting.')
73
+ }
74
+ const assertActive = () => {
75
+ assertAlive()
76
+ if (suspended)
77
+ throw new Error('Motion scope is suspended; resume it before starting work.')
78
+ }
79
+ const validate = (resource: MotionResource) => {
80
+ if (
81
+ typeof resource !== 'function' &&
82
+ (!resource ||
83
+ (typeof (resource as { dispose?: unknown }).dispose !== 'function' &&
84
+ typeof (resource as { stop?: unknown }).stop !== 'function'))
85
+ )
86
+ throw new TypeError(
87
+ 'A motion resource must be a cleanup function or expose stop()/dispose().',
88
+ )
89
+ const finished = (resource as Controlled).finished
90
+ if (finished && typeof finished.then !== 'function')
91
+ throw new TypeError('Resource finished must be a promise.')
92
+ if (owners.has(resource) && owners.get(resource) !== identity)
93
+ throw new Error('Motion resource already belongs to another scope.')
94
+ }
95
+ const forget = (entry: Entry) => {
96
+ const resource = entry.resource
97
+ if (resource) {
98
+ owners.delete(resource)
99
+ adopted.delete(resource)
100
+ entry.resource = undefined
101
+ }
102
+ entry.resume = false
103
+ }
104
+ const disposeResource = (resource: MotionResource) => {
105
+ if (typeof resource === 'function') resource()
106
+ else if (typeof (resource as { dispose?: unknown }).dispose === 'function')
107
+ (resource as { dispose(): void }).dispose()
108
+ else (resource as { stop(): void }).stop()
109
+ }
110
+ const release = (entry: Entry) => {
111
+ const resource = entry.resource
112
+ forget(entry)
113
+ if (!resource) return
114
+ disposeResource(resource)
115
+ }
116
+ const remove = (entry: Entry) => {
117
+ entries.delete(entry)
118
+ release(entry)
119
+ }
120
+ const attach = (entry: Entry, resource: MotionResource) => {
121
+ validate(resource)
122
+ if (adopted.has(resource))
123
+ throw new Error('Subscription factory returned an already owned resource.')
124
+ entry.resource = resource
125
+ adopted.set(resource, entry)
126
+ owners.set(resource, identity)
127
+ const finished = (resource as Controlled).finished
128
+ if (!entry.setup && finished) {
129
+ const settled = () => {
130
+ if (entry.resource !== resource) return
131
+ forget(entry)
132
+ entries.delete(entry)
133
+ }
134
+ void finished.then(settled, settled)
135
+ }
136
+ }
137
+ const startSubscription = (entry: Entry) => {
138
+ const resource = entry.setup!()
139
+ validate(resource)
140
+ // Setup may synchronously navigate away, suspend or remove its own scope.
141
+ if (disposed || suspended || !entries.has(entry)) {
142
+ if (!owners.has(resource)) disposeResource(resource)
143
+ return
144
+ }
145
+ attach(entry, resource)
146
+ }
147
+ // One bad cleanup must not strand the other subscriptions.
148
+ const visit = (action: (entry: Entry) => void) => {
149
+ const errors: unknown[] = []
150
+ for (const entry of [...entries]) {
151
+ if (!entries.has(entry)) continue
152
+ try {
153
+ action(entry)
154
+ } catch (error) {
155
+ errors.push(error)
156
+ }
157
+ }
158
+ if (errors.length)
159
+ throw new AggregateError(errors, 'Motion scope cleanup/lifecycle failed.')
160
+ }
161
+ const startOwned = <T extends MotionResource>(factory: () => T): T => {
162
+ assertActive()
163
+ const resource = factory()
164
+ try {
165
+ return scope.add(resource)
166
+ } catch (error) {
167
+ if (!owners.has(resource)) disposeResource(resource)
168
+ throw error
169
+ }
170
+ }
171
+ const scope: MotionScope = {
172
+ get disposed() {
173
+ return disposed
174
+ },
175
+ get suspended() {
176
+ return suspended
177
+ },
178
+ get size() {
179
+ return entries.size
180
+ },
181
+ animateLayout(
182
+ container: Target,
183
+ changeOrOptions?: (() => void) | LayoutOptions | SpringName,
184
+ options?: LayoutOptions | SpringName,
185
+ ) {
186
+ return startOwned(() =>
187
+ typeof changeOrOptions === 'function'
188
+ ? animateLayout(container, changeOrOptions, options)
189
+ : animateLayout(container, changeOrOptions),
190
+ )
191
+ },
192
+ animate(...args) {
193
+ return startOwned(() => animate(...args))
194
+ },
195
+ decay(...args) {
196
+ return startOwned(() => decay(...args))
197
+ },
198
+ sequence(...args) {
199
+ return startOwned(() => sequence(...args))
200
+ },
201
+ press(...args) {
202
+ return scope.use(() => press(...args))
203
+ },
204
+ drag(...args) {
205
+ return startOwned(() => drag(...args))
206
+ },
207
+ states(targets, definitions, options) {
208
+ return startOwned(() => motionStates(targets, definitions, options))
209
+ },
210
+ add(resource) {
211
+ assertActive()
212
+ validate(resource)
213
+ if (adopted.has(resource)) return resource
214
+ const entry: Entry = { resume: false }
215
+ attach(entry, resource)
216
+ entries.add(entry)
217
+ return resource
218
+ },
219
+ use(setup) {
220
+ assertAlive()
221
+ if (typeof setup !== 'function')
222
+ throw new TypeError('Motion subscription needs a factory.')
223
+ const entry: Entry = { setup, resume: false }
224
+ entries.add(entry)
225
+ try {
226
+ if (!suspended) startSubscription(entry)
227
+ } catch (error) {
228
+ remove(entry)
229
+ throw error
230
+ }
231
+ return () => remove(entry)
232
+ },
233
+ suspend() {
234
+ if (disposed || suspended) return
235
+ suspended = true
236
+ visit((entry) => {
237
+ if (entry.setup) {
238
+ release(entry)
239
+ return
240
+ }
241
+ const resource = entry.resource as Controlled | undefined
242
+ if (
243
+ typeof resource?.pause === 'function' &&
244
+ typeof resource.play === 'function'
245
+ ) {
246
+ entry.resume = resource.paused !== true
247
+ if (entry.resume) resource.pause()
248
+ } else remove(entry)
249
+ })
250
+ },
251
+ resume() {
252
+ if (disposed || !suspended) return
253
+ suspended = false
254
+ visit((entry) => {
255
+ if (disposed || suspended) return
256
+ if (entry.setup) startSubscription(entry)
257
+ else if (entry.resume) {
258
+ entry.resume = false
259
+ ;(entry.resource as Controlled).play?.()
260
+ }
261
+ })
262
+ },
263
+ reset() {
264
+ if (disposed || clearing) return
265
+ clearing = true
266
+ try {
267
+ visit(remove)
268
+ } finally {
269
+ clearing = false
270
+ }
271
+ },
272
+ dispose() {
273
+ if (disposed) return
274
+ disposed = true
275
+ visit(remove)
276
+ },
277
+ }
278
+ return scope
279
+ }
@@ -0,0 +1,215 @@
1
+ import { prepareAnimation, toView } from './animate.js'
2
+ import { isColorProperty, type ColorValue } from './color.js'
3
+ import { isProperty } from './properties.js'
4
+ import type { Animation } from './engine.js'
5
+ import type { PlaybackAnimation } from './playback.js'
6
+ import type { KeyframeProperty, Keyframes, Target } from './types.js'
7
+ import type { Spring, Tween, SpringName } from './transition.js'
8
+ import type { MotionScope } from './motion-scope.js'
9
+
10
+ /** Absolute endpoints only; keyframe arrays, paths and relative offsets are not states. */
11
+ export type MotionValues = {
12
+ [K in KeyframeProperty]?: K extends 'color' | 'backgroundColor' | 'borderColor'
13
+ ? ColorValue
14
+ : number
15
+ }
16
+ export type MotionState<K extends PropertyKey = string> = Partial<Record<K, MotionValues>>
17
+ /** Preserve inference while rejecting unknown target/property keys. */
18
+ export type MotionDefinitions<T, S> = S & {
19
+ [N in keyof S]: Record<Exclude<keyof S[N], keyof T>, never> & {
20
+ [K in keyof S[N]]: Record<Exclude<keyof S[N][K], KeyframeProperty>, never>
21
+ }
22
+ }
23
+ export type StateTransition =
24
+ | SpringName
25
+ | Omit<Spring, 'repeat' | 'repeatType'>
26
+ | Omit<Tween, 'repeat' | 'repeatType'>
27
+ export interface MotionStatesOptions {
28
+ transition?: StateTransition
29
+ scope?: MotionScope
30
+ }
31
+ export interface MotionStates<S extends string = string> {
32
+ /** Latest requested state, not a claim that playback has finished. */
33
+ readonly current: S | undefined
34
+ readonly settled: S | undefined
35
+ readonly disposed: boolean
36
+ readonly paused: boolean
37
+ to(state: S, transition?: StateTransition): Animation
38
+ pause(): void
39
+ play(): void
40
+ stop(): void
41
+ dispose(): void
42
+ }
43
+
44
+ /** Coordinated named targets. Omitted properties freeze when leaving their previous state. */
45
+ export function motionStates<
46
+ T extends Record<string, Target>,
47
+ const S extends Record<string, MotionState<keyof T>>,
48
+ >(
49
+ targets: T,
50
+ definitions: MotionDefinitions<T, S>,
51
+ options: MotionStatesOptions = {},
52
+ ): MotionStates<Extract<keyof S, string>> {
53
+ type Name = Extract<keyof S, string>
54
+ const views = new Map(
55
+ Object.entries(targets).map(([name, target]) => [name, toView(target)]),
56
+ )
57
+ if (new Set(views.values()).size !== views.size)
58
+ throw new TypeError('Motion state targets must refer to distinct views.')
59
+ const states = new Map<string, [Target, Keyframes][]>()
60
+ const defaults = normalize(options.transition)
61
+ for (const [name, state] of Object.entries(definitions)) {
62
+ if (!state || typeof state !== 'object' || Array.isArray(state))
63
+ throw new TypeError(`Invalid motion state "${name}".`)
64
+ const rows: [Target, Keyframes][] = []
65
+ for (const [target, values] of Object.entries(state)) {
66
+ const view = views.get(target)
67
+ if (!view) throw new TypeError(`Unknown motion target "${target}".`)
68
+ if (!values || typeof values !== 'object' || Array.isArray(values))
69
+ throw new TypeError(`Invalid motion values for "${target}".`)
70
+ const frames: Keyframes = {}
71
+ for (const [property, value] of Object.entries(values)) {
72
+ if (value === undefined) continue
73
+ if (property !== 'scale' && !isProperty(property))
74
+ throw new TypeError(`Invalid motion state property "${property}".`)
75
+ if (
76
+ !isColorProperty(property) &&
77
+ (typeof value !== 'number' || !Number.isFinite(value))
78
+ )
79
+ throw new TypeError(`Motion state "${property}" requires a finite endpoint.`)
80
+ if (Array.isArray(value) || value === null)
81
+ throw new TypeError(`Motion state "${property}" requires a single endpoint.`)
82
+ Object.assign(frames, { [property]: value })
83
+ }
84
+ // Validate every definition up front without claiming any native property.
85
+ prepareAnimation(view, frames, defaults)
86
+ rows.push([view, frames])
87
+ }
88
+ states.set(name, rows)
89
+ }
90
+ let current: Name | undefined
91
+ let settled: Name | undefined
92
+ let disposed = false
93
+ let paused = false
94
+ let active: { handle: Animation; children: PlaybackAnimation[] } | undefined
95
+ const controller: MotionStates<Name> = {
96
+ get current() {
97
+ return current
98
+ },
99
+ get settled() {
100
+ return settled
101
+ },
102
+ get disposed() {
103
+ return disposed
104
+ },
105
+ get paused() {
106
+ return paused
107
+ },
108
+ to(name, transition) {
109
+ if (disposed) throw new Error('Motion states controller is disposed.')
110
+ const rows = states.get(name)
111
+ if (!rows) throw new TypeError(`Unknown motion state "${name}".`)
112
+ const timing = transition === undefined ? defaults : normalize(transition)
113
+ const prepared = rows.map(([view, frames]) =>
114
+ prepareAnimation(view, frames, timing),
115
+ )
116
+ const previous = active
117
+ const children: PlaybackAnimation[] = []
118
+ // Start replacements first: the engine transfers visible position AND velocity.
119
+ try {
120
+ for (const start of prepared) {
121
+ const child = start()
122
+ children.push(child)
123
+ if (paused) child.pause()
124
+ }
125
+ } catch (error) {
126
+ children.forEach((child) => child.stop())
127
+ previous?.handle.stop()
128
+ active = undefined
129
+ throw error
130
+ }
131
+ let cancelled = false
132
+ const finished = Promise.all(children.map((child) => child.finished)).then(
133
+ (results) => {
134
+ const completed =
135
+ !cancelled && !disposed && active?.handle === handle && results.every(Boolean)
136
+ if (active?.handle === handle) {
137
+ active = undefined
138
+ if (completed) settled = name
139
+ }
140
+ return completed
141
+ },
142
+ )
143
+ const handle: Animation = {
144
+ finished,
145
+ stop() {
146
+ cancelled = true
147
+ children.forEach((child) => child.stop())
148
+ },
149
+ then: (resolve, reject) => finished.then(resolve, reject),
150
+ }
151
+ active = { handle, children }
152
+ current = name
153
+ // Also cancels old, omitted properties, without touching the new owners.
154
+ previous?.handle.stop()
155
+ return handle
156
+ },
157
+ pause() {
158
+ if (disposed || paused) return
159
+ paused = true
160
+ active?.children.forEach((child) => child.pause())
161
+ },
162
+ play() {
163
+ if (disposed || !paused) return
164
+ paused = false
165
+ active?.children.forEach((child) => child.play())
166
+ },
167
+ stop() {
168
+ active?.handle.stop()
169
+ active = undefined
170
+ },
171
+ dispose() {
172
+ if (disposed) return
173
+ disposed = true
174
+ controller.stop()
175
+ },
176
+ }
177
+ return options.scope ? options.scope.add(controller) : controller
178
+ }
179
+
180
+ function normalize(transition: StateTransition | undefined): StateTransition {
181
+ if (transition === undefined) return 'snappy'
182
+ if (typeof transition === 'string') {
183
+ if (!['smooth', 'snappy', 'bouncy'].includes(transition))
184
+ throw new TypeError('Unknown state spring.')
185
+ return transition
186
+ }
187
+ if (!transition || typeof transition !== 'object')
188
+ throw new TypeError('Invalid state transition.')
189
+ const allowed = new Set([
190
+ 'type',
191
+ 'delay',
192
+ 'duration',
193
+ 'ease',
194
+ 'times',
195
+ 'stiffness',
196
+ 'damping',
197
+ 'mass',
198
+ 'bounce',
199
+ 'velocity',
200
+ ])
201
+ for (const key of Object.keys(transition))
202
+ if (!allowed.has(key))
203
+ throw new TypeError(`State transitions do not support "${key}".`)
204
+ if (
205
+ transition.duration !== undefined &&
206
+ (!Number.isFinite(transition.duration) || transition.duration < 0)
207
+ )
208
+ throw new RangeError('State transition duration must be finite and nonnegative.')
209
+ if (
210
+ transition.delay !== undefined &&
211
+ (!Number.isFinite(transition.delay) || transition.delay < 0)
212
+ )
213
+ throw new RangeError('State transition delay must be finite and nonnegative.')
214
+ return { ...transition }
215
+ }