@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
package/src/config.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { getDriver } from './driver/index.js'
2
+
3
+ export interface JoltConfig {
4
+ /** Playback speed for every engine animation. `0.2` plays everything five times slower. */
5
+ timeScale?: number
6
+ /**
7
+ * When transforms, paths, decorations and decay jump to their end instead of animating.
8
+ * `user` follows the platform setting. Opacity and colors still animate.
9
+ */
10
+ reducedMotion?: 'user' | 'always' | 'never'
11
+ }
12
+
13
+ const settings: Required<JoltConfig> = { timeScale: 1, reducedMotion: 'user' }
14
+ const listeners = new Set<(reduced: boolean) => void>()
15
+ let reported = false
16
+ let unwatch: (() => void) | undefined
17
+ const preferences: readonly JoltConfig['reducedMotion'][] = ['user', 'always', 'never']
18
+
19
+ /**
20
+ * Global settings, applied to animations that start after the call.
21
+ *
22
+ * ```ts
23
+ * configure({ timeScale: 0.2 }) // tune a spring in slow motion
24
+ * configure({ reducedMotion: 'always' }) // check the reduced layout
25
+ * ```
26
+ */
27
+ export function configure(options: JoltConfig): void {
28
+ if (options === null || typeof options !== 'object')
29
+ throw new TypeError('configure() takes an options object.')
30
+ const { timeScale, reducedMotion } = options
31
+ if (timeScale !== undefined && (!Number.isFinite(timeScale) || timeScale <= 0))
32
+ throw new RangeError(`Time scale must be a positive finite number, got ${timeScale}.`)
33
+ if (reducedMotion !== undefined && !preferences.includes(reducedMotion))
34
+ throw new TypeError(
35
+ `Reduced motion must be 'user', 'always' or 'never', got ${String(reducedMotion)}.`,
36
+ )
37
+ if (timeScale !== undefined) settings.timeScale = timeScale
38
+ if (reducedMotion !== undefined) {
39
+ settings.reducedMotion = reducedMotion
40
+ changed()
41
+ }
42
+ }
43
+
44
+ export function timeScale(): number {
45
+ return settings.timeScale
46
+ }
47
+
48
+ /** Whether motion is reduced right now. `user` asks the platform each time. */
49
+ export function reducedMotion(): boolean {
50
+ if (settings.reducedMotion === 'user') return getDriver().reducedMotion?.() ?? false
51
+ return settings.reducedMotion === 'always'
52
+ }
53
+
54
+ /**
55
+ * Calls back with the new value whenever `reducedMotion()` changes, from `configure()`
56
+ * or the platform setting. Returns a cleanup function.
57
+ *
58
+ * ```ts
59
+ * const off = onReducedMotion((reduced) => (illustration.visibility = reduced ? 'visible' : 'collapse'))
60
+ * ```
61
+ */
62
+ export function onReducedMotion(callback: (reduced: boolean) => void): () => void {
63
+ if (typeof callback !== 'function')
64
+ throw new TypeError('onReducedMotion() takes a callback.')
65
+ const listener = (reduced: boolean) => callback(reduced)
66
+ if (listeners.size === 0) {
67
+ reported = reducedMotion()
68
+ unwatch = getDriver().watchReducedMotion?.(changed)
69
+ }
70
+ listeners.add(listener)
71
+ return () => {
72
+ if (!listeners.delete(listener) || listeners.size > 0) return
73
+ unwatch?.()
74
+ unwatch = undefined
75
+ }
76
+ }
77
+
78
+ function changed() {
79
+ if (listeners.size === 0) return
80
+ const reduced = reducedMotion()
81
+ if (reduced === reported) return
82
+ reported = reduced
83
+ for (const listener of [...listeners]) listener(reduced)
84
+ }
package/src/decay.ts ADDED
@@ -0,0 +1,45 @@
1
+ export interface DecayMotion {
2
+ duration: number
3
+ to: number
4
+ position(time: number): number
5
+ velocity(time: number): number
6
+ }
7
+
8
+ /** Velocity retains this fraction every millisecond, independently of the display's refresh rate. */
9
+ export const DEFAULT_FRICTION = 0.998
10
+
11
+ /** Exponential motion, with velocity in units per second and time in milliseconds. */
12
+ export function solveDecay(
13
+ from: number,
14
+ velocity: number,
15
+ friction: number,
16
+ precision: number,
17
+ ): DecayMotion {
18
+ const rate = -Math.log(friction)
19
+ const distance = velocity / (1000 * rate)
20
+ const to = from + distance
21
+ // Stop only when both the remaining distance and speed are imperceptible.
22
+ const duration =
23
+ velocity === 0
24
+ ? 0
25
+ : Math.ceil(
26
+ Math.max(
27
+ 0,
28
+ Math.log(Math.abs(distance) / precision) / rate,
29
+ Math.log(Math.abs(velocity) / (precision * 10)) / rate,
30
+ ),
31
+ )
32
+
33
+ if (!Number.isFinite(to) || !Number.isFinite(duration) || duration > 2_147_483_647) {
34
+ throw new RangeError('Decay velocity and friction produce an unplayable motion.')
35
+ }
36
+
37
+ return {
38
+ duration,
39
+ to,
40
+ position: (time) =>
41
+ time >= duration ? to : from - distance * Math.expm1(-rate * Math.max(0, time)),
42
+ velocity: (time) =>
43
+ time >= duration ? 0 : velocity * Math.exp(-rate * Math.max(0, time)),
44
+ }
45
+ }
@@ -0,0 +1,180 @@
1
+ import { preserveZeroShadow } from './driver/ios-shadow.js'
2
+ import { Color, Utils, isAndroid, type View } from '@nativescript/core'
3
+
4
+ type Shadow = Exclude<View['boxShadow'], string>[number]
5
+ export type DecorationProperty =
6
+ | 'borderRadius'
7
+ | 'borderWidth'
8
+ | 'shadowOpacity'
9
+ | 'shadowRadius'
10
+ | 'shadowOffsetX'
11
+ | 'shadowOffsetY'
12
+ | 'elevation'
13
+ | 'translationZ'
14
+
15
+ export const decorationProperties: readonly DecorationProperty[] = [
16
+ 'borderRadius',
17
+ 'borderWidth',
18
+ 'shadowOpacity',
19
+ 'shadowRadius',
20
+ 'shadowOffsetX',
21
+ 'shadowOffsetY',
22
+ 'elevation',
23
+ 'translationZ',
24
+ ]
25
+ export const radiusKeys = [
26
+ 'borderTopLeftRadius',
27
+ 'borderTopRightRadius',
28
+ 'borderBottomRightRadius',
29
+ 'borderBottomLeftRadius',
30
+ ] as const
31
+ export const widthKeys = [
32
+ 'borderTopWidth',
33
+ 'borderRightWidth',
34
+ 'borderBottomWidth',
35
+ 'borderLeftWidth',
36
+ ] as const
37
+ const writing = new WeakSet<View>()
38
+ export const isWritingDecoration = (view: View) => writing.has(view)
39
+ export const isDecorationProperty = (key: string): key is DecorationProperty =>
40
+ decorationProperties.includes(key as DecorationProperty)
41
+
42
+ /** Core lengths may be numbers, px strings or parsed { value, unit } lengths. */
43
+ export function dip(value: unknown): number {
44
+ if (value === undefined || value === null) return 0
45
+ if (typeof value === 'number' && Number.isFinite(value)) return value
46
+ if (typeof value === 'string') {
47
+ const match = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(dip|px)?$/.exec(value.trim())
48
+ if (match)
49
+ return Number(match[1]) / (match[2] === 'px' ? Utils.layout.getDisplayDensity() : 1)
50
+ }
51
+ if (typeof value === 'object' && 'value' in value && 'unit' in value) {
52
+ const length = value as { value: number; unit: string }
53
+ if (Number.isFinite(length.value) && (length.unit === 'dip' || length.unit === 'px'))
54
+ return length.value / (length.unit === 'px' ? Utils.layout.getDisplayDensity() : 1)
55
+ }
56
+ throw new TypeError('Decoration animations require finite DIP or px lengths.')
57
+ }
58
+
59
+ export function singleShadow(view: View): Shadow {
60
+ const shadows = view.boxShadow
61
+ if (!Array.isArray(shadows) || shadows.length !== 1 || shadows[0]!.inset)
62
+ throw new TypeError('Shadow animation requires one existing outer boxShadow.')
63
+ return shadows[0]!
64
+ }
65
+
66
+ export function validateDecorationTarget(view: View, property: DecorationProperty) {
67
+ const shadow = property.startsWith('shadow')
68
+ const z = property === 'elevation' || property === 'translationZ'
69
+ if ((shadow && isAndroid) || (z && !isAndroid))
70
+ throw new TypeError(`"${property}" is only supported on ${z ? 'Android' : 'iOS'}.`)
71
+ if (z) return
72
+ if (view.style.clipPath || view.style.cornerShape === 'squircle')
73
+ throw new TypeError(
74
+ 'Decoration animations require circular corners without a clipPath.',
75
+ )
76
+ for (const keys of [radiusKeys, widthKeys]) {
77
+ const values = keys.map((key) => dip(view[key]))
78
+ if (values.some((value) => value !== values[0]))
79
+ throw new TypeError(
80
+ 'Decoration animations require uniform border widths and radii.',
81
+ )
82
+ }
83
+ const colors = [
84
+ view.borderTopColor,
85
+ view.borderRightColor,
86
+ view.borderBottomColor,
87
+ view.borderLeftColor,
88
+ ].map((color) => color?.argb ?? 0)
89
+ if (colors.some((color) => color !== colors[0]))
90
+ throw new TypeError('Decoration animations require uniform border colors.')
91
+ if (shadow) {
92
+ const value = singleShadow(view)
93
+ for (const length of [
94
+ value.offsetX,
95
+ value.offsetY,
96
+ value.blurRadius,
97
+ value.spreadRadius,
98
+ ])
99
+ dip(length)
100
+ }
101
+ if (
102
+ property === 'borderRadius' &&
103
+ isAndroid &&
104
+ view.boxShadow &&
105
+ (!Array.isArray(view.boxShadow) || view.boxShadow.length)
106
+ )
107
+ throw new TypeError(
108
+ 'Animate Android radius with elevation shadows; CSS boxShadow caches its shape.',
109
+ )
110
+ }
111
+
112
+ export function validateDecorationFrames(
113
+ property: DecorationProperty,
114
+ frames: readonly (number | null)[],
115
+ ) {
116
+ if (
117
+ frames.some((value) => value !== null && clampDecoration(property, value) !== value)
118
+ )
119
+ throw new RangeError(`Invalid keyframes for "${property}".`)
120
+ }
121
+ export function clampDecoration(property: DecorationProperty, value: number): number {
122
+ if (property === 'shadowOpacity') return Math.max(0, Math.min(1, value))
123
+ return property === 'borderRadius' ||
124
+ property === 'borderWidth' ||
125
+ property === 'shadowRadius' ||
126
+ property === 'elevation'
127
+ ? Math.max(0, value)
128
+ : value
129
+ }
130
+
131
+ export function readDecoration(view: View, property: DecorationProperty): number {
132
+ if (property === 'borderRadius') {
133
+ const radius = !isAndroid ? view.nativeViewProtected?.layer?.cornerRadius : undefined
134
+ return typeof radius === 'number' ? radius : dip(view.borderTopLeftRadius)
135
+ }
136
+ if (property === 'borderWidth') return dip(view.borderTopWidth)
137
+ if (property === 'elevation')
138
+ return view.nativeViewProtected?.getElevation
139
+ ? view.nativeViewProtected.getElevation() / Utils.layout.getDisplayDensity()
140
+ : view.androidElevation || 0
141
+ if (property === 'translationZ')
142
+ return (
143
+ view.nativeViewProtected?.getTranslationZ?.() / Utils.layout.getDisplayDensity() ||
144
+ 0
145
+ )
146
+ const shadow = singleShadow(view)
147
+ if (property === 'shadowOpacity')
148
+ return ((shadow.color?.argb ?? 0xff000000) >>> 24) / 255
149
+ return property === 'shadowRadius'
150
+ ? dip(shadow.blurRadius) / 2
151
+ : dip(property === 'shadowOffsetX' ? shadow.offsetX : shadow.offsetY)
152
+ }
153
+
154
+ export function writeDecoration(view: View, property: DecorationProperty, raw: number) {
155
+ const value = clampDecoration(property, raw)
156
+ writing.add(view)
157
+ try {
158
+ if (property === 'borderRadius' || property === 'borderWidth') view[property] = value
159
+ else if (property === 'elevation') view.androidElevation = value
160
+ else if (property === 'translationZ')
161
+ view.nativeViewProtected?.setTranslationZ?.(
162
+ value * Utils.layout.getDisplayDensity(),
163
+ )
164
+ else {
165
+ const shadow = { ...singleShadow(view) }
166
+ if (property === 'shadowOpacity')
167
+ shadow.color = new Color(
168
+ ((Math.round(value * 255) << 24) | ((shadow.color?.argb ?? 0) & 0xffffff)) >>>
169
+ 0,
170
+ )
171
+ else if (property === 'shadowRadius') shadow.blurRadius = value * 2
172
+ else if (property === 'shadowOffsetX') shadow.offsetX = value
173
+ else shadow.offsetY = value
174
+ view.boxShadow = [shadow]
175
+ if (!isAndroid) preserveZeroShadow(view)
176
+ }
177
+ } finally {
178
+ writing.delete(view)
179
+ }
180
+ }
@@ -0,0 +1,74 @@
1
+ import { Utils, type PanGestureEventData, type View } from '@nativescript/core'
2
+ import { getDriver } from './driver/index.js'
3
+
4
+ interface Sample {
5
+ x: number
6
+ y: number
7
+ time: number
8
+ }
9
+
10
+ /** Android's raw coordinates avoid feedback from translating the touched view. */
11
+ export class DragInput {
12
+ private samples: Sample[] = []
13
+ private lastMoved = 0
14
+ reset() {
15
+ this.samples = []
16
+ this.lastMoved = 0
17
+ }
18
+ read(event: PanGestureEventData, view: View) {
19
+ const ios = event.ios as UIPanGestureRecognizer | undefined
20
+ const android = event.android?.current as android.view.MotionEvent | undefined
21
+ const parent = view.parent?.nativeViewProtected ?? view.nativeViewProtected
22
+ const delta = ios
23
+ ? ios.translationInView(parent)
24
+ : { x: event.deltaX, y: event.deltaY }
25
+ const density = Utils.layout.getDisplayDensity()
26
+ const sample: Sample = android
27
+ ? {
28
+ x: android.getRawX() / density,
29
+ y: android.getRawY() / density,
30
+ time: Number(android.getEventTime()),
31
+ }
32
+ : { ...delta, time: getDriver().now() }
33
+ if (
34
+ Number.isFinite(sample.x) &&
35
+ Number.isFinite(sample.y) &&
36
+ Number.isFinite(sample.time)
37
+ ) {
38
+ const previous = this.samples.at(-1)
39
+ if (!previous || sample.x !== previous.x || sample.y !== previous.y)
40
+ this.lastMoved = sample.time
41
+ // Replace duplicate times (Android sends began + changed for the same event).
42
+ if (this.samples.at(-1)?.time === sample.time) this.samples.pop()
43
+ this.samples.push(sample)
44
+ while (this.samples.length > 2 && this.samples[1]!.time < sample.time - 100)
45
+ this.samples.shift()
46
+ }
47
+ const first = this.samples[0]
48
+ const elapsed = first ? sample.time - first.time : 0
49
+ // UIKit may retain its last movement velocity when the finger holds still
50
+ // before lifting. A stale velocity must not turn that hold into a fling.
51
+ const nativeVelocity = ios
52
+ ? sample.time - this.lastMoved > 100
53
+ ? { x: 0, y: 0 }
54
+ : ios.velocityInView(parent)
55
+ : undefined
56
+ const velocity = nativeVelocity ?? {
57
+ x: elapsed > 0 && elapsed <= 150 ? ((sample.x - first!.x) * 1000) / elapsed : 0,
58
+ y: elapsed > 0 && elapsed <= 150 ? ((sample.y - first!.y) * 1000) / elapsed : 0,
59
+ }
60
+ const action = android?.getActionMasked()
61
+ return {
62
+ x: delta.x,
63
+ y: delta.y,
64
+ velocityX: Number.isFinite(velocity.x) ? velocity.x : 0,
65
+ velocityY: Number.isFinite(velocity.y) ? velocity.y : 0,
66
+ cancelled:
67
+ event.state === 0 ||
68
+ action === 3 ||
69
+ action === 5 ||
70
+ action === 6 ||
71
+ (android ? android.getPointerCount() > 1 : (ios?.numberOfTouches ?? 1) > 1),
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,110 @@
1
+ import { solveDecay } from './decay.js'
2
+ import { solveSpring, springPhysics } from './spring.js'
3
+
4
+ export interface DragCurve {
5
+ type: 'drag'
6
+ velocity: number
7
+ friction: number
8
+ bounds: readonly [number, number]
9
+ snap?: readonly number[]
10
+ rubberband: boolean
11
+ }
12
+
13
+ export const clampDrag = (value: number, [min, max]: readonly [number, number]) =>
14
+ Math.max(min, Math.min(max, value))
15
+
16
+ /** Bounded resistance; coordinates and the resistance length are DIPs. */
17
+ export function resistDrag(
18
+ value: number,
19
+ bounds: readonly [number, number],
20
+ length: number,
21
+ ) {
22
+ const edge = clampDrag(value, bounds)
23
+ const distance = value - edge
24
+ return edge + (distance / (1 + Math.abs(distance) / length)) * 0.55
25
+ }
26
+
27
+ /** Recover the finger-space origin when grabbing an already rubberbanded pose. */
28
+ export function resumeDrag(
29
+ value: number,
30
+ bounds: readonly [number, number],
31
+ length: number,
32
+ ) {
33
+ const edge = clampDrag(value, bounds)
34
+ const distance = value - edge
35
+ // A native edge spring or external owner can exceed the usual resistance range.
36
+ // Expand it for this gesture so every finite visible pose has a stable inverse.
37
+ length = Math.max(length, Math.abs(distance) * 4)
38
+ return { origin: edge + distance / (0.55 - Math.abs(distance) / length), length }
39
+ }
40
+
41
+ export function dragDestination(from: number, curve: DragCurve) {
42
+ const projected = solveDecay(from, curve.velocity, curve.friction, 0.25).to
43
+ if (curve.snap)
44
+ return curve.snap.reduce((best, point) =>
45
+ Math.abs(point - projected) < Math.abs(best - projected) ? point : best,
46
+ )
47
+ return clampDrag(projected, curve.bounds)
48
+ }
49
+
50
+ /** One precomputed motion. Native keyframes play both the coast and edge recovery. */
51
+ export function solveDragRelease(from: number, curve: DragCurve) {
52
+ const to = dragDestination(from, curve)
53
+ const coast = solveDecay(from, curve.velocity, curve.friction, 0.25)
54
+ const edge = clampDrag(from, curve.bounds)
55
+ const springTo = (start: number, target: number, velocity: number) => {
56
+ const motion = solveSpring(
57
+ springPhysics({ duration: 500, bounce: 0 }),
58
+ start,
59
+ target,
60
+ velocity,
61
+ 0.25,
62
+ )
63
+ const position = (time: number) => {
64
+ const value = time >= motion.duration ? target : motion.position(time)
65
+ return curve.rubberband ? value : clampDrag(value, curve.bounds)
66
+ }
67
+ return {
68
+ ...motion,
69
+ position,
70
+ velocity: (time: number) => {
71
+ const raw = motion.position(time)
72
+ return time >= motion.duration ||
73
+ (!curve.rubberband && raw !== clampDrag(raw, curve.bounds))
74
+ ? 0
75
+ : motion.velocity(time)
76
+ },
77
+ }
78
+ }
79
+ if (curve.snap || from !== edge)
80
+ return {
81
+ ...springTo(from, curve.snap ? to : edge, curve.velocity),
82
+ to: curve.snap ? to : edge,
83
+ }
84
+ if (to === coast.to) return coast
85
+
86
+ // Reach the boundary with the decay's actual velocity, then absorb that velocity.
87
+ const rate = -Math.log(curve.friction)
88
+ const collision = Math.max(
89
+ 0,
90
+ -Math.log1p((-(to - from) * rate * 1000) / curve.velocity) / rate,
91
+ )
92
+ const recovery = springTo(to, to, coast.velocity(collision))
93
+ const duration = collision + (curve.rubberband ? recovery.duration : 0)
94
+ return {
95
+ to,
96
+ duration,
97
+ position: (time: number) =>
98
+ time >= duration
99
+ ? to
100
+ : time < collision
101
+ ? coast.position(time)
102
+ : recovery.position(time - collision),
103
+ velocity: (time: number) =>
104
+ time >= duration
105
+ ? 0
106
+ : time < collision
107
+ ? coast.velocity(time)
108
+ : recovery.velocity(time - collision),
109
+ }
110
+ }