@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
package/src/drag.ts ADDED
@@ -0,0 +1,395 @@
1
+ import {
2
+ Application,
3
+ type EventData,
4
+ type PanGestureEventData,
5
+ type TouchGestureEventData,
6
+ type View,
7
+ } from '@nativescript/core'
8
+ import { stop, toView } from './animate.js'
9
+ import { DEFAULT_FRICTION } from './decay.js'
10
+ import { DragInput } from './drag-input.js'
11
+ import {
12
+ clampDrag,
13
+ resistDrag,
14
+ resumeDrag,
15
+ solveDragRelease,
16
+ type DragCurve,
17
+ } from './drag-physics.js'
18
+ import { currentValue, ViewAnimation, type Plan } from './engine.js'
19
+ import type { PlaybackAnimation } from './playback.js'
20
+ import { readValue, writeValue } from './properties.js'
21
+ import type { Target } from './types.js'
22
+
23
+ export interface DragPoint {
24
+ readonly x: number
25
+ readonly y: number
26
+ }
27
+ export interface DragEvent extends DragPoint {
28
+ /** DIPs per second, in the same coordinates as the translation. */
29
+ readonly velocityX: number
30
+ readonly velocityY: number
31
+ }
32
+ export interface DragRelease extends DragEvent {
33
+ readonly target: DragPoint
34
+ /** False on interruption/cancellation. Check before applying completion effects. */
35
+ readonly animation: PlaybackAnimation
36
+ }
37
+ export interface DragOptions {
38
+ /** Defaults to both. One finger only. */
39
+ axis?: 'x' | 'y' | 'both'
40
+ /** Absolute translateX/translateY limits in DIPs; omitted edges are unbounded. */
41
+ bounds?: { left?: number; right?: number; top?: number; bottom?: number }
42
+ /** Closest stop to the projected landing. Arrays apply to every enabled axis. */
43
+ snap?: readonly number[] | { x?: readonly number[]; y?: readonly number[] }
44
+ /** Resist beyond bounds and spring back. Defaults to false (hard limits). */
45
+ rubberband?: boolean
46
+ /** Decay retention per millisecond. Defaults to 0.998. */
47
+ friction?: number
48
+ onStart?: (event: DragEvent) => void
49
+ onMove?: (event: DragEvent) => void
50
+ onRelease?: (event: DragRelease) => void
51
+ onCancel?: (event: DragEvent) => void
52
+ onSettle?: (position: DragPoint) => void
53
+ }
54
+ export interface Drag {
55
+ readonly dragging: boolean
56
+ readonly settling: boolean
57
+ readonly paused: boolean
58
+ readonly disposed: boolean
59
+ /** Current presentation, including a native release animation in flight. */
60
+ readonly position: DragPoint
61
+ /** Cancel contact/release at its visible position, keeping the subscription. */
62
+ cancel(): void
63
+ /** Cancel and detach input. play() accepts new gestures; cancelled work stays cancelled. */
64
+ pause(): void
65
+ play(): void
66
+ dispose(): void
67
+ }
68
+
69
+ type Axis = 'x' | 'y'
70
+ interface Settings {
71
+ axes: Axis[]
72
+ curves: Record<Axis, DragCurve>
73
+ callbacks: Pick<
74
+ DragOptions,
75
+ 'onStart' | 'onMove' | 'onRelease' | 'onCancel' | 'onSettle'
76
+ >
77
+ }
78
+ const owners = new WeakMap<View, Drag>()
79
+
80
+ function settings(options: DragOptions): Settings {
81
+ if (!options || typeof options !== 'object')
82
+ throw new TypeError('Drag options must be an object.')
83
+ const axis = options.axis ?? 'both'
84
+ if (!['x', 'y', 'both'].includes(axis))
85
+ throw new TypeError('Drag axis must be x, y or both.')
86
+ const friction = options.friction ?? DEFAULT_FRICTION
87
+ if (!Number.isFinite(friction) || friction <= 0 || friction >= 1)
88
+ throw new RangeError('Drag friction must be between 0 and 1 exclusively.')
89
+ if (options.rubberband !== undefined && typeof options.rubberband !== 'boolean')
90
+ throw new TypeError('Drag rubberband must be boolean.')
91
+ const bounds = options.bounds ?? {}
92
+ if (
93
+ typeof bounds !== 'object' ||
94
+ Array.isArray(bounds) ||
95
+ Object.entries(bounds).some(
96
+ ([key, value]) =>
97
+ !['left', 'right', 'top', 'bottom'].includes(key) ||
98
+ (value !== undefined && !Number.isFinite(value)),
99
+ )
100
+ )
101
+ throw new RangeError('Drag bounds must have finite left/right/top/bottom edges.')
102
+ const x: readonly [number, number] = [
103
+ bounds.left ?? -Infinity,
104
+ bounds.right ?? Infinity,
105
+ ]
106
+ const y: readonly [number, number] = [
107
+ bounds.top ?? -Infinity,
108
+ bounds.bottom ?? Infinity,
109
+ ]
110
+ if (x[0] > x[1] || y[0] > y[1]) throw new RangeError('Drag bounds must be ordered.')
111
+ const snap = options.snap
112
+ const shared = Array.isArray(snap)
113
+ if (
114
+ snap !== undefined &&
115
+ !shared &&
116
+ (!snap ||
117
+ typeof snap !== 'object' ||
118
+ Object.keys(snap).some((key) => key !== 'x' && key !== 'y'))
119
+ )
120
+ throw new TypeError('Drag snap must be an array or x/y arrays.')
121
+ const stops = (
122
+ value: readonly number[] | undefined,
123
+ range: readonly [number, number],
124
+ ) => {
125
+ if (value === undefined) return undefined
126
+ if (
127
+ !Array.isArray(value) ||
128
+ !value.length ||
129
+ ![...value].every((n) => Number.isFinite(n) && n >= range[0] && n <= range[1])
130
+ )
131
+ throw new RangeError('Drag snap needs nonempty finite positions within bounds.')
132
+ return [...value]
133
+ }
134
+ const perAxis = snap as { x?: readonly number[]; y?: readonly number[] } | undefined
135
+ const axes: Axis[] = axis === 'both' ? ['x', 'y'] : [axis]
136
+ const curves = Object.fromEntries(
137
+ (['x', 'y'] as const).map((key) => [
138
+ key,
139
+ {
140
+ type: 'drag',
141
+ velocity: 0,
142
+ friction,
143
+ bounds: key === 'x' ? x : y,
144
+ snap: stops(
145
+ shared && axes.includes(key) ? snap : perAxis?.[key],
146
+ key === 'x' ? x : y,
147
+ ),
148
+ rubberband: options.rubberband ?? false,
149
+ },
150
+ ]),
151
+ ) as Record<Axis, DragCurve>
152
+ const callbacks = {
153
+ onStart: options.onStart,
154
+ onMove: options.onMove,
155
+ onRelease: options.onRelease,
156
+ onCancel: options.onCancel,
157
+ onSettle: options.onSettle,
158
+ }
159
+ for (const callback of Object.values(callbacks))
160
+ if (callback !== undefined && typeof callback !== 'function')
161
+ throw new TypeError('Drag callbacks must be functions.')
162
+ return { axes, curves, callbacks }
163
+ }
164
+
165
+ /** Main-thread pan updates; release physics play through Jolt's native animation drivers. */
166
+ export function drag(target: Target, options: DragOptions = {}): Drag {
167
+ const view = toView(target)
168
+ const config = settings(options) // Invalid replacements leave the existing owner intact.
169
+ const previous = owners.get(view)
170
+ const controller = new DragController(view, config)
171
+ owners.set(view, controller)
172
+ // onCancel may synchronously install another drag. The most recent owner wins.
173
+ try {
174
+ previous?.dispose()
175
+ } catch (error) {
176
+ controller.dispose()
177
+ throw error
178
+ }
179
+ return controller
180
+ }
181
+
182
+ class DragController implements Drag {
183
+ private active = false
184
+ private disabled = false
185
+ private destroyed = false
186
+ private background = false
187
+ private attached = false
188
+ private blocked = false
189
+ private generation = 0
190
+ private release: PlaybackAnimation | undefined
191
+ private input = new DragInput()
192
+ private origin = { x: 0, y: 0 }
193
+ private last: DragEvent = { x: 0, y: 0, velocityX: 0, velocityY: 0 }
194
+ private lengths = { x: 200, y: 200 }
195
+ constructor(
196
+ private view: View,
197
+ private config: Settings,
198
+ ) {
199
+ view.on('loaded', this.attach)
200
+ view.on('unloaded', this.unload)
201
+ Application.on(Application.suspendEvent, this.suspend)
202
+ Application.on(Application.resumeEvent, this.resume)
203
+ this.attach()
204
+ }
205
+ get dragging() {
206
+ return this.active
207
+ }
208
+ get settling() {
209
+ return !!this.release
210
+ }
211
+ get paused() {
212
+ return this.disabled
213
+ }
214
+ get disposed() {
215
+ return this.destroyed
216
+ }
217
+ get position(): DragPoint {
218
+ return { x: currentValue(this.view, 'x'), y: currentValue(this.view, 'y') }
219
+ }
220
+ cancel = () => {
221
+ const wasActive = this.active
222
+ this.active = false
223
+ this.blocked = true
224
+ this.generation++
225
+ const release = this.release
226
+ this.release = undefined
227
+ release?.stop()
228
+ this.input.reset()
229
+ if (wasActive)
230
+ this.config.callbacks.onCancel?.({ ...this.last, velocityX: 0, velocityY: 0 })
231
+ }
232
+ pause = () => {
233
+ if (this.destroyed || this.disabled) return
234
+ this.disabled = true
235
+ this.detach()
236
+ }
237
+ play = () => {
238
+ if (this.destroyed) return
239
+ this.disabled = false
240
+ this.attach()
241
+ }
242
+ dispose = () => {
243
+ if (this.destroyed) return
244
+ this.destroyed = true
245
+ this.view.off('loaded', this.attach)
246
+ this.view.off('unloaded', this.unload)
247
+ Application.off(Application.suspendEvent, this.suspend)
248
+ Application.off(Application.resumeEvent, this.resume)
249
+ if (owners.get(this.view) === this) owners.delete(this.view)
250
+ this.detach()
251
+ }
252
+ private attach = () => {
253
+ if (
254
+ this.attached ||
255
+ this.destroyed ||
256
+ this.disabled ||
257
+ this.background ||
258
+ !this.view.isLoaded
259
+ )
260
+ return
261
+ this.blocked = false
262
+ this.attached = true
263
+ this.view.on('touch', this.touch)
264
+ this.view.on('pan', this.pan)
265
+ }
266
+ private detach = () => {
267
+ this.attached = false
268
+ this.view.off('touch', this.touch)
269
+ this.view.off('pan', this.pan)
270
+ this.cancel()
271
+ }
272
+ private unload = () => this.detach()
273
+ private suspend = () => {
274
+ this.background = true
275
+ this.detach()
276
+ }
277
+ private resume = () => {
278
+ this.background = false
279
+ this.attach()
280
+ }
281
+ private touch = (data: EventData) => {
282
+ const event = data as TouchGestureEventData
283
+ if (event.action === 'down') {
284
+ this.cancel()
285
+ this.blocked = false
286
+ } else if (event.action === 'cancel') this.cancel()
287
+ }
288
+ private pan = (data: EventData) => {
289
+ const event = data as PanGestureEventData
290
+ if (!this.attached || this.destroyed || this.disabled || this.background) return
291
+ if (this.view.isEnabled === false) {
292
+ this.cancel()
293
+ return
294
+ }
295
+ if (event.state === 1) this.input.reset()
296
+ const input = this.input.read(event, this.view)
297
+ if (input.cancelled) {
298
+ this.cancel()
299
+ return
300
+ }
301
+ if (this.blocked) return
302
+ if (event.state === 1) {
303
+ this.cancel()
304
+ if (!this.attached || this.destroyed || this.disabled) return
305
+ this.blocked = false
306
+ // Claim just the enabled axes, preserving the native presentation on takeover.
307
+ stop(this.view, this.config.axes)
308
+ this.origin = { x: readValue(this.view, 'x'), y: readValue(this.view, 'y') }
309
+ const size =
310
+ (this.view.parent as View | undefined)?.getActualSize() ??
311
+ this.view.getActualSize()
312
+ this.lengths = {
313
+ x: Math.max(1, size.width || 200),
314
+ y: Math.max(1, size.height || 200),
315
+ }
316
+ this.active = true
317
+ this.last = { ...this.origin, velocityX: 0, velocityY: 0 }
318
+ for (const axis of this.config.axes) {
319
+ const curve = this.config.curves[axis]
320
+ if (!curve.rubberband) continue
321
+ const resumed = resumeDrag(this.origin[axis], curve.bounds, this.lengths[axis])
322
+ this.origin[axis] = resumed.origin
323
+ this.lengths[axis] = resumed.length
324
+ }
325
+ this.input.read(event, this.view)
326
+ this.emit(() => this.config.callbacks.onStart?.({ ...this.last }))
327
+ }
328
+ if (!this.active) return
329
+ const generation = this.generation
330
+ if (Number.isFinite(input.x) && Number.isFinite(input.y)) {
331
+ const position = { x: this.last.x, y: this.last.y }
332
+ let vx = 0,
333
+ vy = 0
334
+ for (const axis of this.config.axes) {
335
+ const curve = this.config.curves[axis]
336
+ const raw = this.origin[axis] + input[axis]
337
+ const distance = raw - clampDrag(raw, curve.bounds)
338
+ const resistance =
339
+ distance === 0
340
+ ? 1
341
+ : curve.rubberband
342
+ ? 0.55 / (1 + Math.abs(distance) / this.lengths[axis]) ** 2
343
+ : 0
344
+ position[axis] = curve.rubberband
345
+ ? resistDrag(raw, curve.bounds, this.lengths[axis])
346
+ : clampDrag(raw, curve.bounds)
347
+ if (axis === 'x') vx = input.velocityX * resistance
348
+ else vy = input.velocityY * resistance
349
+ stop(this.view, [axis])
350
+ writeValue(this.view, axis, position[axis])
351
+ }
352
+ this.last = { ...position, velocityX: vx, velocityY: vy }
353
+ if (event.state !== 3)
354
+ this.emit(() => this.config.callbacks.onMove?.({ ...this.last }))
355
+ }
356
+ if (!this.active || generation !== this.generation) return
357
+ if (event.state === 3) this.emit(() => this.finish())
358
+ }
359
+ private emit(callback: () => void) {
360
+ try {
361
+ callback()
362
+ } catch (error) {
363
+ // A failing application callback must not leave live input or an orphaned fling.
364
+ this.active = false
365
+ this.dispose()
366
+ throw error
367
+ }
368
+ }
369
+ private finish() {
370
+ this.active = false
371
+ const generation = ++this.generation
372
+ const event = { ...this.last }
373
+ const target = { x: event.x, y: event.y }
374
+ const plans: Plan[] = this.config.axes.map((axis) => {
375
+ const curve = {
376
+ ...this.config.curves[axis],
377
+ velocity: axis === 'x' ? event.velocityX : event.velocityY,
378
+ }
379
+ target[axis] = solveDragRelease(event[axis], curve).to
380
+ return {
381
+ property: axis,
382
+ keyframes: [null, null],
383
+ transition: { curve, delay: 0, repeat: 0, reverse: false },
384
+ }
385
+ })
386
+ const animation = new ViewAnimation(this.view, plans)
387
+ this.release = animation
388
+ void animation.finished.then((finished) => {
389
+ if (generation !== this.generation || this.destroyed) return
390
+ this.release = undefined
391
+ if (finished) this.config.callbacks.onSettle?.({ ...target })
392
+ })
393
+ this.emit(() => this.config.callbacks.onRelease?.({ ...event, target, animation }))
394
+ }
395
+ }
@@ -0,0 +1,170 @@
1
+ import { type View } from '@nativescript/core'
2
+ import { readColor, type ColorProperty } from '../color.js'
3
+ import { androidPlayback, type AndroidPlayback } from './android-playback.js'
4
+ import type { PlaybackCommand } from '../playback.js'
5
+ import type { Track } from '../track.js'
6
+
7
+ interface NativeTarget {
8
+ getColor(): number
9
+ setColor(value: number): void
10
+ isSupported(): boolean
11
+ }
12
+ interface ColorHandle {
13
+ control(elapsed: number, paused: boolean, command?: PlaybackCommand): void
14
+ cancel(): void
15
+ elapsed(): number
16
+ target: NativeTarget
17
+ }
18
+ const playing = new WeakMap<View, Map<ColorProperty, ColorHandle>>()
19
+
20
+ export function syncAndroidColor(view: View) {
21
+ for (const handle of playing.get(view)?.values() ?? [])
22
+ handle.target.setColor(handle.target.getColor())
23
+ }
24
+
25
+ export function presentedAndroidColor(view: View, property: ColorProperty): number {
26
+ const active = playing.get(view)?.get(property)
27
+ if (active) return active.target.getColor() >>> 0
28
+ if (property === 'color' && view.nativeViewProtected instanceof android.widget.TextView)
29
+ return view.nativeViewProtected.getCurrentTextColor() >>> 0
30
+ return readColor(view, property)
31
+ }
32
+
33
+ export function playAndroidColor(
34
+ view: View,
35
+ track: Track,
36
+ onEnd: (finished?: boolean) => void,
37
+ ): ColorHandle {
38
+ const property = track.property as ColorProperty
39
+ const changes =
40
+ property === 'borderColor'
41
+ ? [
42
+ 'borderTopColorChange',
43
+ 'borderRightColorChange',
44
+ 'borderBottomColorChange',
45
+ 'borderLeftColorChange',
46
+ ]
47
+ : [`${property}Change`]
48
+ const bridge = globalThis as unknown as {
49
+ org: {
50
+ rafaelaugusto: {
51
+ jolt: {
52
+ ColorTarget: new (
53
+ view: android.view.View,
54
+ property: number,
55
+ color: number,
56
+ ) => NativeTarget
57
+ }
58
+ }
59
+ }
60
+ }
61
+ let target: NativeTarget
62
+ try {
63
+ target = new bridge.org.rafaelaugusto.jolt.ColorTarget(
64
+ view.nativeViewProtected,
65
+ property === 'backgroundColor' ? 0 : property === 'borderColor' ? 1 : 2,
66
+ track.values[0]! | 0,
67
+ )
68
+ } catch {
69
+ // A JS-only update cannot install the native plugin. Keep the final model.
70
+ onEnd(false)
71
+ return {
72
+ cancel() {},
73
+ control() {},
74
+ elapsed: () => 0,
75
+ target: {
76
+ getColor: () => readColor(view, property),
77
+ setColor() {},
78
+ isSupported: () => false,
79
+ },
80
+ }
81
+ }
82
+ const frames = Array.create(android.animation.Keyframe, track.values.length)
83
+ track.values.forEach((value, index) => {
84
+ frames[index] = android.animation.Keyframe.ofInt(track.times[index]!, value | 0)
85
+ })
86
+ const holder = android.animation.PropertyValuesHolder.ofKeyframe('color', frames)
87
+ holder.setEvaluator(new android.animation.ArgbEvaluator())
88
+ const holders = Array.create(android.animation.PropertyValuesHolder, 1)
89
+ holders[0] = holder
90
+ const animator = android.animation.ObjectAnimator.ofPropertyValuesHolder(
91
+ target as unknown as java.lang.Object,
92
+ holders,
93
+ )
94
+ animator.setDuration(Math.max(track.duration, 1))
95
+ animator.setStartDelay(track.delay)
96
+ animator.setInterpolator(new android.view.animation.LinearInterpolator())
97
+ if (track.repeat > 0) {
98
+ animator.setRepeatCount(
99
+ Number.isFinite(track.repeat)
100
+ ? track.repeat
101
+ : android.animation.ValueAnimator.INFINITE,
102
+ )
103
+ animator.setRepeatMode(
104
+ track.reverse
105
+ ? android.animation.ValueAnimator.REVERSE
106
+ : android.animation.ValueAnimator.RESTART,
107
+ )
108
+ }
109
+ let stopped = false,
110
+ queued = false
111
+ const cleanup = () => {
112
+ if (stopped) return
113
+ stopped = true
114
+ view.off('unloaded', cancel)
115
+ if (property === 'color') {
116
+ view.off('textChange', invalidate)
117
+ view.off('formattedTextChange', invalidate)
118
+ }
119
+ for (const name of changes) view.style.off(name, invalidate)
120
+ if (playing.get(view)?.get(property) === handle) playing.get(view)?.delete(property)
121
+ }
122
+ let player: AndroidPlayback | undefined
123
+ const cancel = () => {
124
+ if (stopped) return
125
+ cleanup()
126
+ player?.cancel()
127
+ onEnd(false)
128
+ }
129
+ const invalidate = () => {
130
+ if (stopped || queued) return
131
+ queued = true
132
+ void Promise.resolve().then(() => {
133
+ queued = false
134
+ if (!stopped) cancel()
135
+ })
136
+ }
137
+ const handle: ColorHandle = {
138
+ target,
139
+ elapsed: () => player?.elapsed() ?? 0,
140
+ control(time, paused, command) {
141
+ player?.control(time, paused, command)
142
+ },
143
+ cancel() {
144
+ cleanup()
145
+ player?.cancel()
146
+ },
147
+ }
148
+ let entries = playing.get(view)
149
+ if (!entries) playing.set(view, (entries = new Map()))
150
+ entries.set(property, handle)
151
+ view.on('unloaded', cancel)
152
+ if (property === 'color') {
153
+ view.on('textChange', invalidate)
154
+ view.on('formattedTextChange', invalidate)
155
+ }
156
+ for (const name of changes) view.style.on(name, invalidate)
157
+ if (target.isSupported()) {
158
+ target.setColor(track.values[0]! | 0)
159
+ player = androidPlayback(animator, track, () => {
160
+ if (!stopped) {
161
+ cleanup()
162
+ onEnd()
163
+ }
164
+ })
165
+ } else {
166
+ cleanup()
167
+ onEnd(false)
168
+ }
169
+ return handle
170
+ }