@rafaelaugusto/jolt 0.1.0-beta.1 → 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 (302) hide show
  1. package/README.md +367 -18
  2. package/dist/animate.d.ts +19 -3
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +186 -14
  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 +163 -28
  75. package/dist/driver/ios.js.map +1 -1
  76. package/dist/engine.d.ts +75 -5
  77. package/dist/engine.d.ts.map +1 -1
  78. package/dist/engine.js +293 -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 +43 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +20 -1
  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/press.d.ts.map +1 -1
  157. package/dist/press.js +23 -7
  158. package/dist/press.js.map +1 -1
  159. package/dist/properties.d.ts +6 -3
  160. package/dist/properties.d.ts.map +1 -1
  161. package/dist/properties.js +26 -2
  162. package/dist/properties.js.map +1 -1
  163. package/dist/reveal.d.ts +25 -0
  164. package/dist/reveal.d.ts.map +1 -0
  165. package/dist/reveal.js +294 -0
  166. package/dist/reveal.js.map +1 -0
  167. package/dist/roll-diff.d.ts +22 -0
  168. package/dist/roll-diff.d.ts.map +1 -0
  169. package/dist/roll-diff.js +36 -0
  170. package/dist/roll-diff.js.map +1 -0
  171. package/dist/roll.d.ts +29 -0
  172. package/dist/roll.d.ts.map +1 -0
  173. package/dist/roll.js +442 -0
  174. package/dist/roll.js.map +1 -0
  175. package/dist/scroll-linked.d.ts +10 -0
  176. package/dist/scroll-linked.d.ts.map +1 -0
  177. package/dist/scroll-linked.js +58 -0
  178. package/dist/scroll-linked.js.map +1 -0
  179. package/dist/sequence.d.ts +10 -5
  180. package/dist/sequence.d.ts.map +1 -1
  181. package/dist/sequence.js +208 -14
  182. package/dist/sequence.js.map +1 -1
  183. package/dist/shimmer.d.ts +12 -0
  184. package/dist/shimmer.d.ts.map +1 -0
  185. package/dist/shimmer.js +161 -0
  186. package/dist/shimmer.js.map +1 -0
  187. package/dist/svg-path.d.ts +14 -0
  188. package/dist/svg-path.d.ts.map +1 -0
  189. package/dist/svg-path.js +281 -0
  190. package/dist/svg-path.js.map +1 -0
  191. package/dist/symbol-effect.d.ts +23 -0
  192. package/dist/symbol-effect.d.ts.map +1 -0
  193. package/dist/symbol-effect.js +187 -0
  194. package/dist/symbol-effect.js.map +1 -0
  195. package/dist/text-layout.d.ts +30 -0
  196. package/dist/text-layout.d.ts.map +1 -0
  197. package/dist/text-layout.js +185 -0
  198. package/dist/text-layout.js.map +1 -0
  199. package/dist/text.d.ts +17 -0
  200. package/dist/text.d.ts.map +1 -0
  201. package/dist/text.js +263 -0
  202. package/dist/text.js.map +1 -0
  203. package/dist/track.d.ts +3 -1
  204. package/dist/track.d.ts.map +1 -1
  205. package/dist/track.js +266 -13
  206. package/dist/track.js.map +1 -1
  207. package/dist/transition.d.ts +17 -1
  208. package/dist/transition.d.ts.map +1 -1
  209. package/dist/transition.js +41 -6
  210. package/dist/transition.js.map +1 -1
  211. package/dist/types.d.ts +48 -1
  212. package/dist/types.d.ts.map +1 -1
  213. package/dist/vue/index.d.ts +95 -0
  214. package/dist/vue/index.d.ts.map +1 -0
  215. package/dist/vue/index.js +163 -0
  216. package/dist/vue/index.js.map +1 -0
  217. package/dist/zoom-native.d.ts +30 -0
  218. package/dist/zoom-native.d.ts.map +1 -0
  219. package/dist/zoom-native.js +56 -0
  220. package/dist/zoom-native.js.map +1 -0
  221. package/dist/zoomable.d.ts +48 -0
  222. package/dist/zoomable.d.ts.map +1 -0
  223. package/dist/zoomable.js +249 -0
  224. package/dist/zoomable.js.map +1 -0
  225. package/package.json +29 -5
  226. package/platforms/android/include.gradle +7 -0
  227. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  228. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  229. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  230. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  231. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
  232. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  233. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  234. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  235. package/platforms/android/proguard-rules.pro +19 -0
  236. package/platforms/ios/build.xcconfig +1 -0
  237. package/platforms/ios/src/JoltImageEffect.h +25 -0
  238. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  239. package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
  240. package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
  241. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  242. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  243. package/platforms/ios/src/module.modulemap +6 -0
  244. package/src/android-plugin.d.ts +155 -0
  245. package/src/animate.ts +249 -36
  246. package/src/color.ts +101 -0
  247. package/src/config.ts +84 -0
  248. package/src/decay.ts +45 -0
  249. package/src/decoration.ts +180 -0
  250. package/src/drag-input.ts +74 -0
  251. package/src/drag-physics.ts +110 -0
  252. package/src/drag.ts +395 -0
  253. package/src/driver/android-color.ts +170 -0
  254. package/src/driver/android-decoration.ts +182 -0
  255. package/src/driver/android-playback.ts +157 -0
  256. package/src/driver/android.ts +208 -20
  257. package/src/driver/index.ts +37 -2
  258. package/src/driver/ios-color.ts +441 -0
  259. package/src/driver/ios-decoration.ts +332 -0
  260. package/src/driver/ios-layout.ts +63 -0
  261. package/src/driver/ios-playback.ts +51 -0
  262. package/src/driver/ios-shadow.ts +50 -0
  263. package/src/driver/ios.ts +193 -33
  264. package/src/engine.ts +321 -27
  265. package/src/haptics.ts +148 -0
  266. package/src/image-effect-native.ts +129 -0
  267. package/src/image-effect.ts +336 -0
  268. package/src/image-owner.ts +12 -0
  269. package/src/index.ts +59 -2
  270. package/src/interaction-surface.ts +41 -0
  271. package/src/ios-plugin.d.ts +81 -0
  272. package/src/items.ts +140 -0
  273. package/src/keyboard.ts +291 -0
  274. package/src/label.ts +76 -0
  275. package/src/layout.ts +408 -0
  276. package/src/liquid-grid.ts +28 -0
  277. package/src/liquid.ts +287 -0
  278. package/src/loader.ts +202 -0
  279. package/src/motion-scope.ts +279 -0
  280. package/src/motion-states.ts +215 -0
  281. package/src/particles.ts +323 -0
  282. package/src/path.ts +124 -0
  283. package/src/playback-group.ts +143 -0
  284. package/src/playback.ts +62 -0
  285. package/src/press.ts +26 -10
  286. package/src/properties.ts +32 -6
  287. package/src/reveal.ts +374 -0
  288. package/src/roll-diff.ts +57 -0
  289. package/src/roll.ts +515 -0
  290. package/src/scroll-linked.ts +73 -0
  291. package/src/sequence.ts +204 -14
  292. package/src/shimmer.ts +180 -0
  293. package/src/svg-path.ts +355 -0
  294. package/src/symbol-effect.ts +226 -0
  295. package/src/text-layout.ts +228 -0
  296. package/src/text.ts +279 -0
  297. package/src/track.ts +347 -12
  298. package/src/transition.ts +63 -6
  299. package/src/types.ts +49 -1
  300. package/src/vue/index.ts +231 -0
  301. package/src/zoom-native.ts +124 -0
  302. package/src/zoomable.ts +330 -0
