@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/liquid.ts ADDED
@@ -0,0 +1,287 @@
1
+ import { Application, Image, isIOS } from '@nativescript/core'
2
+ import { tintColorProperty } from '@nativescript/core/ui/image'
3
+ import { toView } from './animate.js'
4
+ import { liquidGrid } from './liquid-grid.js'
5
+ import type { Target } from './types.js'
6
+ import { reducedMotion } from './config.js'
7
+ import { ownImage } from './image-owner.js'
8
+
9
+ export interface LiquidOptions {
10
+ /** Amount of internal deformation, from 0 to 1. Defaults to 0.7. */
11
+ intensity?: number
12
+ /** Milliseconds per seamless cycle. Defaults to 8000. */
13
+ duration?: number
14
+ }
15
+
16
+ export interface LiquidEffect {
17
+ /** Freeze the current deformation. */
18
+ pause(): void
19
+ /** Continue from the paused deformation. */
20
+ resume(): void
21
+ /** Remove the effect and restore the image. Idempotent. */
22
+ stop(): void
23
+ }
24
+
25
+ const running = new WeakMap<Image, LiquidEffect>()
26
+ const key = 'jolt:liquid'
27
+ const columns = 24
28
+ const rows = 24
29
+ const steps = 64
30
+ const noop = () => {}
31
+
32
+ /** Deforms a raster image with native iOS texture warps. Android leaves it unchanged. */
33
+ export function liquid(target: Target, options: LiquidOptions = {}): LiquidEffect {
34
+ const settings = {
35
+ intensity: options.intensity ?? 0.7,
36
+ duration: options.duration ?? 8000,
37
+ }
38
+ if (
39
+ !Number.isFinite(settings.intensity) ||
40
+ settings.intensity < 0 ||
41
+ settings.intensity > 1
42
+ )
43
+ throw new RangeError('Liquid intensity must be between zero and one.')
44
+ if (!Number.isFinite(settings.duration) || settings.duration <= 0)
45
+ throw new RangeError('Liquid duration must be a positive finite number.')
46
+ const image = toView(target)
47
+ if (!(image instanceof Image)) throw new TypeError('Expected a NativeScript Image.')
48
+ running.get(image)?.stop()
49
+ if (!isIOS || settings.intensity === 0) return { pause: noop, resume: noop, stop: noop }
50
+ return new Liquid(image, settings)
51
+ }
52
+
53
+ class Liquid implements LiquidEffect {
54
+ private releaseOwnership: (() => void) | undefined
55
+ private stopped = false
56
+ private queued = false
57
+ private pausedByUser = false
58
+ private active = UIApplication.sharedApplication.applicationState === 0 // UIApplicationStateActive
59
+ private native: UIImageView | undefined
60
+ private original: UIImage | undefined
61
+ private source: Image['imageSource'] | undefined
62
+ private renderer: SKView | undefined
63
+ private scene: SKScene | undefined
64
+ private sprite: SKSpriteNode | undefined
65
+ private observers: [
66
+ string,
67
+ ReturnType<typeof Application.ios.addNotificationObserver>,
68
+ ][] = []
69
+
70
+ constructor(
71
+ private readonly image: Image,
72
+ private readonly settings: Required<LiquidOptions>,
73
+ ) {
74
+ this.releaseOwnership = ownImage(image, this.stop)
75
+ running.set(image, this)
76
+ for (const event of ['loaded', 'layoutChanged', 'imageSourceChange', 'stretchChange'])
77
+ image.on(event, this.schedule)
78
+ image.on('unloaded', this.unloaded)
79
+ image.style.on('visibilityChange', this.schedule)
80
+ image.style.on('tintColorChange', this.schedule)
81
+ this.observe(UIAccessibilityReduceMotionStatusDidChangeNotification, this.schedule)
82
+ this.observe(UIApplicationWillResignActiveNotification, this.suspend)
83
+ this.observe(UIApplicationDidBecomeActiveNotification, this.activate)
84
+ this.schedule()
85
+ }
86
+
87
+ pause = () => {
88
+ this.pausedByUser = true
89
+ if (this.renderer) this.renderer.paused = true
90
+ }
91
+ resume = () => {
92
+ if (this.stopped) return
93
+ this.pausedByUser = false
94
+ this.schedule()
95
+ }
96
+ stop = () => {
97
+ if (this.stopped) return
98
+ this.stopped = true
99
+ this.releaseOwnership?.()
100
+ for (const event of ['loaded', 'layoutChanged', 'imageSourceChange', 'stretchChange'])
101
+ this.image.off(event, this.schedule)
102
+ this.image.off('unloaded', this.unloaded)
103
+ this.image.style.off('visibilityChange', this.schedule)
104
+ this.image.style.off('tintColorChange', this.schedule)
105
+ for (const [name, observer] of this.observers)
106
+ Application.ios.removeNotificationObserver(observer, name)
107
+ this.observers = []
108
+ if (running.get(this.image) === this) running.delete(this.image)
109
+ this.remove()
110
+ }
111
+ private observe(name: string, callback: () => void) {
112
+ this.observers.push([name, Application.ios.addNotificationObserver(name, callback)])
113
+ }
114
+ private suspend = () => {
115
+ this.active = false
116
+ if (this.renderer) this.renderer.paused = true
117
+ }
118
+ private activate = () => {
119
+ this.active = true
120
+ this.schedule()
121
+ }
122
+ private unloaded = () => this.remove()
123
+ private schedule = () => {
124
+ if (this.stopped || this.queued) return
125
+ this.queued = true
126
+ void Promise.resolve().then(() => {
127
+ this.queued = false
128
+ if (this.stopped) return
129
+ try {
130
+ this.update()
131
+ } catch (error) {
132
+ this.stop()
133
+ throw error
134
+ }
135
+ })
136
+ }
137
+ private update() {
138
+ if (
139
+ !this.image.isLoaded ||
140
+ this.image.visibility !== 'visible' ||
141
+ this.image.tintColor ||
142
+ reducedMotion()
143
+ )
144
+ return this.remove()
145
+ const native = this.image.nativeViewProtected as UIImageView | undefined
146
+ if (!(native instanceof UIImageView) || !native.window) return this.remove()
147
+ if (
148
+ this.native &&
149
+ (this.native !== native ||
150
+ native.image !== null ||
151
+ this.image.imageSource !== this.source)
152
+ )
153
+ this.remove()
154
+ const source = this.original ?? native.image
155
+ if (
156
+ !source?.CGImage ||
157
+ source.images?.count ||
158
+ source.renderingMode === 2 /* AlwaysTemplate */
159
+ )
160
+ return this.remove()
161
+ if (source.size.width * source.size.height * source.scale ** 2 > 16_777_216)
162
+ return this.remove()
163
+ const bounds = native.bounds
164
+ if (
165
+ bounds.size.width <= 0 ||
166
+ bounds.size.height <= 0 ||
167
+ source.size.width <= 0 ||
168
+ source.size.height <= 0
169
+ )
170
+ return this.remove()
171
+ if (!this.renderer) {
172
+ // A paused SKView cannot draw its first frame. Keep the original visible
173
+ // until rendering can start, including source changes while paused.
174
+ if (this.pausedByUser || !this.active) return
175
+ this.create(native, source)
176
+ }
177
+ const renderer = this.renderer!
178
+ renderer.frame = CGRectMake(
179
+ bounds.origin.x,
180
+ bounds.origin.y,
181
+ bounds.size.width,
182
+ bounds.size.height,
183
+ )
184
+ this.scene!.size = CGSizeMake(bounds.size.width, bounds.size.height)
185
+ const iw = source.size.width
186
+ const ih = source.size.height
187
+ const { width, height } = bounds.size
188
+ const stretch = this.image.stretch
189
+ const scale =
190
+ stretch === 'aspectFill'
191
+ ? Math.max(width / iw, height / ih)
192
+ : Math.min(width / iw, height / ih)
193
+ this.sprite!.size = CGSizeMake(
194
+ stretch === 'fill' ? width : stretch === 'none' ? iw : iw * scale,
195
+ stretch === 'fill' ? height : stretch === 'none' ? ih : ih * scale,
196
+ )
197
+ this.sprite!.position = CGPointMake(width / 2, height / 2)
198
+ renderer.paused = this.pausedByUser || !this.active
199
+ }
200
+ private create(native: UIImageView, source: UIImage) {
201
+ const renderer = SKView.alloc().initWithFrame(native.bounds)
202
+ this.renderer = renderer
203
+ this.native = native
204
+ this.original = source
205
+ this.source = this.image.imageSource
206
+ renderer.opaque = false
207
+ renderer.backgroundColor = UIColor.clearColor
208
+ renderer.allowsTransparency = true
209
+ renderer.asynchronous = true
210
+ renderer.userInteractionEnabled = false
211
+ renderer.accessibilityElementsHidden = true
212
+ renderer.clipsToBounds = true
213
+ renderer.preferredFramesPerSecond = Math.min(
214
+ 120,
215
+ native.window?.screen?.maximumFramesPerSecond ?? 60,
216
+ )
217
+ renderer.paused = true
218
+ const scene = SKScene.sceneWithSize(native.bounds.size)
219
+ this.scene = scene
220
+ scene.backgroundColor = UIColor.clearColor
221
+ scene.scaleMode = 3 // SKSceneScaleModeResizeFill
222
+ const texture = SKTexture.textureWithImage(source)
223
+ const sprite = SKSpriteNode.spriteNodeWithTexture(texture)
224
+ this.sprite = sprite
225
+ const warps: SKWarpGeometryGrid[] = []
226
+ const times: number[] = []
227
+ for (let step = 0; step <= steps; step++) {
228
+ const positions = liquidGrid(
229
+ columns,
230
+ rows,
231
+ step === steps ? 0 : step / steps,
232
+ this.settings.intensity,
233
+ )
234
+ warps.push(
235
+ SKWarpGeometryGrid.gridWithColumnsRowsSourcePositionsDestPositions(
236
+ columns,
237
+ rows,
238
+ null,
239
+ positions,
240
+ ),
241
+ )
242
+ times.push(((step / steps) * this.settings.duration) / 1000)
243
+ }
244
+ const loop = SKAction.animateWithWarpsTimes(warps, times)
245
+ if (!loop) throw new Error('Unable to create the liquid warp action.')
246
+ sprite.warpGeometry = SKWarpGeometryGrid.gridWithColumnsRows(columns, rows)
247
+ const entrance = SKAction.warpToDuration(
248
+ warps[0]!,
249
+ Math.min(0.35, this.settings.duration / 10000),
250
+ )
251
+ if (!entrance) throw new Error('Unable to create the liquid entrance.')
252
+ sprite.runActionWithKey(
253
+ SKAction.sequence([entrance, SKAction.repeatActionForever(loop)]),
254
+ key,
255
+ )
256
+ scene.addChild(sprite)
257
+ renderer.presentScene(scene)
258
+ native.addSubview(renderer)
259
+ native.image = null
260
+ }
261
+ private remove() {
262
+ const { renderer, sprite, native, original } = this
263
+ if (renderer) {
264
+ renderer.paused = true
265
+ sprite?.removeAllActions()
266
+ sprite?.removeFromParent()
267
+ renderer.presentScene(null)
268
+ renderer.removeFromSuperview()
269
+ }
270
+ if (
271
+ native &&
272
+ native.image === null &&
273
+ original &&
274
+ this.image.imageSource === this.source
275
+ ) {
276
+ native.image = original
277
+ // Core couldn't turn the suppressed UIImage into a template while it was
278
+ // hidden. Reapply its public native setter when a tint takes over.
279
+ if (this.image.tintColor)
280
+ (this.image as unknown as Record<symbol, (value: unknown) => void>)[
281
+ tintColorProperty.setNative
282
+ ]!(this.image.tintColor)
283
+ }
284
+ this.renderer = this.scene = this.sprite = this.native = this.original = undefined
285
+ this.source = undefined
286
+ }
287
+ }
package/src/loader.ts ADDED
@@ -0,0 +1,202 @@
1
+ import { Application, Color, isIOS, type View } from '@nativescript/core'
2
+ import { toView } from './animate.js'
3
+ import type { Target } from './types.js'
4
+ import { reducedMotion } from './config.js'
5
+
6
+ export type LoaderStyle = 'dots' | 'bars' | 'ring'
7
+
8
+ export interface LoaderOptions {
9
+ /** Defaults to 'dots'. */
10
+ style?: LoaderStyle
11
+ /** A NativeScript color string. Defaults to '#71717a'. */
12
+ color?: string
13
+ /** Maximum width and height in DIPs, fitted inside the target. Defaults to 48. */
14
+ size?: number
15
+ /** Milliseconds per cycle. Defaults to 1200. */
16
+ duration?: number
17
+ }
18
+
19
+ const running = new WeakMap<View, () => void>()
20
+ const key = 'jolt:loader'
21
+
22
+ /**
23
+ * Adds a centered native iOS loading indicator. Returns an idempotent cleanup.
24
+ * Unloading releases native resources; loading resumes until explicitly stopped.
25
+ * Reduce Motion keeps the indicator static. Android is a no-op.
26
+ */
27
+ export function loader(target: Target, options: LoaderOptions = {}): () => void {
28
+ const settings: Required<LoaderOptions> = {
29
+ style: options.style ?? 'dots',
30
+ color: options.color ?? '#71717a',
31
+ size: options.size ?? 48,
32
+ duration: options.duration ?? 1200,
33
+ }
34
+ if (!['dots', 'bars', 'ring'].includes(settings.style))
35
+ throw new TypeError('Unknown loader style.')
36
+ if (typeof settings.color !== 'string' || !Color.isValid(settings.color))
37
+ throw new TypeError('Loader color must be a valid color string.')
38
+ for (const name of ['size', 'duration'] as const)
39
+ if (!Number.isFinite(settings[name]) || settings[name] <= 0)
40
+ throw new RangeError(`Loader ${name} must be a positive finite number.`)
41
+ const view = toView(target)
42
+ running.get(view)?.()
43
+ if (!isIOS) return () => {}
44
+ return new Loader(view, settings).stop
45
+ }
46
+
47
+ class Loader {
48
+ private stopped = false
49
+ private queued = false
50
+ private waitingForLoad: boolean
51
+ private native: UIView | undefined
52
+ private layer: CAReplicatorLayer | undefined
53
+ private item: CALayer | undefined
54
+ private animating = false
55
+ private observer: ReturnType<typeof Application.ios.addNotificationObserver> | undefined
56
+
57
+ constructor(
58
+ private readonly view: View,
59
+ private readonly settings: Required<LoaderOptions>,
60
+ ) {
61
+ this.waitingForLoad = !view.isLoaded
62
+ running.set(view, this.stop)
63
+ view.on('loaded', this.loaded)
64
+ view.on('layoutChanged', this.schedule)
65
+ view.on('unloaded', this.unloaded)
66
+ this.schedule()
67
+ }
68
+
69
+ stop = () => {
70
+ if (this.stopped) return
71
+ this.stopped = true
72
+ this.view.off('loaded', this.loaded)
73
+ this.view.off('layoutChanged', this.schedule)
74
+ this.view.off('unloaded', this.unloaded)
75
+ if (running.get(this.view) === this.stop) running.delete(this.view)
76
+ this.unloaded()
77
+ }
78
+
79
+ private loaded = () => {
80
+ this.waitingForLoad = false
81
+ this.schedule()
82
+ }
83
+
84
+ private unloaded = () => {
85
+ this.waitingForLoad = true
86
+ if (this.observer) {
87
+ Application.ios.removeNotificationObserver(
88
+ this.observer,
89
+ UIAccessibilityReduceMotionStatusDidChangeNotification,
90
+ )
91
+ this.observer = undefined
92
+ }
93
+ this.remove()
94
+ }
95
+
96
+ private schedule = () => {
97
+ if (this.stopped || this.queued) return
98
+ this.queued = true
99
+ void Promise.resolve().then(() => {
100
+ this.queued = false
101
+ if (this.stopped) return
102
+ try {
103
+ this.update()
104
+ } catch (error) {
105
+ this.stop()
106
+ throw error
107
+ }
108
+ })
109
+ }
110
+
111
+ private update() {
112
+ if (this.waitingForLoad || !this.view.isLoaded) return
113
+ const native = this.view.nativeViewProtected as UIView | undefined
114
+ if (!native?.window) return this.remove()
115
+ const bounds = native.layer.bounds
116
+ const size = Math.min(this.settings.size, bounds.size.width, bounds.size.height)
117
+ if (size <= 0) return this.remove()
118
+ if (this.native && this.native !== native) this.remove()
119
+ this.observer ??= Application.ios.addNotificationObserver(
120
+ UIAccessibilityReduceMotionStatusDidChangeNotification,
121
+ this.schedule,
122
+ )
123
+
124
+ CATransaction.begin()
125
+ CATransaction.setDisableActions(true)
126
+ try {
127
+ if (!this.layer) {
128
+ this.native = native
129
+ this.layer = CAReplicatorLayer.new()
130
+ this.layer.name = key
131
+ this.item = CALayer.new()
132
+ this.item.backgroundColor = new Color(this.settings.color).ios.CGColor
133
+ this.layer.addSublayer(this.item)
134
+ native.layer.addSublayer(this.layer)
135
+ }
136
+ const layer = this.layer
137
+ const item = this.item!
138
+ const { style, duration } = this.settings
139
+ layer.bounds = CGRectMake(0, 0, size, size)
140
+ layer.position = CGPointMake(
141
+ bounds.origin.x + bounds.size.width / 2,
142
+ bounds.origin.y + bounds.size.height / 2,
143
+ )
144
+ layer.instanceCount = style === 'dots' ? 3 : style === 'bars' ? 5 : 12
145
+ layer.instanceDelay = duration / 1000 / (style === 'ring' ? 12 : 5)
146
+ if (style === 'ring') {
147
+ const diameter = size * 0.16
148
+ item.bounds = CGRectMake(0, 0, diameter, diameter)
149
+ item.position = CGPointMake(size / 2, diameter / 2)
150
+ item.cornerRadius = diameter / 2
151
+ layer.instanceTransform = CATransform3DMakeRotation((2 * Math.PI) / 12, 0, 0, 1)
152
+ } else {
153
+ const width = size * (style === 'dots' ? 0.2 : 0.12)
154
+ const height = style === 'dots' ? width : size * 0.75
155
+ item.bounds = CGRectMake(0, 0, width, height)
156
+ item.position = CGPointMake(width / 2, size / 2)
157
+ item.cornerRadius = width / 2
158
+ layer.instanceTransform = CATransform3DMakeTranslation(
159
+ (size - width) / (layer.instanceCount - 1),
160
+ 0,
161
+ 0,
162
+ )
163
+ }
164
+ const reduced = reducedMotion()
165
+ item.opacity = style === 'ring' ? 0.35 : 1
166
+ if (reduced && this.animating) {
167
+ item.removeAnimationForKey(key)
168
+ this.animating = false
169
+ } else if (!reduced && !this.animating) {
170
+ const animation = CAKeyframeAnimation.animationWithKeyPath(
171
+ style === 'ring'
172
+ ? 'opacity'
173
+ : style === 'bars'
174
+ ? 'transform.scale.y'
175
+ : 'transform.scale',
176
+ )
177
+ animation.values = NSArray.arrayWithArray(
178
+ style === 'ring' ? [1, 0.2] : [0.35, 1, 0.35, 0.35],
179
+ )
180
+ animation.keyTimes = NSArray.arrayWithArray(
181
+ style === 'ring' ? [0, 1] : [0, 0.3, 0.6, 1],
182
+ )
183
+ animation.duration = duration / 1000
184
+ animation.repeatCount = Infinity
185
+ // Start one cycle in so every delayed copy is already participating.
186
+ animation.beginTime =
187
+ item.convertTimeFromLayer(CACurrentMediaTime(), null) - animation.duration
188
+ item.addAnimationForKey(animation, key)
189
+ this.animating = true
190
+ }
191
+ } finally {
192
+ CATransaction.commit()
193
+ }
194
+ }
195
+
196
+ private remove() {
197
+ this.item?.removeAnimationForKey(key)
198
+ this.layer?.removeFromSuperlayer()
199
+ this.item = this.layer = this.native = undefined
200
+ this.animating = false
201
+ }
202
+ }