@rafaelaugusto/jolt 0.1.0-beta.1 → 0.1.0-beta.11

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 (318) hide show
  1. package/README.md +385 -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-gradient.d.ts +5 -0
  43. package/dist/driver/android-gradient.d.ts.map +1 -0
  44. package/dist/driver/android-gradient.js +89 -0
  45. package/dist/driver/android-gradient.js.map +1 -0
  46. package/dist/driver/android-playback.d.ts +13 -0
  47. package/dist/driver/android-playback.d.ts.map +1 -0
  48. package/dist/driver/android-playback.js +141 -0
  49. package/dist/driver/android-playback.js.map +1 -0
  50. package/dist/driver/android.d.ts +1 -1
  51. package/dist/driver/android.d.ts.map +1 -1
  52. package/dist/driver/android.js +165 -13
  53. package/dist/driver/android.js.map +1 -1
  54. package/dist/driver/index.d.ts +25 -2
  55. package/dist/driver/index.d.ts.map +1 -1
  56. package/dist/driver/index.js.map +1 -1
  57. package/dist/driver/ios-color.d.ts +16 -0
  58. package/dist/driver/ios-color.d.ts.map +1 -0
  59. package/dist/driver/ios-color.js +392 -0
  60. package/dist/driver/ios-color.js.map +1 -0
  61. package/dist/driver/ios-decoration.d.ts +17 -0
  62. package/dist/driver/ios-decoration.d.ts.map +1 -0
  63. package/dist/driver/ios-decoration.js +271 -0
  64. package/dist/driver/ios-decoration.js.map +1 -0
  65. package/dist/driver/ios-gradient.d.ts +5 -0
  66. package/dist/driver/ios-gradient.d.ts.map +1 -0
  67. package/dist/driver/ios-gradient.js +109 -0
  68. package/dist/driver/ios-gradient.js.map +1 -0
  69. package/dist/driver/ios-layout.d.ts +9 -0
  70. package/dist/driver/ios-layout.d.ts.map +1 -0
  71. package/dist/driver/ios-layout.js +52 -0
  72. package/dist/driver/ios-layout.js.map +1 -0
  73. package/dist/driver/ios-playback.d.ts +10 -0
  74. package/dist/driver/ios-playback.d.ts.map +1 -0
  75. package/dist/driver/ios-playback.js +47 -0
  76. package/dist/driver/ios-playback.js.map +1 -0
  77. package/dist/driver/ios-shadow.d.ts +6 -0
  78. package/dist/driver/ios-shadow.d.ts.map +1 -0
  79. package/dist/driver/ios-shadow.js +52 -0
  80. package/dist/driver/ios-shadow.js.map +1 -0
  81. package/dist/driver/ios.d.ts.map +1 -1
  82. package/dist/driver/ios.js +163 -28
  83. package/dist/driver/ios.js.map +1 -1
  84. package/dist/engine.d.ts +75 -5
  85. package/dist/engine.d.ts.map +1 -1
  86. package/dist/engine.js +293 -23
  87. package/dist/engine.js.map +1 -1
  88. package/dist/gradient.d.ts +90 -0
  89. package/dist/gradient.d.ts.map +1 -0
  90. package/dist/gradient.js +247 -0
  91. package/dist/gradient.js.map +1 -0
  92. package/dist/haptics.d.ts +21 -0
  93. package/dist/haptics.d.ts.map +1 -0
  94. package/dist/haptics.js +120 -0
  95. package/dist/haptics.js.map +1 -0
  96. package/dist/image-effect-native.d.ts +26 -0
  97. package/dist/image-effect-native.d.ts.map +1 -0
  98. package/dist/image-effect-native.js +73 -0
  99. package/dist/image-effect-native.js.map +1 -0
  100. package/dist/image-effect.d.ts +50 -0
  101. package/dist/image-effect.d.ts.map +1 -0
  102. package/dist/image-effect.js +290 -0
  103. package/dist/image-effect.js.map +1 -0
  104. package/dist/image-owner.d.ts +3 -0
  105. package/dist/image-owner.d.ts.map +1 -0
  106. package/dist/image-owner.js +11 -0
  107. package/dist/image-owner.js.map +1 -0
  108. package/dist/index.d.ts +47 -4
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +22 -2
  111. package/dist/index.js.map +1 -1
  112. package/dist/interaction-surface.d.ts +12 -0
  113. package/dist/interaction-surface.d.ts.map +1 -0
  114. package/dist/interaction-surface.js +36 -0
  115. package/dist/interaction-surface.js.map +1 -0
  116. package/dist/items.d.ts +20 -0
  117. package/dist/items.d.ts.map +1 -0
  118. package/dist/items.js +109 -0
  119. package/dist/items.js.map +1 -0
  120. package/dist/keyboard.d.ts +41 -0
  121. package/dist/keyboard.d.ts.map +1 -0
  122. package/dist/keyboard.js +256 -0
  123. package/dist/keyboard.js.map +1 -0
  124. package/dist/label.d.ts +7 -0
  125. package/dist/label.d.ts.map +1 -0
  126. package/dist/label.js +60 -0
  127. package/dist/label.js.map +1 -0
  128. package/dist/layout.d.ts +13 -0
  129. package/dist/layout.d.ts.map +1 -0
  130. package/dist/layout.js +348 -0
  131. package/dist/layout.js.map +1 -0
  132. package/dist/liquid-grid.d.ts +3 -0
  133. package/dist/liquid-grid.d.ts.map +1 -0
  134. package/dist/liquid-grid.js +24 -0
  135. package/dist/liquid-grid.js.map +1 -0
  136. package/dist/liquid.d.ts +18 -0
  137. package/dist/liquid.d.ts.map +1 -0
  138. package/dist/liquid.js +234 -0
  139. package/dist/liquid.js.map +1 -0
  140. package/dist/loader.d.ts +19 -0
  141. package/dist/loader.d.ts.map +1 -0
  142. package/dist/loader.js +173 -0
  143. package/dist/loader.js.map +1 -0
  144. package/dist/motion-scope.d.ts +39 -0
  145. package/dist/motion-scope.d.ts.map +1 -0
  146. package/dist/motion-scope.js +238 -0
  147. package/dist/motion-scope.js.map +1 -0
  148. package/dist/motion-states.d.ts +43 -0
  149. package/dist/motion-states.d.ts.map +1 -0
  150. package/dist/motion-states.js +220 -0
  151. package/dist/motion-states.js.map +1 -0
  152. package/dist/particles.d.ts +39 -0
  153. package/dist/particles.d.ts.map +1 -0
  154. package/dist/particles.js +265 -0
  155. package/dist/particles.js.map +1 -0
  156. package/dist/path.d.ts +12 -0
  157. package/dist/path.d.ts.map +1 -0
  158. package/dist/path.js +90 -0
  159. package/dist/path.js.map +1 -0
  160. package/dist/playback-group.d.ts +6 -0
  161. package/dist/playback-group.d.ts.map +1 -0
  162. package/dist/playback-group.js +141 -0
  163. package/dist/playback-group.js.map +1 -0
  164. package/dist/playback.d.ts +25 -0
  165. package/dist/playback.d.ts.map +1 -0
  166. package/dist/playback.js +35 -0
  167. package/dist/playback.js.map +1 -0
  168. package/dist/press.d.ts.map +1 -1
  169. package/dist/press.js +23 -7
  170. package/dist/press.js.map +1 -1
  171. package/dist/properties.d.ts +6 -3
  172. package/dist/properties.d.ts.map +1 -1
  173. package/dist/properties.js +26 -2
  174. package/dist/properties.js.map +1 -1
  175. package/dist/reveal.d.ts +25 -0
  176. package/dist/reveal.d.ts.map +1 -0
  177. package/dist/reveal.js +294 -0
  178. package/dist/reveal.js.map +1 -0
  179. package/dist/roll-diff.d.ts +22 -0
  180. package/dist/roll-diff.d.ts.map +1 -0
  181. package/dist/roll-diff.js +36 -0
  182. package/dist/roll-diff.js.map +1 -0
  183. package/dist/roll.d.ts +29 -0
  184. package/dist/roll.d.ts.map +1 -0
  185. package/dist/roll.js +442 -0
  186. package/dist/roll.js.map +1 -0
  187. package/dist/scroll-linked.d.ts +10 -0
  188. package/dist/scroll-linked.d.ts.map +1 -0
  189. package/dist/scroll-linked.js +58 -0
  190. package/dist/scroll-linked.js.map +1 -0
  191. package/dist/sequence.d.ts +10 -5
  192. package/dist/sequence.d.ts.map +1 -1
  193. package/dist/sequence.js +208 -14
  194. package/dist/sequence.js.map +1 -1
  195. package/dist/shimmer.d.ts +12 -0
  196. package/dist/shimmer.d.ts.map +1 -0
  197. package/dist/shimmer.js +161 -0
  198. package/dist/shimmer.js.map +1 -0
  199. package/dist/svg-path.d.ts +14 -0
  200. package/dist/svg-path.d.ts.map +1 -0
  201. package/dist/svg-path.js +281 -0
  202. package/dist/svg-path.js.map +1 -0
  203. package/dist/symbol-effect.d.ts +23 -0
  204. package/dist/symbol-effect.d.ts.map +1 -0
  205. package/dist/symbol-effect.js +187 -0
  206. package/dist/symbol-effect.js.map +1 -0
  207. package/dist/text-layout.d.ts +30 -0
  208. package/dist/text-layout.d.ts.map +1 -0
  209. package/dist/text-layout.js +185 -0
  210. package/dist/text-layout.js.map +1 -0
  211. package/dist/text.d.ts +17 -0
  212. package/dist/text.d.ts.map +1 -0
  213. package/dist/text.js +263 -0
  214. package/dist/text.js.map +1 -0
  215. package/dist/track.d.ts +3 -1
  216. package/dist/track.d.ts.map +1 -1
  217. package/dist/track.js +266 -13
  218. package/dist/track.js.map +1 -1
  219. package/dist/transition.d.ts +32 -1
  220. package/dist/transition.d.ts.map +1 -1
  221. package/dist/transition.js +58 -6
  222. package/dist/transition.js.map +1 -1
  223. package/dist/types.d.ts +48 -1
  224. package/dist/types.d.ts.map +1 -1
  225. package/dist/vue/index.d.ts +95 -0
  226. package/dist/vue/index.d.ts.map +1 -0
  227. package/dist/vue/index.js +163 -0
  228. package/dist/vue/index.js.map +1 -0
  229. package/dist/zoom-native.d.ts +30 -0
  230. package/dist/zoom-native.d.ts.map +1 -0
  231. package/dist/zoom-native.js +56 -0
  232. package/dist/zoom-native.js.map +1 -0
  233. package/dist/zoomable.d.ts +48 -0
  234. package/dist/zoomable.d.ts.map +1 -0
  235. package/dist/zoomable.js +249 -0
  236. package/dist/zoomable.js.map +1 -0
  237. package/package.json +29 -5
  238. package/platforms/android/include.gradle +7 -0
  239. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  240. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  241. package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +195 -0
  242. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  243. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  244. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
  245. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  246. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  247. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  248. package/platforms/android/proguard-rules.pro +21 -0
  249. package/platforms/ios/build.xcconfig +1 -0
  250. package/platforms/ios/src/JoltImageEffect.h +25 -0
  251. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  252. package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
  253. package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
  254. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  255. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  256. package/platforms/ios/src/module.modulemap +6 -0
  257. package/src/android-plugin.d.ts +184 -0
  258. package/src/animate.ts +249 -36
  259. package/src/color.ts +101 -0
  260. package/src/config.ts +84 -0
  261. package/src/decay.ts +45 -0
  262. package/src/decoration.ts +180 -0
  263. package/src/drag-input.ts +74 -0
  264. package/src/drag-physics.ts +110 -0
  265. package/src/drag.ts +395 -0
  266. package/src/driver/android-color.ts +170 -0
  267. package/src/driver/android-decoration.ts +182 -0
  268. package/src/driver/android-gradient.ts +109 -0
  269. package/src/driver/android-playback.ts +157 -0
  270. package/src/driver/android.ts +208 -20
  271. package/src/driver/index.ts +37 -2
  272. package/src/driver/ios-color.ts +441 -0
  273. package/src/driver/ios-decoration.ts +332 -0
  274. package/src/driver/ios-gradient.ts +114 -0
  275. package/src/driver/ios-layout.ts +63 -0
  276. package/src/driver/ios-playback.ts +51 -0
  277. package/src/driver/ios-shadow.ts +50 -0
  278. package/src/driver/ios.ts +193 -33
  279. package/src/engine.ts +321 -27
  280. package/src/gradient.ts +372 -0
  281. package/src/haptics.ts +148 -0
  282. package/src/image-effect-native.ts +129 -0
  283. package/src/image-effect.ts +336 -0
  284. package/src/image-owner.ts +12 -0
  285. package/src/index.ts +75 -4
  286. package/src/interaction-surface.ts +41 -0
  287. package/src/ios-plugin.d.ts +81 -0
  288. package/src/items.ts +140 -0
  289. package/src/keyboard.ts +291 -0
  290. package/src/label.ts +76 -0
  291. package/src/layout.ts +408 -0
  292. package/src/liquid-grid.ts +28 -0
  293. package/src/liquid.ts +287 -0
  294. package/src/loader.ts +202 -0
  295. package/src/motion-scope.ts +279 -0
  296. package/src/motion-states.ts +269 -0
  297. package/src/particles.ts +323 -0
  298. package/src/path.ts +124 -0
  299. package/src/playback-group.ts +143 -0
  300. package/src/playback.ts +62 -0
  301. package/src/press.ts +26 -10
  302. package/src/properties.ts +32 -6
  303. package/src/reveal.ts +374 -0
  304. package/src/roll-diff.ts +57 -0
  305. package/src/roll.ts +515 -0
  306. package/src/scroll-linked.ts +73 -0
  307. package/src/sequence.ts +204 -14
  308. package/src/shimmer.ts +180 -0
  309. package/src/svg-path.ts +355 -0
  310. package/src/symbol-effect.ts +226 -0
  311. package/src/text-layout.ts +228 -0
  312. package/src/text.ts +279 -0
  313. package/src/track.ts +347 -12
  314. package/src/transition.ts +93 -6
  315. package/src/types.ts +49 -1
  316. package/src/vue/index.ts +231 -0
  317. package/src/zoom-native.ts +124 -0
  318. package/src/zoomable.ts +330 -0