@@ -0,0 +1,155 @@
1
+ declare namespace org.rafaelaugusto.jolt {
2
+ class PhotoZoom {
3
+ constructor(
4
+ image: android.widget.ImageView,
5
+ viewport: android.view.View,
6
+ bitmapWidth: number,
7
+ bitmapHeight: number,
8
+ minimum: number,
9
+ maximum: number,
10
+ doubleTap: number,
11
+ dismiss: boolean,
12
+ reduced: boolean,
13
+ backdrop: android.view.View | null,
14
+ chrome: android.view.View | null,
15
+ previousTouch: android.view.View.OnTouchListener | null,
16
+ previousImageTouch: android.view.View.OnTouchListener | null,
17
+ listener: PhotoZoom.Listener,
18
+ )
19
+ getX(): number
20
+ getY(): number
21
+ getScale(): number
22
+ getDismissProgress(): number
23
+ isDismissPending(): boolean
24
+ zoom(scale: number, x: number, y: number, animated: boolean): void
25
+ reset(animated: boolean): void
26
+ requestDismiss(): void
27
+ setReduced(value: boolean): void
28
+ setActive(value: boolean): void
29
+ stop(): void
30
+ }
31
+ namespace PhotoZoom {
32
+ class Listener {
33
+ constructor(implementation: {
34
+ onDismiss(
35
+ x: number,
36
+ y: number,
37
+ scale: number,
38
+ velocity: number,
39
+ progress: number,
40
+ ): void
41
+ onSettled(x: number, y: number, scale: number): void
42
+ })
43
+ }
44
+ }
45
+ class ImageEffect {
46
+ constructor(
47
+ image: android.widget.ImageView,
48
+ source: android.graphics.Bitmap | null,
49
+ destination: android.graphics.Bitmap | null,
50
+ geometry: androidNative.Array<number>,
51
+ mode: number,
52
+ x: number,
53
+ y: number,
54
+ amplitude: number,
55
+ radius: number,
56
+ duration: number,
57
+ seed: number,
58
+ previousTouch: android.view.View.OnTouchListener | null,
59
+ listener: ImageEffect.Listener,
60
+ )
61
+ isSupported(): boolean
62
+ getFailureReason(): string
63
+ getProgress(): number
64
+ getFrames(): number
65
+ getTextureBytes(): number
66
+ getLongestFrameNanos(): number
67
+ play(endpoint: number): void
68
+ pause(): void
69
+ resume(): void
70
+ seek(progress: number): void
71
+ finish(endpoint: number): void
72
+ stop(): void
73
+ stopPreservingImage(): void
74
+ }
75
+ namespace ImageEffect {
76
+ class Listener {
77
+ constructor(implementation: { onEnd(completed: boolean, progress: number): void })
78
+ }
79
+ }
80
+ class KeyboardFollower {
81
+ constructor(
82
+ view: android.view.View,
83
+ offset: number,
84
+ listener: KeyboardFollower.Listener,
85
+ )
86
+ attach(): void
87
+ detach(): void
88
+ }
89
+ namespace KeyboardFollower {
90
+ class Listener {
91
+ constructor(implementation: {
92
+ onStart(): void
93
+ onSettled(translation: number): void
94
+ })
95
+ }
96
+ }
97
+ class KeyboardObserver {
98
+ constructor(root: android.view.View, listener: KeyboardObserver.Listener)
99
+ attach(): void
100
+ detach(): void
101
+ height(): number
102
+ }
103
+ namespace KeyboardObserver {
104
+ class Listener {
105
+ constructor(implementation: {
106
+ onChange(height: number, duration: number, curve: ArrayLike<number> | null): void
107
+ })
108
+ }
109
+ }
110
+ class TextReveal {
111
+ constructor(
112
+ view: android.widget.TextView,
113
+ byWord: boolean,
114
+ duration: number,
115
+ stagger: number,
116
+ delay: number,
117
+ offset: number,
118
+ callback: TextReveal.Callback,
119
+ )
120
+ isSupported(): boolean
121
+ start(): void
122
+ cancel(): void
123
+ }
124
+ namespace TextReveal {
125
+ class Callback {
126
+ constructor(implementation: { onEnd(finished: boolean): void })
127
+ }
128
+ }
129
+ class NumberRoll {
130
+ constructor(
131
+ view: android.widget.TextView,
132
+ starts: androidNative.Array<number>,
133
+ ends: androidNative.Array<number>,
134
+ previous: androidNative.Array<string>,
135
+ direction: number,
136
+ times: androidNative.Array<number>,
137
+ values: androidNative.Array<number>,
138
+ duration: number,
139
+ delay: number,
140
+ stagger: number,
141
+ shift: number,
142
+ fade: number,
143
+ tabular: boolean,
144
+ callback: NumberRoll.Callback,
145
+ )
146
+ isSupported(): boolean
147
+ start(): void
148
+ cancel(): void
149
+ }
150
+ namespace NumberRoll {
151
+ class Callback {
152
+ constructor(implementation: { onEnd(finished: boolean): void })
153
+ }
154
+ }
155
+ }
package/src/animate.ts CHANGED
@@ -1,4 +1,19 @@
1
+ import {
2
+ isDecorationProperty,
3
+ validateDecorationTarget,
4
+ validateDecorationFrames,
5
+ } from './decoration.js'
6
+ import { playbackGroup } from './playback-group.js'
7
+ import type { PlaybackAnimation } from './playback.js'
8
+ import { preparePath, type PreparedPath } from './path.js'
1
9
  import { View } from '@nativescript/core'
10
+ import {
11
+ isColorProperty,
12
+ parseColor,
13
+ validateColorTarget,
14
+ type ColorKeyframes,
15
+ } from './color.js'
16
+ import { DEFAULT_FRICTION, solveDecay } from './decay.js'
2
17
  import {
3
18
  arrivalOf,
4
19
  setArrival,
@@ -8,7 +23,8 @@ import {
8
23
  type Plan,
9
24
  } from './engine.js'
10
25
  import { getPreset, type PresetName } from './presets.js'
11
- import { isProperty, type Property } from './properties.js'
26
+ import { resolveHaptic, type Haptic } from './haptics.js'
27
+ import { isProperty, precision, type Property } from './properties.js'
12
28
  import {
13
29
  hasCurve,
14
30
  mergeTransitions,
@@ -19,17 +35,22 @@ import {
19
35
  } from './transition.js'
20
36
  import type {
21
37
  AnimateOptions,
38
+ DecayOptions,
39
+ DecayVelocity,
22
40
  Keyframes,
23
41
  KeyframeValue,
42
+ KeyframeProperty,
24
43
  Stagger,
25
44
  Target,
45
+ Values,
26
46
  } from './types.js'
27
47
 
28
48
  interface Options {
29
49
  base: Transition
30
- overrides: Partial<Record<keyof Keyframes, Transition>>
50
+ overrides: Partial<Record<KeyframeProperty | 'path', Transition>>
31
51
  stagger: number | Stagger | undefined
32
52
  relative: boolean | undefined
53
+ haptic: Haptic | undefined
33
54
  }
34
55
 
35
56
  /**
@@ -46,38 +67,129 @@ export function animate(
46
67
  target: Target | readonly Target[],
47
68
  animation: Keyframes | PresetName,
48
69
  options?: AnimateOptions | SpringName,
49
- ): Animation {
50
- const { keyframes, base, overrides, stagger, relative } = prepare(animation, options)
70
+ ): PlaybackAnimation {
71
+ return prepareAnimation(target, animation, options)()
72
+ }
73
+
74
+ /** Validate and snapshot a complete animation before changing any view ownership. */
75
+ export function prepareAnimation(
76
+ target: Target | readonly Target[],
77
+ animation: Keyframes | PresetName,
78
+ options?: AnimateOptions | SpringName,
79
+ ): () => PlaybackAnimation {
80
+ const { keyframes, base, overrides, stagger, relative, haptic, path } = prepare(
81
+ animation,
82
+ options,
83
+ )
84
+ const feedback = resolveHaptic(haptic)
51
85
 
52
86
  if (!isList(target)) {
53
- return new ViewAnimation(
54
- toView(target),
55
- plan(keyframes, base, overrides, 0, relative),
56
- )
87
+ const view = toView(target)
88
+ for (const property of Object.keys(keyframes))
89
+ if (isColorProperty(property)) validateColorTarget(view, property)
90
+ else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
91
+ const plans = plan(keyframes, base, overrides, 0, relative, path)
92
+ return () => new ViewAnimation(view, plans, feedback)
57
93
  }
58
94
 
59
95
  const views = target.map(toView)
60
- return group(
61
- views.map(
62
- (view, index) =>
63
- new ViewAnimation(
64
- view,
65
- plan(
66
- keyframes,
67
- base,
68
- overrides,
69
- staggerDelay(stagger, index, views.length),
70
- relative,
71
- ),
72
- ),
96
+ for (const view of views)
97
+ for (const property of Object.keys(keyframes))
98
+ if (isColorProperty(property)) validateColorTarget(view, property)
99
+ else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
100
+ const plans = views.map((_, index) =>
101
+ plan(
102
+ keyframes,
103
+ base,
104
+ overrides,
105
+ staggerDelay(stagger, index, views.length),
106
+ relative,
107
+ path,
73
108
  ),
74
109
  )
110
+ return () =>
111
+ playbackGroup(
112
+ views.map((view, index) => new ViewAnimation(view, plans[index]!, feedback, true)),
113
+ )
114
+ }
115
+
116
+ /** Starts at the current translation and slows down from a velocity in DIPs per second. */
117
+ export function decay(
118
+ target: Target | readonly Target[],
119
+ velocity: DecayVelocity,
120
+ options: DecayOptions = {},
121
+ ): PlaybackAnimation {
122
+ const friction = options.friction ?? DEFAULT_FRICTION
123
+ if (!Number.isFinite(friction) || friction <= 0 || friction >= 1) {
124
+ throw new RangeError('Decay friction must be between 0 and 1 exclusively.')
125
+ }
126
+
127
+ const stops = (values: readonly number[] | undefined) => {
128
+ if (values === undefined) return undefined
129
+ if (
130
+ !Array.isArray(values) ||
131
+ values.length === 0 ||
132
+ ![...values].every(Number.isFinite)
133
+ ) {
134
+ throw new RangeError('Decay snap needs a nonempty array of finite positions.')
135
+ }
136
+ return [...values]
137
+ }
138
+ const snap = options.snap
139
+ if (
140
+ snap !== undefined &&
141
+ !Array.isArray(snap) &&
142
+ (snap === null ||
143
+ typeof snap !== 'object' ||
144
+ Object.keys(snap).some((key) => key !== 'x' && key !== 'y'))
145
+ ) {
146
+ throw new TypeError('Decay snap must be an array or an object with x and y stops.')
147
+ }
148
+ const shared = Array.isArray(snap)
149
+ const perAxis = snap as { x?: readonly number[]; y?: readonly number[] } | undefined
150
+ const snapped = shared ? stops(snap) : undefined
151
+ const snapX = shared ? snapped : stops(perAxis?.x)
152
+ const snapY = shared ? snapped : stops(perAxis?.y)
153
+
154
+ const plans = Object.entries(velocity).flatMap<Plan>(([property, value]) => {
155
+ if (property !== 'x' && property !== 'y') {
156
+ throw new Error(`Can't decay "${property}". Use x or y.`)
157
+ }
158
+ if (value === undefined) return []
159
+ if (!Number.isFinite(value)) {
160
+ throw new RangeError(`Invalid decay velocity for "${property}".`)
161
+ }
162
+ // Validate every axis before handing any plans to the engine.
163
+ solveDecay(0, value, friction, precision[property])
164
+ return [
165
+ {
166
+ property,
167
+ keyframes: [null, null],
168
+ transition: {
169
+ curve: {
170
+ type: 'decay',
171
+ friction,
172
+ velocity: value,
173
+ snap: property === 'x' ? snapX : snapY,
174
+ },
175
+ delay: 0,
176
+ repeat: 0,
177
+ reverse: false,
178
+ },
179
+ },
180
+ ]
181
+ })
182
+ const views = toViews(target)
183
+ if (plans.length === 0) return playbackGroup([])
184
+ return playbackGroup(
185
+ views.map((view) => new ViewAnimation(view, plans, undefined, true)),
186
+ )
75
187
  }
76
188
 
77
189
  /** Stops all or selected properties at their current values. `scale` stops both axes. */
78
190
  export function stop(
79
191
  target: Target | readonly Target[],
80
- properties?: readonly (keyof Keyframes)[],
192
+ properties?: readonly KeyframeProperty[],
81
193
  ) {
82
194
  const expanded = properties?.flatMap<Property>((property) =>
83
195
  property === 'scale' ? ['scaleX', 'scaleY'] : [property],
@@ -85,6 +197,30 @@ export function stop(
85
197
  for (const view of toViews(target)) stopView(view, expanded)
86
198
  }
87
199
 
200
+ /**
201
+ * Applies values at once, stopping whatever animates those properties.
202
+ * `scale` sets both axes. A view that isn't loaded yet gets them right away.
203
+ *
204
+ * ```ts
205
+ * set(logo, { opacity: 1, scale: 1, y: 0 })
206
+ * ```
207
+ */
208
+ export function set(target: Target | readonly Target[], values: Values): void {
209
+ if (values === null || typeof values !== 'object' || Array.isArray(values))
210
+ throw new TypeError('set() takes an object of values.')
211
+ const keyframes: Record<string, unknown> = {}
212
+ for (const [key, value] of Object.entries(values)) {
213
+ if (value === undefined) continue
214
+ if (key !== 'scale' && !isProperty(key)) throw new Error(`Can't set "${key}".`)
215
+ if (value === null || Array.isArray(value))
216
+ throw new TypeError(
217
+ `set() takes one value per property, got ${String(value)} for "${key}".`,
218
+ )
219
+ keyframes[key] = [value, value]
220
+ }
221
+ animate(target, keyframes as Keyframes, { duration: 0 })
222
+ }
223
+
88
224
  /** Plays several animations as one: finished once they all are, and stopped together. */
89
225
  export function group(animations: readonly Animation[]): Animation {
90
226
  const finished = Promise.all(animations.map((animation) => animation.finished)).then(
@@ -119,6 +255,17 @@ function isList<T>(target: T | readonly T[]): target is readonly T[] {
119
255
  function prepare(
120
256
  animation: Keyframes | PresetName,
121
257
  options?: AnimateOptions | SpringName,
258
+ ) {
259
+ const prepared = resolveAnimation(animation, options)
260
+ return {
261
+ ...prepared,
262
+ path: preparePath(prepared.keyframes, prepared.base, prepared.overrides),
263
+ }
264
+ }
265
+
266
+ function resolveAnimation(
267
+ animation: Keyframes | PresetName,
268
+ options?: AnimateOptions | SpringName,
122
269
  ) {
123
270
  const user = normalize(options)
124
271
  if (typeof animation !== 'string') return { keyframes: animation, ...user }
@@ -133,33 +280,43 @@ function prepare(
133
280
  : { ...own.overrides, ...user.overrides },
134
281
  stagger: user.stagger ?? own.stagger,
135
282
  relative: user.relative ?? own.relative,
283
+ haptic: user.haptic ?? own.haptic,
136
284
  }
137
285
  }
138
286
 
139
287
  function normalize(options: AnimateOptions | SpringName | undefined): Options {
140
288
  if (options === undefined)
141
- return { base: {}, overrides: {}, stagger: undefined, relative: undefined }
289
+ return {
290
+ base: {},
291
+ overrides: {},
292
+ stagger: undefined,
293
+ relative: undefined,
294
+ haptic: undefined,
295
+ }
142
296
  if (typeof options === 'string')
143
297
  return {
144
298
  base: spring(options),
145
299
  overrides: {},
146
300
  stagger: undefined,
147
301
  relative: undefined,
302
+ haptic: undefined,
148
303
  }
149
304
 
150
305
  const base: Record<string, unknown> = {}
151
306
  const overrides: Options['overrides'] = {}
152
307
  let stagger: Options['stagger']
153
308
  let relative: Options['relative']
309
+ let haptic: Options['haptic']
154
310
  for (const [key, value] of Object.entries(options)) {
155
311
  if (value === undefined) continue
156
312
  if (key === 'stagger') stagger = value as Options['stagger']
157
313
  else if (key === 'relative') relative = value as Options['relative']
158
- else if (key === 'scale' || isProperty(key)) {
314
+ else if (key === 'haptic') haptic = value as Options['haptic']
315
+ else if (key === 'path' || key === 'scale' || isProperty(key)) {
159
316
  overrides[key] = typeof value === 'string' ? spring(value as SpringName) : value
160
317
  } else base[key] = value
161
318
  }
162
- return { base, overrides, stagger, relative }
319
+ return { base, overrides, stagger, relative, haptic }
163
320
  }
164
321
 
165
322
  function plan(
@@ -168,8 +325,25 @@ function plan(
168
325
  overrides: Options['overrides'],
169
326
  offset: number,
170
327
  relative = false,
328
+ path?: PreparedPath,
171
329
  ): Plan[] {
172
- return expand(keyframes).map(([property, frames]) => {
330
+ return expand(keyframes, path).map(([property, frames]) => {
331
+ if (path?.properties.includes(property as 'x' | 'y' | 'rotate')) {
332
+ if (
333
+ !Number.isFinite(path.transition.delay + offset) ||
334
+ path.transition.delay + offset < 0
335
+ )
336
+ throw new RangeError(
337
+ 'Path delays, including stagger, must be finite and nonnegative.',
338
+ )
339
+ return {
340
+ property,
341
+ keyframes: frames,
342
+ transition: { ...path.transition, delay: path.transition.delay + offset },
343
+ relative,
344
+ path,
345
+ }
346
+ }
173
347
  const override =
174
348
  overrides[property] ?? (property.startsWith('scale') ? overrides.scale : undefined)
175
349
  const transition = resolveTransition(
@@ -187,32 +361,70 @@ export function startingValues(
187
361
  animation: Keyframes | PresetName,
188
362
  options?: AnimateOptions | SpringName,
189
363
  ) {
190
- const { keyframes, relative } = prepare(animation, options)
364
+ const { keyframes, relative, path } = prepare(animation, options)
191
365
  if (!keyframes) throw new Error(`Unknown preset "${animation}".`)
192
366
  return new Map(
193
- expand(keyframes).map(([property, [first]]) => [property, relative ? null : first!]),
367
+ expand(keyframes, path).map(([property, [first]]) => [
368
+ property,
369
+ relative && !isColorProperty(property) ? null : first!,
370
+ ]),
371
+ )
372
+ }
373
+
374
+ /** Each property the animation touches, with the value it leaves behind when that is known up front. */
375
+ export function endingValues(
376
+ animation: Keyframes | PresetName,
377
+ options?: AnimateOptions | SpringName,
378
+ ) {
379
+ const { keyframes, base, overrides, relative, path } = prepare(animation, options)
380
+ return new Map(
381
+ plan(keyframes, base, overrides, 0, relative, path).map(
382
+ ({ property, keyframes: frames, transition, path: owned }) => {
383
+ const finite = Number.isFinite(transition.repeat)
384
+ const backwards = transition.reverse && finite && transition.repeat % 2 === 1
385
+ const unknown = owned !== undefined || (relative && !isColorProperty(property))
386
+ const frame = !finite || backwards ? frames[0] : frames.at(-1)
387
+ return [property, unknown ? null : frame!]
388
+ },
389
+ ),
194
390
  )
195
391
  }
196
392
 
197
- function expand(keyframes: Keyframes): [Property, (number | null)[]][] {
393
+ function expand(
394
+ keyframes: Keyframes,
395
+ path?: PreparedPath,
396
+ ): [Property, (number | null)[]][] {
198
397
  const expanded: [Property, (number | null)[]][] = []
199
- for (const [key, value] of Object.entries(keyframes) as [
200
- keyof Keyframes,
201
- KeyframeValue | undefined,
202
- ][]) {
203
- if (value === undefined) continue
398
+ for (const [key, value] of Object.entries(keyframes)) {
399
+ if (value === undefined || key === 'path' || key === 'orient') continue
204
400
  if (key !== 'scale' && !isProperty(key)) throw new Error(`Can't animate "${key}".`)
205
401
 
206
402
  const properties: Property[] = key === 'scale' ? ['scaleX', 'scaleY'] : [key]
207
403
  for (const property of properties) {
208
404
  if (key === 'scale' && keyframes[property] !== undefined) continue
209
- expanded.push([property, toKeyframes(property, value)])
405
+ expanded.push([
406
+ property,
407
+ toKeyframes(property, value as KeyframeValue | ColorKeyframes),
408
+ ])
210
409
  }
211
410
  }
411
+ for (const [property, value] of path?.starts ?? [])
412
+ expanded.push([property, [value, value]])
212
413
  return expanded
213
414
  }
214
415
 
215
- function toKeyframes(property: Property, value: KeyframeValue): (number | null)[] {
416
+ function toKeyframes(
417
+ property: Property,
418
+ value: KeyframeValue | ColorKeyframes,
419
+ ): (number | null)[] {
420
+ if (isColorProperty(property)) {
421
+ const frames = Array.isArray(value) ? [...value] : [null, value]
422
+ if (!frames.length) throw new TypeError(`Empty color keyframes for "${property}".`)
423
+ if (frames.length === 1) frames.unshift(null)
424
+ return frames.map((frame) => (frame === null ? null : parseColor(frame)))
425
+ }
426
+ if (typeof value !== 'number' && !Array.isArray(value))
427
+ throw new TypeError(`Invalid numeric keyframes for "${property}".`)
216
428
  const frames = typeof value === 'number' ? [null, value] : [...value]
217
429
  if (frames.length === 1) frames.unshift(null)
218
430
  if (
@@ -221,6 +433,7 @@ function toKeyframes(property: Property, value: KeyframeValue): (number | null)[
221
433
  ) {
222
434
  throw new Error(`Invalid keyframes for "${property}": ${JSON.stringify(value)}.`)
223
435
  }
436
+ if (isDecorationProperty(property)) validateDecorationFrames(property, frames)
224
437
  return frames
225
438
  }
226
439
 
package/src/color.ts ADDED
@@ -0,0 +1,101 @@
1
+ import { Button, Color, Label, TextBase, isIOS, type View } from '@nativescript/core'
2
+
3
+ export type ColorProperty = 'backgroundColor' | 'borderColor' | 'color'
4
+ export type ColorValue = string | Color
5
+ export type ColorKeyframes = ColorValue | readonly (ColorValue | null)[]
6
+ export const isColorProperty = (property: string): property is ColorProperty =>
7
+ property === 'backgroundColor' || property === 'borderColor' || property === 'color'
8
+
9
+ export function parseColor(value: unknown): number {
10
+ if (value instanceof Color) return value.argb >>> 0
11
+ if (typeof value !== 'string' || !Color.isValid(value))
12
+ throw new TypeError(`Invalid animation color: ${String(value)}.`)
13
+ return new Color(value).argb >>> 0
14
+ }
15
+
16
+ export function readColor(view: View, property: ColorProperty): number {
17
+ const value = property === 'borderColor' ? view.borderTopColor : view[property]
18
+ return value instanceof Color
19
+ ? value.argb >>> 0
20
+ : property === 'backgroundColor'
21
+ ? 0
22
+ : 0xff000000
23
+ }
24
+
25
+ export function validateColorTarget(view: View, property: ColorProperty) {
26
+ if (
27
+ property === 'color' &&
28
+ !(isIOS ? view instanceof Label || view instanceof Button : view instanceof TextBase)
29
+ )
30
+ throw new TypeError(
31
+ 'Text color animation requires a Label or Button on iOS, or a TextBase on Android.',
32
+ )
33
+ if (property === 'borderColor') {
34
+ const colors = [
35
+ view.borderTopColor,
36
+ view.borderRightColor,
37
+ view.borderBottomColor,
38
+ view.borderLeftColor,
39
+ ]
40
+ if (
41
+ colors.some(
42
+ (color) => (color?.argb ?? 0xff000000) !== (colors[0]?.argb ?? 0xff000000),
43
+ )
44
+ )
45
+ throw new TypeError(
46
+ 'borderColor animation requires the same starting color on every edge.',
47
+ )
48
+ }
49
+ }
50
+
51
+ const clamp = (value: number) => Math.max(0, Math.min(1, value))
52
+ const linear = (channel: number) =>
53
+ channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4
54
+ const encoded = (channel: number) =>
55
+ channel <= 0.0031308 ? 12.92 * channel : 1.055 * channel ** (1 / 2.4) - 0.055
56
+
57
+ function lab(value: number) {
58
+ const r = linear(((value >>> 16) & 255) / 255)
59
+ const g = linear(((value >>> 8) & 255) / 255)
60
+ const b = linear((value & 255) / 255)
61
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b)
62
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b)
63
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b)
64
+ return [
65
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
66
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
67
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
68
+ ] as const
69
+ }
70
+
71
+ /** Premultiplied OKLab interpolation, clipped to the sRGB output gamut. */
72
+ export function colorMixer(from: number, to: number): (progress: number) => number {
73
+ const a = lab(from),
74
+ b = lab(to)
75
+ const alphaA = (from >>> 24) / 255,
76
+ alphaB = (to >>> 24) / 255
77
+ return (progress) => {
78
+ const p = clamp(progress)
79
+ if (p === 0) return from
80
+ if (p === 1) return to
81
+ const alpha = alphaA + (alphaB - alphaA) * p
82
+ if (alpha <= 0) return 0
83
+ const channel = (i: number) => (a[i]! * alphaA * (1 - p) + b[i]! * alphaB * p) / alpha
84
+ const L = channel(0),
85
+ A = channel(1),
86
+ B = channel(2)
87
+ const l = (L + 0.3963377774 * A + 0.2158037573 * B) ** 3
88
+ const m = (L - 0.1055613458 * A - 0.0638541728 * B) ** 3
89
+ const s = (L - 0.0894841775 * A - 1.291485548 * B) ** 3
90
+ const r = Math.round(
91
+ clamp(encoded(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s)) * 255,
92
+ )
93
+ const g = Math.round(
94
+ clamp(encoded(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s)) * 255,
95
+ )
96
+ const blue = Math.round(
97
+ clamp(encoded(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)) * 255,
98
+ )
99
+ return ((Math.round(alpha * 255) << 24) | (r << 16) | (g << 8) | blue) >>> 0
100
+ }
101
+ }