@@ -0,0 +1,372 @@
1
+ import { isAndroid, isIOS, type View } from '@nativescript/core'
2
+ import { toView } from './animate.js'
3
+ import { colorMixer, parseColor, type ColorValue } from './color.js'
4
+ import { reducedMotion } from './config.js'
5
+ import { getDriver } from './driver/index.js'
6
+ import { androidGradient } from './driver/android-gradient.js'
7
+ import { iosGradient } from './driver/ios-gradient.js'
8
+ import type { Animation } from './engine.js'
9
+ import { createTrack, type Track } from './track.js'
10
+ import {
11
+ resolveTransition,
12
+ springs,
13
+ type SpringName,
14
+ type Transition,
15
+ } from './transition.js'
16
+ import type { Target } from './types.js'
17
+
18
+ export type GradientType = 'linear' | 'radial' | 'conic'
19
+
20
+ export interface GradientValues {
21
+ /** Two to eight colors. Animating keeps the count. */
22
+ colors?: readonly ColorValue[]
23
+ /** Where each color sits, from 0 to 1. Evenly spaced by default. */
24
+ stops?: readonly number[]
25
+ /** Degrees, like CSS: `linear` points this way (180 runs top to bottom), `conic` starts here (0 is up). */
26
+ angle?: number
27
+ /** `radial` and `conic`: the center, in fractions of the view's width and height. */
28
+ center?: { readonly x: number; readonly y: number }
29
+ /** `radial`: the radius, in fractions of the view's shorter side. */
30
+ radius?: number
31
+ }
32
+
33
+ export interface GradientOptions extends GradientValues {
34
+ type: GradientType
35
+ colors: readonly ColorValue[]
36
+ }
37
+
38
+ export interface Gradient {
39
+ readonly type: GradientType
40
+ /** Animates to new values from the ones on screen. Colors and stops keep their count. */
41
+ animate(values: GradientValues, transition?: Transition | SpringName): Animation
42
+ /** Applies values at once, stopping the animation in progress. */
43
+ set(values: GradientValues): void
44
+ /** Stops the animation in progress where it is. */
45
+ stop(): void
46
+ /** Takes the gradient off the view. */
47
+ remove(): void
48
+ }
49
+
50
+ /** Everything the platform needs to paint one frame. */
51
+ export interface GradientFrame {
52
+ colors: number[]
53
+ stops: number[]
54
+ angle: number
55
+ x: number
56
+ y: number
57
+ radius: number
58
+ }
59
+
60
+ export interface GradientTiming {
61
+ duration: number
62
+ delay: number
63
+ repeat: number
64
+ reverse: boolean
65
+ }
66
+
67
+ export interface GradientPainter {
68
+ paint(frame: GradientFrame): void
69
+ /** Plays sampled frames natively; `onEnd(false)` when cancelled. */
70
+ play(
71
+ frames: readonly GradientFrame[],
72
+ times: readonly number[],
73
+ timing: GradientTiming,
74
+ onEnd: (finished: boolean) => void,
75
+ ): { cancel(): void }
76
+ remove(): void
77
+ }
78
+
79
+ type PainterFactory = (view: View, type: GradientType) => GradientPainter | undefined
80
+
81
+ let factory: PainterFactory | undefined
82
+
83
+ /** Test seam, like `setDriver`. */
84
+ export function setGradientPainter(next: PainterFactory | undefined) {
85
+ factory = next
86
+ }
87
+
88
+ function platformPainter(view: View, type: GradientType) {
89
+ if (isIOS) return iosGradient(view, type)
90
+ if (isAndroid) return androidGradient(view, type)
91
+ return undefined
92
+ }
93
+
94
+ const types: readonly GradientType[] = ['linear', 'radial', 'conic']
95
+
96
+ /**
97
+ * Paints a native gradient behind a layout's children and animates it.
98
+ *
99
+ * ```ts
100
+ * const glow = gradient(halo, { type: 'radial', colors: ['#FF0080', '#7928CA00'], radius: 0.8 })
101
+ * glow.animate({ center: { x: 0.8, y: 0.2 }, radius: 1.2 }, { duration: 2000 })
102
+ * ```
103
+ */
104
+ export function gradient(target: Target, options: GradientOptions): Gradient {
105
+ const view = toView(target)
106
+ if (!options || !types.includes(options.type))
107
+ throw new TypeError(`Gradient type must be 'linear', 'radial' or 'conic'.`)
108
+ const first = resolve(
109
+ { colors: [], stops: [], angle: 180, x: 0.5, y: 0.5, radius: 0.5 },
110
+ options,
111
+ false,
112
+ )
113
+ return new GradientController(view, options.type, first)
114
+ }
115
+
116
+ class GradientController implements Gradient {
117
+ private painter: GradientPainter | undefined
118
+ private pending: GradientFrame | undefined
119
+ private removed = false
120
+ private model: GradientFrame
121
+ private running:
122
+ | {
123
+ from: GradientFrame
124
+ to: GradientFrame
125
+ track: Track
126
+ startedAt: number
127
+ handle: { cancel(): void }
128
+ settle: (finished: boolean) => void
129
+ }
130
+ | undefined
131
+
132
+ constructor(
133
+ private readonly view: View,
134
+ readonly type: GradientType,
135
+ frame: GradientFrame,
136
+ ) {
137
+ this.model = frame
138
+ this.attach((factory ?? platformPainter)(view, type))
139
+ view.on('unloaded', this.unloaded)
140
+ }
141
+
142
+ private unloaded = () => this.stop()
143
+
144
+ private attach(painter: GradientPainter | undefined) {
145
+ if (!painter) return
146
+ if (this.removed) return painter.remove()
147
+ this.painter = painter
148
+ painter.paint(this.pending ?? this.model)
149
+ this.pending = undefined
150
+ }
151
+
152
+ animate(values: GradientValues, transition?: Transition | SpringName): Animation {
153
+ this.assertActive()
154
+ const from = this.presented()
155
+ const to = resolve(from, values, true)
156
+ const resolved =
157
+ typeof transition === 'string' ? springOf(transition) : (transition ?? {})
158
+ const track = createTrack(
159
+ 'opacity',
160
+ [0, 1],
161
+ resolveTransition(resolved, 'opacity', 2),
162
+ 0,
163
+ )
164
+ const reduced = reducedMotion()
165
+ const frameAt = (progress: number) => mix(from, to, progress, reduced)
166
+ this.cancelRunning()
167
+ this.model = frameAt(track.to)
168
+
169
+ let settle!: (finished: boolean) => void
170
+ const finished = new Promise<boolean>((resolve) => (settle = resolve))
171
+ const animation: Animation = {
172
+ finished,
173
+ stop: () => {
174
+ if (this.running?.settle === settle) this.stop()
175
+ },
176
+ then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
177
+ }
178
+ if (track.end === 0 || !this.painter) {
179
+ this.paint(this.model)
180
+ settle(true)
181
+ return animation
182
+ }
183
+ const handle = this.painter.play(
184
+ track.values.map(frameAt),
185
+ track.times,
186
+ {
187
+ duration: track.duration,
188
+ delay: track.delay,
189
+ repeat: track.repeat,
190
+ reverse: track.reverse,
191
+ },
192
+ (completed) => {
193
+ if (this.running?.settle !== settle) return
194
+ this.running = undefined
195
+ settle(completed)
196
+ },
197
+ )
198
+ this.running = {
199
+ from,
200
+ to,
201
+ track,
202
+ startedAt: getDriver().now(),
203
+ handle,
204
+ settle,
205
+ }
206
+ return animation
207
+ }
208
+
209
+ set(values: GradientValues) {
210
+ this.assertActive()
211
+ const next = resolve(this.presented(), values, false)
212
+ this.cancelRunning()
213
+ this.model = next
214
+ this.paint(next)
215
+ }
216
+
217
+ stop() {
218
+ if (!this.running) return
219
+ const current = this.presented()
220
+ this.cancelRunning()
221
+ this.model = current
222
+ this.paint(current)
223
+ }
224
+
225
+ remove() {
226
+ if (this.removed) return
227
+ this.cancelRunning()
228
+ this.removed = true
229
+ this.view.off('unloaded', this.unloaded)
230
+ this.painter?.remove()
231
+ this.painter = undefined
232
+ }
233
+
234
+ private paint(frame: GradientFrame) {
235
+ if (this.painter) this.painter.paint(frame)
236
+ else this.pending = frame
237
+ }
238
+
239
+ private presented(): GradientFrame {
240
+ const running = this.running
241
+ if (!running) return this.model
242
+ const elapsed = getDriver().now() - running.startedAt
243
+ return mix(running.from, running.to, running.track.valueAt(elapsed), reducedMotion())
244
+ }
245
+
246
+ private cancelRunning() {
247
+ const running = this.running
248
+ if (!running) return
249
+ this.running = undefined
250
+ running.handle.cancel()
251
+ running.settle(false)
252
+ }
253
+
254
+ private assertActive() {
255
+ if (this.removed) throw new Error('This gradient was removed.')
256
+ }
257
+ }
258
+
259
+ function springOf(name: SpringName): Transition {
260
+ const spring = springs[name]
261
+ if (!spring || !Object.hasOwn(springs, name))
262
+ throw new Error(`Unknown spring "${name}".`)
263
+ return spring
264
+ }
265
+
266
+ /** Validates values and fills in what they leave out from `base`. */
267
+ function resolve(
268
+ base: GradientFrame,
269
+ values: GradientValues,
270
+ sameCount: boolean,
271
+ ): GradientFrame {
272
+ if (!values || typeof values !== 'object')
273
+ throw new TypeError('Gradient values must be an object.')
274
+ const colors = values.colors ? values.colors.map(parseColor) : base.colors
275
+ if (colors.length < 2 || colors.length > 8)
276
+ throw new RangeError('A gradient needs between two and eight colors.')
277
+ if (sameCount && values.colors && colors.length !== base.colors.length)
278
+ throw new RangeError(
279
+ `Animating a gradient keeps its ${base.colors.length} colors; use set() to change the count.`,
280
+ )
281
+ const stops = values.stops
282
+ ? [...values.stops]
283
+ : values.colors || base.stops.length !== colors.length
284
+ ? colors.map((_, i) => i / (colors.length - 1))
285
+ : base.stops
286
+ if (
287
+ stops.length !== colors.length ||
288
+ stops.some(
289
+ (stop, i) =>
290
+ !Number.isFinite(stop) || stop < 0 || stop > 1 || stop < (stops[i - 1] ?? 0),
291
+ )
292
+ )
293
+ throw new RangeError(
294
+ 'Gradient stops need one value per color, from 0 to 1, without going back.',
295
+ )
296
+ const angle = values.angle ?? base.angle
297
+ const x = values.center?.x ?? base.x
298
+ const y = values.center?.y ?? base.y
299
+ const radius = values.radius ?? base.radius
300
+ for (const [name, value] of [
301
+ ['angle', angle],
302
+ ['center', x],
303
+ ['center', y],
304
+ ['radius', radius],
305
+ ] as const)
306
+ if (!Number.isFinite(value))
307
+ throw new RangeError(`Invalid gradient ${name}: ${value}.`)
308
+ if (radius < 0)
309
+ throw new RangeError(`Gradient radius must not be negative, got ${radius}.`)
310
+ return { colors, stops, angle, x, y, radius }
311
+ }
312
+
313
+ /** One frame between two states. Geometry follows the curve, overshoot included; colors and stops stay between their ends. */
314
+ export function mix(
315
+ from: GradientFrame,
316
+ to: GradientFrame,
317
+ progress: number,
318
+ reduced = false,
319
+ ): GradientFrame {
320
+ const p = Math.min(Math.max(progress, 0), 1)
321
+ const g = reduced ? 1 : progress
322
+ const lerp = (a: number, b: number, t: number) => a + (b - a) * t
323
+ return {
324
+ colors: to.colors.map((color, i) => colorMixer(from.colors[i] ?? color, color)(p)),
325
+ stops: to.stops.map((stop, i) => lerp(from.stops[i] ?? stop, stop, p)),
326
+ angle: lerp(from.angle, to.angle, g),
327
+ x: lerp(from.x, to.x, g),
328
+ y: lerp(from.y, to.y, g),
329
+ radius: Math.max(0, lerp(from.radius, to.radius, g)),
330
+ }
331
+ }
332
+
333
+ /**
334
+ * The points a platform gradient needs, in unit coordinates of a `width` × `height` box.
335
+ * Linear follows CSS: the line runs through the center at `angle` and is long enough
336
+ * for the corners to get the first and last colors.
337
+ */
338
+ export function gradientPoints(
339
+ type: GradientType,
340
+ frame: GradientFrame,
341
+ width: number,
342
+ height: number,
343
+ ) {
344
+ const radians = (frame.angle * Math.PI) / 180
345
+ const sin = Math.sin(radians)
346
+ const cos = Math.cos(radians)
347
+ if (type === 'linear') {
348
+ const length = Math.abs(width * sin) + Math.abs(height * cos)
349
+ const dx = width > 0 ? (sin * length) / (2 * width) : 0
350
+ const dy = height > 0 ? (-cos * length) / (2 * height) : 0
351
+ return { start: { x: 0.5 - dx, y: 0.5 - dy }, end: { x: 0.5 + dx, y: 0.5 + dy } }
352
+ }
353
+ const center = { x: frame.x, y: frame.y }
354
+ const shorter = Math.min(width, height)
355
+ if (type === 'radial') {
356
+ const r = frame.radius * shorter
357
+ return {
358
+ start: center,
359
+ end: {
360
+ x: center.x + (width > 0 ? r / width : 0),
361
+ y: center.y + (height > 0 ? r / height : 0),
362
+ },
363
+ }
364
+ }
365
+ return {
366
+ start: center,
367
+ end: {
368
+ x: center.x + (width > 0 ? (sin * shorter) / (2 * width) : 0),
369
+ y: center.y + (height > 0 ? (-cos * shorter) / (2 * height) : 0),
370
+ },
371
+ }
372
+ }
package/src/haptics.ts ADDED
@@ -0,0 +1,148 @@
1
+ import { isIOS, type View } from '@nativescript/core'
2
+ import type { Driver } from './driver/index.js'
3
+ import type { Track } from './track.js'
4
+
5
+ export type HapticStyle = 'light' | 'medium' | 'heavy'
6
+
7
+ export interface HapticOptions {
8
+ style: HapticStyle
9
+ /** Points from 0 to 1 over the finite animation, excluding its initial delay. Defaults to arrival. */
10
+ times?: readonly number[]
11
+ }
12
+
13
+ export type Haptic = HapticStyle | HapticOptions | false
14
+
15
+ /** Validate before any running animation is interrupted. */
16
+ export function resolveHaptic(haptic: Haptic | undefined): HapticOptions | undefined {
17
+ if (haptic === undefined || haptic === false) return undefined
18
+ const options = typeof haptic === 'string' ? { style: haptic } : haptic
19
+ if (!options || !['light', 'medium', 'heavy'].includes(options.style))
20
+ throw new TypeError('Haptic style must be light, medium or heavy.')
21
+ const times = options.times
22
+ if (
23
+ times !== undefined &&
24
+ (!Array.isArray(times) ||
25
+ times.length === 0 ||
26
+ Array.from(times).some(
27
+ (time, i) =>
28
+ !Number.isFinite(time) ||
29
+ time <= 0 ||
30
+ time > 1 ||
31
+ (i > 0 && time <= times[i - 1]!),
32
+ ))
33
+ )
34
+ throw new RangeError(
35
+ 'Haptic times must be increasing points greater than 0 and at most 1.',
36
+ )
37
+ return { style: options.style, times: times && [...times] }
38
+ }
39
+
40
+ const PREPARE_AHEAD = 100
41
+ const MAX_LATENESS = 100
42
+ const COMPLETION_TOLERANCE = 20
43
+
44
+ /** Discrete feedback only: animation playback remains entirely native. */
45
+ export function playHaptics(
46
+ view: View,
47
+ tracks: readonly Track[],
48
+ options: HapticOptions | undefined,
49
+ driver: Driver,
50
+ startedAt: number,
51
+ elapsedTime = () => driver.now() - startedAt,
52
+ ) {
53
+ if (!isIOS || !options || typeof UIImpactFeedbackGenerator === 'undefined') return
54
+ const moving = tracks.filter(
55
+ (track) =>
56
+ track.duration > 0 && track.values.some((value) => value !== track.values[0]),
57
+ )
58
+ if (!moving.length) return
59
+ const end = Math.max(...tracks.map((track) => track.end))
60
+ if (!Number.isFinite(end)) return
61
+ const delay = Math.min(...moving.map((track) => track.delay))
62
+ const offsets = options.times
63
+ ? options.times.map((time) => delay + (end - delay) * time)
64
+ : [Math.max(...tracks.map((track) => track.arrival))]
65
+ const styles = {
66
+ light: 0,
67
+ medium: 1,
68
+ heavy: 2,
69
+ } satisfies Record<HapticStyle, UIImpactFeedbackStyle>
70
+ let generator: UIImpactFeedbackGenerator | undefined =
71
+ UIImpactFeedbackGenerator.alloc().initWithStyle(styles[options.style])
72
+ const native = view.nativeViewProtected as UIView
73
+ let paused = false
74
+ let next = 0
75
+ let cancelPrepare: (() => void) | undefined
76
+ let cancelImpact: (() => void) | undefined
77
+
78
+ const active = () =>
79
+ view.isLoaded &&
80
+ view.nativeViewProtected === native &&
81
+ !!native.window &&
82
+ UIApplication.sharedApplication.applicationState === 0 // UIApplicationState.Active
83
+
84
+ const cancelTimers = () => {
85
+ cancelPrepare?.()
86
+ cancelImpact?.()
87
+ cancelPrepare = cancelImpact = undefined
88
+ }
89
+ const cancel = () => {
90
+ cancelPrepare?.()
91
+ cancelImpact?.()
92
+ cancelPrepare = cancelImpact = undefined
93
+ generator = undefined
94
+ view.off('unloaded', cancel)
95
+ }
96
+
97
+ const fire = (tolerance = 0) => {
98
+ const elapsed = elapsedTime()
99
+ // A busy UI thread can delay timers. Drop stale impacts instead of replaying
100
+ // several old taps at once when it becomes responsive again.
101
+ while (next < offsets.length && offsets[next]! <= elapsed + tolerance) {
102
+ const offset = offsets[next++]!
103
+ if (active() && elapsed - offset <= MAX_LATENESS) generator?.impactOccurred()
104
+ }
105
+ }
106
+
107
+ const schedule = () => {
108
+ if (paused) return
109
+ if (!generator || next === offsets.length) return cancel()
110
+ const remaining = offsets[next]! - elapsedTime()
111
+ if (active()) generator.prepare()
112
+ if (remaining > PREPARE_AHEAD) {
113
+ cancelPrepare = driver.after(remaining - PREPARE_AHEAD, () => {
114
+ if (active()) generator?.prepare()
115
+ })
116
+ }
117
+ cancelImpact = driver.after(Math.max(0, remaining), () => {
118
+ fire()
119
+ schedule()
120
+ })
121
+ }
122
+
123
+ view.on('unloaded', cancel)
124
+ schedule()
125
+ return {
126
+ cancel,
127
+ pause() {
128
+ paused = true
129
+ cancelTimers()
130
+ },
131
+ play() {
132
+ if (paused) {
133
+ paused = false
134
+ schedule()
135
+ }
136
+ },
137
+ seek() {
138
+ cancelTimers()
139
+ while (next < offsets.length && offsets[next]! <= elapsedTime()) next++
140
+ schedule()
141
+ },
142
+ finish() {
143
+ // Native completion may beat the timer at the final keyframe by a frame.
144
+ fire(COMPLETION_TOLERANCE)
145
+ cancel()
146
+ },
147
+ }
148
+ }
@@ -0,0 +1,129 @@
1
+ import { Utils, isAndroid, isIOS, type Image, type ImageSource } from '@nativescript/core'
2
+
3
+ export interface ImageEffectSettings {
4
+ mode: number
5
+ x: number
6
+ y: number
7
+ amplitude: number
8
+ radius: number
9
+ duration: number
10
+ seed: number
11
+ to?: ImageSource
12
+ }
13
+
14
+ export interface ImageRenderer {
15
+ readonly backend: 'metal' | 'agsl'
16
+ readonly progress: number
17
+ readonly frames: number
18
+ readonly textureBytes: number
19
+ readonly longestFrameMs: number
20
+ play(endpoint: number): void
21
+ pause(): void
22
+ resume(): void
23
+ seek(progress: number): void
24
+ finish(endpoint: number): void
25
+ stop(preserveImage?: boolean): void
26
+ }
27
+
28
+ export function createImageRenderer(
29
+ image: Image,
30
+ settings: ImageEffectSettings,
31
+ ended: (completed: boolean, progress: number) => void,
32
+ ): ImageRenderer | undefined {
33
+ const { mode, x, y, amplitude, radius, duration, seed, to } = settings
34
+ if (isIOS && typeof JoltImageEffect !== 'undefined') {
35
+ const native =
36
+ JoltImageEffect.createWithViewDestinationModeOriginXOriginYAmplitudeRadiusDurationSeedListener(
37
+ image.nativeViewProtected,
38
+ to?.ios ?? null,
39
+ mode,
40
+ x,
41
+ y,
42
+ amplitude,
43
+ radius,
44
+ duration,
45
+ seed,
46
+ ended,
47
+ )
48
+ if (!native) return undefined
49
+ return {
50
+ backend: 'metal',
51
+ get progress() {
52
+ return native.progress
53
+ },
54
+ get frames() {
55
+ return native.frames
56
+ },
57
+ get textureBytes() {
58
+ return native.textureBytes
59
+ },
60
+ get longestFrameMs() {
61
+ return native.longestFrame * 1000
62
+ },
63
+ play: (p) => native.play(p),
64
+ pause: () => native.pause(),
65
+ resume: () => native.resume(),
66
+ seek: (p) => native.seek(p),
67
+ finish: (p) => native.finish(p),
68
+ stop: (preserveImage) =>
69
+ preserveImage ? native.stopPreservingImage() : native.stop(),
70
+ }
71
+ }
72
+ if (isAndroid && android.os.Build.VERSION.SDK_INT >= 33) {
73
+ const density = Utils.layout.getDisplayDensity()
74
+ const background = image.nativeViewProtected.getBackground()
75
+ // NativeScript draws bitmap pixels itself: ImageView.getImageMatrix() does
76
+ // not describe its stretch, EXIF rotation or inner border clipping.
77
+ const geometry = Array.create('float', 6)
78
+ geometry[0] = image.imageSource?.rotationAngle ?? 0
79
+ geometry[1] = to?.rotationAngle ?? 0
80
+ const corners = ['TopLeft', 'TopRight', 'BottomRight', 'BottomLeft']
81
+ corners.forEach((corner, i) => {
82
+ geometry[i + 2] = background?.[`getBorder${corner}Radius`]?.() ?? 0
83
+ })
84
+ const native = new org.rafaelaugusto.jolt.ImageEffect(
85
+ image.nativeViewProtected,
86
+ image.imageSource?.android ?? null,
87
+ to?.android ?? null,
88
+ geometry,
89
+ mode,
90
+ x * density,
91
+ y * density,
92
+ amplitude * density,
93
+ radius * density,
94
+ duration,
95
+ seed,
96
+ (image as Image & { touchListener?: android.view.View.OnTouchListener })
97
+ .touchListener ?? null,
98
+ new org.rafaelaugusto.jolt.ImageEffect.Listener({ onEnd: ended }),
99
+ )
100
+ if (!native.isSupported()) {
101
+ const reason = native.getFailureReason()
102
+ native.stop()
103
+ throw new Error(reason)
104
+ }
105
+ return {
106
+ backend: 'agsl',
107
+ get progress() {
108
+ return native.getProgress()
109
+ },
110
+ get frames() {
111
+ return native.getFrames()
112
+ },
113
+ get textureBytes() {
114
+ return native.getTextureBytes()
115
+ },
116
+ get longestFrameMs() {
117
+ return native.getLongestFrameNanos() / 1e6
118
+ },
119
+ play: (p) => native.play(p),
120
+ pause: () => native.pause(),
121
+ resume: () => native.resume(),
122
+ seek: (p) => native.seek(p),
123
+ finish: (p) => native.finish(p),
124
+ stop: (preserveImage) =>
125
+ preserveImage ? native.stopPreservingImage() : native.stop(),
126
+ }
127
+ }
128
+ return undefined
129
+ }