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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/README.md +367 -18
  2. package/dist/animate.d.ts +19 -3
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +186 -14
  5. package/dist/animate.js.map +1 -1
  6. package/dist/color.d.ts +11 -0
  7. package/dist/color.d.ts.map +1 -0
  8. package/dist/color.js +73 -0
  9. package/dist/color.js.map +1 -0
  10. package/dist/config.d.ts +31 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +73 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/decay.d.ts +11 -0
  15. package/dist/decay.d.ts.map +1 -0
  16. package/dist/decay.js +22 -0
  17. package/dist/decay.js.map +1 -0
  18. package/dist/decoration.d.ts +18 -0
  19. package/dist/decoration.d.ts.map +1 -0
  20. package/dist/decoration.js +155 -0
  21. package/dist/decoration.js.map +1 -0
  22. package/dist/drag-input.d.ts +15 -0
  23. package/dist/drag-input.d.ts.map +1 -0
  24. package/dist/drag-input.js +66 -0
  25. package/dist/drag-input.js.map +1 -0
  26. package/dist/drag-physics.d.ts +20 -0
  27. package/dist/drag-physics.d.ts.map +1 -0
  28. package/dist/drag-physics.js +75 -0
  29. package/dist/drag-physics.js.map +1 -0
  30. package/dist/drag.d.ts +58 -0
  31. package/dist/drag.d.ts.map +1 -0
  32. package/dist/drag.js +324 -0
  33. package/dist/drag.js.map +1 -0
  34. package/dist/driver/android-color.d.ts +20 -0
  35. package/dist/driver/android-color.d.ts.map +1 -0
  36. package/dist/driver/android-color.js +136 -0
  37. package/dist/driver/android-color.js.map +1 -0
  38. package/dist/driver/android-decoration.d.ts +22 -0
  39. package/dist/driver/android-decoration.d.ts.map +1 -0
  40. package/dist/driver/android-decoration.js +139 -0
  41. package/dist/driver/android-decoration.js.map +1 -0
  42. package/dist/driver/android-playback.d.ts +13 -0
  43. package/dist/driver/android-playback.d.ts.map +1 -0
  44. package/dist/driver/android-playback.js +141 -0
  45. package/dist/driver/android-playback.js.map +1 -0
  46. package/dist/driver/android.d.ts +1 -1
  47. package/dist/driver/android.d.ts.map +1 -1
  48. package/dist/driver/android.js +165 -13
  49. package/dist/driver/android.js.map +1 -1
  50. package/dist/driver/index.d.ts +25 -2
  51. package/dist/driver/index.d.ts.map +1 -1
  52. package/dist/driver/index.js.map +1 -1
  53. package/dist/driver/ios-color.d.ts +16 -0
  54. package/dist/driver/ios-color.d.ts.map +1 -0
  55. package/dist/driver/ios-color.js +392 -0
  56. package/dist/driver/ios-color.js.map +1 -0
  57. package/dist/driver/ios-decoration.d.ts +17 -0
  58. package/dist/driver/ios-decoration.d.ts.map +1 -0
  59. package/dist/driver/ios-decoration.js +271 -0
  60. package/dist/driver/ios-decoration.js.map +1 -0
  61. package/dist/driver/ios-layout.d.ts +9 -0
  62. package/dist/driver/ios-layout.d.ts.map +1 -0
  63. package/dist/driver/ios-layout.js +52 -0
  64. package/dist/driver/ios-layout.js.map +1 -0
  65. package/dist/driver/ios-playback.d.ts +10 -0
  66. package/dist/driver/ios-playback.d.ts.map +1 -0
  67. package/dist/driver/ios-playback.js +47 -0
  68. package/dist/driver/ios-playback.js.map +1 -0
  69. package/dist/driver/ios-shadow.d.ts +6 -0
  70. package/dist/driver/ios-shadow.d.ts.map +1 -0
  71. package/dist/driver/ios-shadow.js +52 -0
  72. package/dist/driver/ios-shadow.js.map +1 -0
  73. package/dist/driver/ios.d.ts.map +1 -1
  74. package/dist/driver/ios.js +163 -28
  75. package/dist/driver/ios.js.map +1 -1
  76. package/dist/engine.d.ts +75 -5
  77. package/dist/engine.d.ts.map +1 -1
  78. package/dist/engine.js +293 -23
  79. package/dist/engine.js.map +1 -1
  80. package/dist/haptics.d.ts +21 -0
  81. package/dist/haptics.d.ts.map +1 -0
  82. package/dist/haptics.js +120 -0
  83. package/dist/haptics.js.map +1 -0
  84. package/dist/image-effect-native.d.ts +26 -0
  85. package/dist/image-effect-native.d.ts.map +1 -0
  86. package/dist/image-effect-native.js +73 -0
  87. package/dist/image-effect-native.js.map +1 -0
  88. package/dist/image-effect.d.ts +50 -0
  89. package/dist/image-effect.d.ts.map +1 -0
  90. package/dist/image-effect.js +290 -0
  91. package/dist/image-effect.js.map +1 -0
  92. package/dist/image-owner.d.ts +3 -0
  93. package/dist/image-owner.d.ts.map +1 -0
  94. package/dist/image-owner.js +11 -0
  95. package/dist/image-owner.js.map +1 -0
  96. package/dist/index.d.ts +43 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +20 -1
  99. package/dist/index.js.map +1 -1
  100. package/dist/interaction-surface.d.ts +12 -0
  101. package/dist/interaction-surface.d.ts.map +1 -0
  102. package/dist/interaction-surface.js +36 -0
  103. package/dist/interaction-surface.js.map +1 -0
  104. package/dist/items.d.ts +20 -0
  105. package/dist/items.d.ts.map +1 -0
  106. package/dist/items.js +109 -0
  107. package/dist/items.js.map +1 -0
  108. package/dist/keyboard.d.ts +41 -0
  109. package/dist/keyboard.d.ts.map +1 -0
  110. package/dist/keyboard.js +256 -0
  111. package/dist/keyboard.js.map +1 -0
  112. package/dist/label.d.ts +7 -0
  113. package/dist/label.d.ts.map +1 -0
  114. package/dist/label.js +60 -0
  115. package/dist/label.js.map +1 -0
  116. package/dist/layout.d.ts +13 -0
  117. package/dist/layout.d.ts.map +1 -0
  118. package/dist/layout.js +348 -0
  119. package/dist/layout.js.map +1 -0
  120. package/dist/liquid-grid.d.ts +3 -0
  121. package/dist/liquid-grid.d.ts.map +1 -0
  122. package/dist/liquid-grid.js +24 -0
  123. package/dist/liquid-grid.js.map +1 -0
  124. package/dist/liquid.d.ts +18 -0
  125. package/dist/liquid.d.ts.map +1 -0
  126. package/dist/liquid.js +234 -0
  127. package/dist/liquid.js.map +1 -0
  128. package/dist/loader.d.ts +19 -0
  129. package/dist/loader.d.ts.map +1 -0
  130. package/dist/loader.js +173 -0
  131. package/dist/loader.js.map +1 -0
  132. package/dist/motion-scope.d.ts +39 -0
  133. package/dist/motion-scope.d.ts.map +1 -0
  134. package/dist/motion-scope.js +238 -0
  135. package/dist/motion-scope.js.map +1 -0
  136. package/dist/motion-states.d.ts +36 -0
  137. package/dist/motion-states.d.ts.map +1 -0
  138. package/dist/motion-states.js +165 -0
  139. package/dist/motion-states.js.map +1 -0
  140. package/dist/particles.d.ts +39 -0
  141. package/dist/particles.d.ts.map +1 -0
  142. package/dist/particles.js +265 -0
  143. package/dist/particles.js.map +1 -0
  144. package/dist/path.d.ts +12 -0
  145. package/dist/path.d.ts.map +1 -0
  146. package/dist/path.js +90 -0
  147. package/dist/path.js.map +1 -0
  148. package/dist/playback-group.d.ts +6 -0
  149. package/dist/playback-group.d.ts.map +1 -0
  150. package/dist/playback-group.js +141 -0
  151. package/dist/playback-group.js.map +1 -0
  152. package/dist/playback.d.ts +25 -0
  153. package/dist/playback.d.ts.map +1 -0
  154. package/dist/playback.js +35 -0
  155. package/dist/playback.js.map +1 -0
  156. package/dist/press.d.ts.map +1 -1
  157. package/dist/press.js +23 -7
  158. package/dist/press.js.map +1 -1
  159. package/dist/properties.d.ts +6 -3
  160. package/dist/properties.d.ts.map +1 -1
  161. package/dist/properties.js +26 -2
  162. package/dist/properties.js.map +1 -1
  163. package/dist/reveal.d.ts +25 -0
  164. package/dist/reveal.d.ts.map +1 -0
  165. package/dist/reveal.js +294 -0
  166. package/dist/reveal.js.map +1 -0
  167. package/dist/roll-diff.d.ts +22 -0
  168. package/dist/roll-diff.d.ts.map +1 -0
  169. package/dist/roll-diff.js +36 -0
  170. package/dist/roll-diff.js.map +1 -0
  171. package/dist/roll.d.ts +29 -0
  172. package/dist/roll.d.ts.map +1 -0
  173. package/dist/roll.js +442 -0
  174. package/dist/roll.js.map +1 -0
  175. package/dist/scroll-linked.d.ts +10 -0
  176. package/dist/scroll-linked.d.ts.map +1 -0
  177. package/dist/scroll-linked.js +58 -0
  178. package/dist/scroll-linked.js.map +1 -0
  179. package/dist/sequence.d.ts +10 -5
  180. package/dist/sequence.d.ts.map +1 -1
  181. package/dist/sequence.js +208 -14
  182. package/dist/sequence.js.map +1 -1
  183. package/dist/shimmer.d.ts +12 -0
  184. package/dist/shimmer.d.ts.map +1 -0
  185. package/dist/shimmer.js +161 -0
  186. package/dist/shimmer.js.map +1 -0
  187. package/dist/svg-path.d.ts +14 -0
  188. package/dist/svg-path.d.ts.map +1 -0
  189. package/dist/svg-path.js +281 -0
  190. package/dist/svg-path.js.map +1 -0
  191. package/dist/symbol-effect.d.ts +23 -0
  192. package/dist/symbol-effect.d.ts.map +1 -0
  193. package/dist/symbol-effect.js +187 -0
  194. package/dist/symbol-effect.js.map +1 -0
  195. package/dist/text-layout.d.ts +30 -0
  196. package/dist/text-layout.d.ts.map +1 -0
  197. package/dist/text-layout.js +185 -0
  198. package/dist/text-layout.js.map +1 -0
  199. package/dist/text.d.ts +17 -0
  200. package/dist/text.d.ts.map +1 -0
  201. package/dist/text.js +263 -0
  202. package/dist/text.js.map +1 -0
  203. package/dist/track.d.ts +3 -1
  204. package/dist/track.d.ts.map +1 -1
  205. package/dist/track.js +266 -13
  206. package/dist/track.js.map +1 -1
  207. package/dist/transition.d.ts +17 -1
  208. package/dist/transition.d.ts.map +1 -1
  209. package/dist/transition.js +41 -6
  210. package/dist/transition.js.map +1 -1
  211. package/dist/types.d.ts +48 -1
  212. package/dist/types.d.ts.map +1 -1
  213. package/dist/vue/index.d.ts +95 -0
  214. package/dist/vue/index.d.ts.map +1 -0
  215. package/dist/vue/index.js +163 -0
  216. package/dist/vue/index.js.map +1 -0
  217. package/dist/zoom-native.d.ts +30 -0
  218. package/dist/zoom-native.d.ts.map +1 -0
  219. package/dist/zoom-native.js +56 -0
  220. package/dist/zoom-native.js.map +1 -0
  221. package/dist/zoomable.d.ts +48 -0
  222. package/dist/zoomable.d.ts.map +1 -0
  223. package/dist/zoomable.js +249 -0
  224. package/dist/zoomable.js.map +1 -0
  225. package/package.json +29 -5
  226. package/platforms/android/include.gradle +7 -0
  227. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  228. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  229. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  230. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  231. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
  232. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  233. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  234. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  235. package/platforms/android/proguard-rules.pro +19 -0
  236. package/platforms/ios/build.xcconfig +1 -0
  237. package/platforms/ios/src/JoltImageEffect.h +25 -0
  238. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  239. package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
  240. package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
  241. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  242. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  243. package/platforms/ios/src/module.modulemap +6 -0
  244. package/src/android-plugin.d.ts +155 -0
  245. package/src/animate.ts +249 -36
  246. package/src/color.ts +101 -0
  247. package/src/config.ts +84 -0
  248. package/src/decay.ts +45 -0
  249. package/src/decoration.ts +180 -0
  250. package/src/drag-input.ts +74 -0
  251. package/src/drag-physics.ts +110 -0
  252. package/src/drag.ts +395 -0
  253. package/src/driver/android-color.ts +170 -0
  254. package/src/driver/android-decoration.ts +182 -0
  255. package/src/driver/android-playback.ts +157 -0
  256. package/src/driver/android.ts +208 -20
  257. package/src/driver/index.ts +37 -2
  258. package/src/driver/ios-color.ts +441 -0
  259. package/src/driver/ios-decoration.ts +332 -0
  260. package/src/driver/ios-layout.ts +63 -0
  261. package/src/driver/ios-playback.ts +51 -0
  262. package/src/driver/ios-shadow.ts +50 -0
  263. package/src/driver/ios.ts +193 -33
  264. package/src/engine.ts +321 -27
  265. package/src/haptics.ts +148 -0
  266. package/src/image-effect-native.ts +129 -0
  267. package/src/image-effect.ts +336 -0
  268. package/src/image-owner.ts +12 -0
  269. package/src/index.ts +59 -2
  270. package/src/interaction-surface.ts +41 -0
  271. package/src/ios-plugin.d.ts +81 -0
  272. package/src/items.ts +140 -0
  273. package/src/keyboard.ts +291 -0
  274. package/src/label.ts +76 -0
  275. package/src/layout.ts +408 -0
  276. package/src/liquid-grid.ts +28 -0
  277. package/src/liquid.ts +287 -0
  278. package/src/loader.ts +202 -0
  279. package/src/motion-scope.ts +279 -0
  280. package/src/motion-states.ts +215 -0
  281. package/src/particles.ts +323 -0
  282. package/src/path.ts +124 -0
  283. package/src/playback-group.ts +143 -0
  284. package/src/playback.ts +62 -0
  285. package/src/press.ts +26 -10
  286. package/src/properties.ts +32 -6
  287. package/src/reveal.ts +374 -0
  288. package/src/roll-diff.ts +57 -0
  289. package/src/roll.ts +515 -0
  290. package/src/scroll-linked.ts +73 -0
  291. package/src/sequence.ts +204 -14
  292. package/src/shimmer.ts +180 -0
  293. package/src/svg-path.ts +355 -0
  294. package/src/symbol-effect.ts +226 -0
  295. package/src/text-layout.ts +228 -0
  296. package/src/text.ts +279 -0
  297. package/src/track.ts +347 -12
  298. package/src/transition.ts +63 -6
  299. package/src/types.ts +49 -1
  300. package/src/vue/index.ts +231 -0
  301. package/src/zoom-native.ts +124 -0
  302. package/src/zoomable.ts +330 -0
@@ -0,0 +1,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
+ }
@@ -0,0 +1,336 @@
1
+ import {
2
+ Application,
3
+ Image,
4
+ ImageSource,
5
+ isIOS,
6
+ type EventData,
7
+ } from '@nativescript/core'
8
+ import { toView } from './animate.js'
9
+ import { reducedMotion, timeScale } from './config.js'
10
+ import type { Animation } from './engine.js'
11
+ import { ownImage } from './image-owner.js'
12
+ import { createImageRenderer, type ImageRenderer } from './image-effect-native.js'
13
+ import type { Target } from './types.js'
14
+
15
+ export type ImageEffectName = 'ripple' | 'distortion' | 'dissolve'
16
+ export interface ImageEffectOptions {
17
+ /** Origin in target-local DIPs; defaults to the center. */
18
+ origin?: { x: number; y: number }
19
+ /** Maximum displacement in DIPs, 0..64. Defaults to 8. */
20
+ amplitude?: number
21
+ /** Influence radius in DIPs, greater than zero. Defaults to 96. */
22
+ radius?: number
23
+ /** Milliseconds for a finite effect (or drag release). Defaults to 650. */
24
+ duration?: number
25
+ /** Deterministic dissolve noise seed, 0..65535. Defaults to 1. */
26
+ seed?: number
27
+ /** Loaded destination raster for dissolve. */
28
+ to?: ImageSource
29
+ /** Prepare at progress zero without starting. */
30
+ paused?: boolean
31
+ }
32
+ export interface ImageEffectDiagnostics {
33
+ backend: 'waiting' | 'metal' | 'agsl' | 'fallback'
34
+ reason?: 'reduced-motion' | 'unsupported' | 'not-visible'
35
+ /** Native setup error, when available. Intended for developer diagnostics. */
36
+ detail?: string
37
+ frames: number
38
+ textureBytes: number
39
+ peakTextureBytes: number
40
+ preparationMs: number
41
+ longestFrameMs: number
42
+ }
43
+ export interface ImageEffect extends Animation {
44
+ readonly effect: ImageEffectName
45
+ readonly paused: boolean
46
+ /** Finite progress; distortion rejects seeking because it follows native touch input. */
47
+ progress: number
48
+ readonly diagnostics: Readonly<ImageEffectDiagnostics>
49
+ pause(): void
50
+ play(): void
51
+ /** Reverse a finite effect from its current pixels. Terminal handles remain terminal. */
52
+ reverse(): void
53
+ /** Complete immediately at the current direction's endpoint. */
54
+ finish(): void
55
+ }
56
+
57
+ const names: readonly ImageEffectName[] = ['ripple', 'distortion', 'dissolve']
58
+ const surfaceChanges = [
59
+ 'tintColorChange',
60
+ ...['TopLeft', 'TopRight', 'BottomRight', 'BottomLeft'].map(
61
+ (corner) => `border${corner}RadiusChange`,
62
+ ),
63
+ ...['Top', 'Right', 'Bottom', 'Left'].flatMap((edge) => [
64
+ `padding${edge}Change`,
65
+ `border${edge}WidthChange`,
66
+ ]),
67
+ ]
68
+
69
+ /** Optional native image effects. Stop restores the source; dissolve completion commits its destination. */
70
+ export function imageEffect(
71
+ target: Target,
72
+ effect: ImageEffectName,
73
+ options: ImageEffectOptions = {},
74
+ ): ImageEffect {
75
+ const image = toView(target)
76
+ if (!(image instanceof Image))
77
+ throw new TypeError('imageEffect() expects a NativeScript Image.')
78
+ if (!names.includes(effect))
79
+ throw new TypeError(`Unknown image effect: ${String(effect)}.`)
80
+ if (!options || typeof options !== 'object')
81
+ throw new TypeError('Image effect options must be an object.')
82
+ const amplitude = options.amplitude ?? 8,
83
+ radius = options.radius ?? 96
84
+ const duration = options.duration ?? 650,
85
+ seed = options.seed ?? 1
86
+ if (!Number.isFinite(amplitude) || amplitude < 0 || amplitude > 64)
87
+ throw new RangeError('Image displacement must be between 0 and 64 DIPs.')
88
+ if (
89
+ !Number.isFinite(radius) ||
90
+ radius <= 0 ||
91
+ !Number.isFinite(duration) ||
92
+ duration <= 0
93
+ )
94
+ throw new RangeError('Image radius and duration must be positive finite numbers.')
95
+ if (!Number.isInteger(seed) || seed < 0 || seed > 65535)
96
+ throw new RangeError('Image seed must be an integer between 0 and 65535.')
97
+ if (
98
+ options.origin &&
99
+ (!Number.isFinite(options.origin.x) || !Number.isFinite(options.origin.y))
100
+ )
101
+ throw new RangeError('Image origin must contain finite local coordinates.')
102
+ if (options.paused !== undefined && typeof options.paused !== 'boolean')
103
+ throw new TypeError('paused must be a boolean.')
104
+ if (
105
+ effect === 'dissolve' &&
106
+ (!(options.to instanceof ImageSource) ||
107
+ !Number.isFinite(options.to.width) ||
108
+ !Number.isFinite(options.to.height) ||
109
+ options.to.width <= 0 ||
110
+ options.to.height <= 0)
111
+ )
112
+ throw new TypeError('Dissolve requires a loaded destination ImageSource.')
113
+ return new Effect(image, effect, {
114
+ ...options,
115
+ amplitude,
116
+ radius,
117
+ duration: duration / timeScale(),
118
+ seed,
119
+ })
120
+ }
121
+
122
+ class Effect implements ImageEffect {
123
+ readonly finished: Promise<boolean>
124
+ private resolve!: (completed: boolean) => void
125
+ private renderer: ImageRenderer | undefined
126
+ private releaseOwnership: () => void = () => {}
127
+ private terminal = false
128
+ private queued = false
129
+ private committing = false
130
+ private userPaused: boolean
131
+ private suspended = false
132
+ private position = 0
133
+ private direction: 0 | 1 = 1
134
+ private dimensions = ''
135
+ private stats: ImageEffectDiagnostics = {
136
+ backend: 'waiting',
137
+ frames: 0,
138
+ textureBytes: 0,
139
+ peakTextureBytes: 0,
140
+ preparationMs: 0,
141
+ longestFrameMs: 0,
142
+ }
143
+ private reduceObserver:
144
+ ReturnType<typeof Application.ios.addNotificationObserver> | undefined
145
+
146
+ constructor(
147
+ private readonly image: Image,
148
+ readonly effect: ImageEffectName,
149
+ private readonly options: ImageEffectOptions &
150
+ Required<Pick<ImageEffectOptions, 'amplitude' | 'radius' | 'duration' | 'seed'>>,
151
+ ) {
152
+ this.userPaused = options.paused ?? false
153
+ this.finished = new Promise((resolve) => {
154
+ this.resolve = resolve
155
+ })
156
+ this.releaseOwnership = ownImage(image, this.stop)
157
+ for (const event of ['loaded', 'layoutChanged']) image.on(event, this.schedule)
158
+ for (const event of ['imageSourceChange', 'stretchChange'])
159
+ image.on(event, this.changed)
160
+ image.on('unloaded', this.stop)
161
+ image.style.on('visibilityChange', this.visible)
162
+ for (const event of surfaceChanges) image.style.on(event, this.changed)
163
+ Application.on(Application.suspendEvent, this.suspend)
164
+ Application.on(Application.resumeEvent, this.resume)
165
+ if (isIOS)
166
+ this.reduceObserver = Application.ios.addNotificationObserver(
167
+ UIAccessibilityReduceMotionStatusDidChangeNotification,
168
+ this.preferences,
169
+ )
170
+ this.schedule()
171
+ }
172
+ then<TResult1 = boolean, TResult2 = never>(
173
+ fulfilled?: ((value: boolean) => TResult1 | PromiseLike<TResult1>) | null,
174
+ rejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
175
+ ): PromiseLike<TResult1 | TResult2> {
176
+ return this.finished.then(fulfilled, rejected)
177
+ }
178
+ get paused() {
179
+ return this.userPaused
180
+ }
181
+ get progress() {
182
+ return this.renderer?.progress ?? this.position
183
+ }
184
+ set progress(value: number) {
185
+ if (this.effect === 'distortion')
186
+ throw new TypeError('Distortion follows touch input and cannot be scrubbed.')
187
+ if (!Number.isFinite(value) || value < 0 || value > 1)
188
+ throw new RangeError('Image progress must be between 0 and 1.')
189
+ if (this.terminal) return
190
+ this.position = value
191
+ this.renderer?.seek(value)
192
+ }
193
+ get diagnostics(): Readonly<ImageEffectDiagnostics> {
194
+ return {
195
+ ...this.stats,
196
+ ...(this.renderer
197
+ ? {
198
+ frames: this.renderer.frames,
199
+ textureBytes: this.renderer.textureBytes,
200
+ longestFrameMs: this.renderer.longestFrameMs,
201
+ }
202
+ : {}),
203
+ }
204
+ }
205
+ pause = () => {
206
+ if (this.terminal) return
207
+ this.userPaused = true
208
+ this.renderer?.pause()
209
+ }
210
+ play = () => {
211
+ if (this.terminal) return
212
+ this.userPaused = false
213
+ if (this.suspended) return
214
+ if (this.renderer) {
215
+ if (this.effect === 'distortion') this.renderer.resume()
216
+ else this.renderer.play(this.direction)
217
+ } else this.schedule()
218
+ }
219
+ reverse = () => {
220
+ if (this.effect === 'distortion')
221
+ throw new TypeError('Distortion has no finite playback direction.')
222
+ if (this.terminal) return
223
+ this.direction = this.direction === 1 ? 0 : 1
224
+ this.play()
225
+ }
226
+ finish = () => {
227
+ if (this.terminal) return
228
+ if (this.effect === 'distortion') return this.stop()
229
+ this.complete(true, this.direction)
230
+ }
231
+ stop = () => this.complete(false, this.progress)
232
+ private complete = (completed: boolean, position: number, preserveImage = false) => {
233
+ if (this.terminal) return
234
+ this.terminal = true
235
+ this.position = position
236
+ this.stats = { ...this.diagnostics, textureBytes: 0 }
237
+ this.renderer?.stop(preserveImage)
238
+ this.renderer = undefined
239
+ this.releaseOwnership()
240
+ for (const event of ['loaded', 'layoutChanged']) this.image.off(event, this.schedule)
241
+ for (const event of ['imageSourceChange', 'stretchChange'])
242
+ this.image.off(event, this.changed)
243
+ this.image.off('unloaded', this.stop)
244
+ this.image.style.off('visibilityChange', this.visible)
245
+ for (const event of surfaceChanges) this.image.style.off(event, this.changed)
246
+ Application.off(Application.suspendEvent, this.suspend)
247
+ Application.off(Application.resumeEvent, this.resume)
248
+ if (this.reduceObserver)
249
+ Application.ios.removeNotificationObserver(
250
+ this.reduceObserver,
251
+ UIAccessibilityReduceMotionStatusDidChangeNotification,
252
+ )
253
+ this.reduceObserver = undefined
254
+ if (completed && this.effect === 'dissolve' && position === 1) {
255
+ this.committing = true
256
+ // The view may clear its ImageSource on disposal. Keep the caller's
257
+ // destination reusable by giving the view its own wrapper.
258
+ const destination = new ImageSource(this.options.to!.getNativeSource())
259
+ destination.rotationAngle = this.options.to!.rotationAngle
260
+ this.image.imageSource = destination
261
+ this.committing = false
262
+ }
263
+ this.resolve(completed)
264
+ }
265
+ private changed = (event?: EventData) => {
266
+ if (this.committing) return
267
+ if (this.renderer)
268
+ this.complete(false, this.progress, event?.eventName === 'imageSourceChange')
269
+ else this.schedule()
270
+ }
271
+ private suspend = () => {
272
+ this.suspended = true
273
+ this.renderer?.pause()
274
+ }
275
+ private resume = () => {
276
+ this.suspended = false
277
+ this.schedule()
278
+ if (!this.userPaused) this.play()
279
+ }
280
+ private visible = () => {
281
+ if (this.image.visibility !== 'visible') this.stop()
282
+ }
283
+ private preferences = () => {
284
+ if (reducedMotion()) this.fallback('reduced-motion')
285
+ }
286
+ private fallback(reason: ImageEffectDiagnostics['reason']) {
287
+ this.stats = { ...this.stats, backend: 'fallback', reason }
288
+ // Reduced/unsupported rendering still performs a requested content change.
289
+ if (this.effect === 'dissolve' && this.direction === 1) {
290
+ this.complete(true, 1)
291
+ } else this.complete(false, 0)
292
+ }
293
+ private schedule = () => {
294
+ if (this.queued || this.terminal) return
295
+ this.queued = true
296
+ void Promise.resolve().then(() => {
297
+ this.queued = false
298
+ if (this.terminal || this.suspended || !this.image.isLoaded) return
299
+ if (reducedMotion()) return this.fallback('reduced-motion')
300
+ if (this.image.visibility !== 'visible') return this.fallback('not-visible')
301
+ const { width, height } = this.image.getActualSize()
302
+ if (width <= 0 || height <= 0) return
303
+ const dimensions = `${width}:${height}`
304
+ if (this.renderer) {
305
+ if (this.dimensions !== dimensions) this.stop()
306
+ return
307
+ }
308
+ if (!this.image.imageSource) return
309
+ this.dimensions = dimensions
310
+ const started = Date.now()
311
+ try {
312
+ this.renderer = createImageRenderer(
313
+ this.image,
314
+ {
315
+ ...this.options,
316
+ mode: names.indexOf(this.effect),
317
+ x: this.options.origin?.x ?? width / 2,
318
+ y: this.options.origin?.y ?? height / 2,
319
+ },
320
+ this.complete,
321
+ )
322
+ } catch (error) {
323
+ this.stats.preparationMs = Date.now() - started
324
+ this.stats.detail = String(error)
325
+ return this.fallback('unsupported')
326
+ }
327
+ if (!this.renderer) return this.fallback('unsupported')
328
+ this.stats.backend = this.renderer.backend
329
+ this.stats.preparationMs = Date.now() - started
330
+ this.stats.peakTextureBytes = this.renderer.textureBytes
331
+ this.renderer.pause()
332
+ if (this.position) this.renderer.seek(this.position)
333
+ if (!this.userPaused && !this.suspended) this.play()
334
+ })
335
+ }
336
+ }
@@ -0,0 +1,12 @@
1
+ import type { Image } from '@nativescript/core'
2
+
3
+ /** Rendering effects share one image surface; ordinary view transforms stay independent. */
4
+ const owners = new WeakMap<Image, () => void>()
5
+
6
+ export function ownImage(image: Image, stop: () => void): () => void {
7
+ owners.get(image)?.()
8
+ owners.set(image, stop)
9
+ return () => {
10
+ if (owners.get(image) === stop) owners.delete(image)
11
+ }
12
+ }
package/src/index.ts CHANGED
@@ -1,12 +1,69 @@
1
- export { animate, stop } from './animate.js'
1
+ export { animate, decay, set, stop } from './animate.js'
2
2
  export { sequence } from './sequence.js'
3
- export type { Step } from './sequence.js'
3
+ export type { Step, SequenceOptions, SequencePosition } from './sequence.js'
4
4
  export { press } from './press.js'
5
+ export { interactionSurface } from './interaction-surface.js'
6
+ export type { InteractionSurface } from './interaction-surface.js'
7
+ export { drag } from './drag.js'
8
+ export type { Drag, DragOptions, DragPoint, DragEvent, DragRelease } from './drag.js'
9
+ export { scrollLinked } from './scroll-linked.js'
10
+ export type { ScrollLinkedOptions } from './scroll-linked.js'
11
+ export { animateItems } from './items.js'
12
+ export type { ItemsOptions } from './items.js'
13
+ export { followKeyboard, onKeyboard } from './keyboard.js'
14
+ export type { KeyboardOptions, KeyboardState } from './keyboard.js'
15
+ export type { Haptic, HapticOptions, HapticStyle } from './haptics.js'
16
+ export { confetti, particles } from './particles.js'
17
+ export type { ConfettiOptions, ParticleOptions } from './particles.js'
18
+ export { shimmer } from './shimmer.js'
19
+ export type { ShimmerOptions } from './shimmer.js'
20
+ export { liquid } from './liquid.js'
21
+ export type { LiquidOptions, LiquidEffect } from './liquid.js'
22
+ export { imageEffect } from './image-effect.js'
23
+ export type {
24
+ ImageEffect,
25
+ ImageEffectName,
26
+ ImageEffectOptions,
27
+ ImageEffectDiagnostics,
28
+ } from './image-effect.js'
29
+ export { zoomable } from './zoomable.js'
30
+ export type { Zoomable, ZoomableOptions, ZoomState, PhotoDismissal } from './zoomable.js'
31
+ export { loader } from './loader.js'
32
+ export type { LoaderOptions, LoaderStyle } from './loader.js'
33
+ export { reveal, conceal } from './reveal.js'
34
+ export type { RevealOptions, ConcealOptions } from './reveal.js'
35
+ export { animateText } from './text.js'
36
+ export type { TextOptions } from './text.js'
37
+ export { rollTo } from './roll.js'
38
+ export type { RollOptions } from './roll.js'
39
+ export { symbolEffect } from './symbol-effect.js'
40
+ export type {
41
+ SymbolEffect,
42
+ SymbolEffectName,
43
+ SymbolEffectOptions,
44
+ } from './symbol-effect.js'
45
+ export { configure, onReducedMotion, reducedMotion } from './config.js'
46
+ export type { JoltConfig } from './config.js'
5
47
  export { definePreset, presets } from './presets.js'
6
48
  export type { Preset, PresetName, Presets } from './presets.js'
7
49
  export { springs } from './transition.js'
8
50
  export type { Spring, SpringName, Timing, Transition, Tween } from './transition.js'
9
51
  export { cubicBezier } from './easing.js'
10
52
  export type { CubicBezier, Easing, EasingFunction, EasingName } from './easing.js'
53
+ export type { PlaybackAnimation } from './playback.js'
11
54
  export type { Animation } from './engine.js'
55
+ export type { ColorValue, ColorKeyframes } from './color.js'
12
56
  export type * from './types.js'
57
+ export { motionScope } from './motion-scope.js'
58
+ export type { MotionScope, MotionResource } from './motion-scope.js'
59
+ export { motionStates } from './motion-states.js'
60
+ export type {
61
+ MotionStates,
62
+ MotionStatesOptions,
63
+ MotionState,
64
+ MotionValues,
65
+ StateTransition,
66
+ } from './motion-states.js'
67
+
68
+ export { animateLayout } from './layout.js'
69
+ export type { LayoutOptions } from './layout.js'
@@ -0,0 +1,41 @@
1
+ import type { View, ViewBase } from '@nativescript/core'
2
+ import { toView } from './animate.js'
3
+ import type { Target } from './types.js'
4
+
5
+ const surfaces = new WeakMap<ViewBase, Set<object>>()
6
+ export interface InteractionSurface {
7
+ readonly disposed: boolean
8
+ /** Existing animations retain their driver until they end. Disables future opt-in. */
9
+ dispose(): void
10
+ }
11
+
12
+ /** Opt a view/subtree into native geometry updates before starting its animations. */
13
+ export function interactionSurface(target: Target): InteractionSurface {
14
+ const view = toView(target)
15
+ let tokens = surfaces.get(view)
16
+ if (!tokens) surfaces.set(view, (tokens = new Set()))
17
+ const token = {}
18
+ tokens.add(token)
19
+ let disposed = false
20
+ return {
21
+ get disposed() {
22
+ return disposed
23
+ },
24
+ dispose() {
25
+ if (disposed) return
26
+ disposed = true
27
+ tokens.delete(token)
28
+ if (!tokens.size) surfaces.delete(view)
29
+ },
30
+ }
31
+ }
32
+
33
+ /** Evaluated at animation start; no per-frame JS traversal. */
34
+ export function hasInteractionSurface(view: View): boolean {
35
+ let current: ViewBase | undefined = view
36
+ while (current) {
37
+ if (surfaces.get(current)?.size) return true
38
+ current = current.parent
39
+ }
40
+ return false
41
+ }
@@ -0,0 +1,81 @@
1
+ declare class JoltInteractiveAnimation extends NSObject {
2
+ static startViewLayersKeyPathValuesTimesDurationDelayRepeatReverseFinalValueStartedAtCompletion(
3
+ view: UIView,
4
+ layers: NSArray<CALayer>,
5
+ keyPath: string,
6
+ values: NSArray<number>,
7
+ times: NSArray<number>,
8
+ duration: number,
9
+ delay: number,
10
+ repeat: number,
11
+ reverse: boolean,
12
+ finalValue: number,
13
+ startedAt: number,
14
+ completion: (completed: boolean) => void,
15
+ ): JoltInteractiveAnimation
16
+ static syncViewExcluding(view: UIView, keyPath: string | null): void
17
+ static valueForViewKeyPath(view: UIView, keyPath: string): number | null
18
+ static activeCount(): number
19
+ readonly elapsed: number
20
+ controlPaused(elapsed: number, paused: boolean): void
21
+ cancel(): void
22
+ }
23
+ declare class JoltImageEffect extends NSObject {
24
+ stopPreservingImage(): void
25
+ static createWithViewDestinationModeOriginXOriginYAmplitudeRadiusDurationSeedListener(
26
+ image: UIImageView,
27
+ destination: UIImage | null,
28
+ mode: number,
29
+ x: number,
30
+ y: number,
31
+ amplitude: number,
32
+ radius: number,
33
+ duration: number,
34
+ seed: number,
35
+ listener: (completed: boolean, progress: number) => void,
36
+ ): JoltImageEffect | null
37
+ readonly progress: number
38
+ readonly paused: boolean
39
+ readonly frames: number
40
+ readonly longestFrame: number
41
+ readonly textureBytes: number
42
+ play(endpoint: number): void
43
+ pause(): void
44
+ resume(): void
45
+ seek(progress: number): void
46
+ finish(endpoint: number): void
47
+ stop(): void
48
+ }
49
+ declare class JoltPhotoZoom extends NSObject {
50
+ static attachImageViewportMinimumMaximumDoubleTapDismissReducedBackdropChromeOnDismissOnSettled(
51
+ image: UIImageView,
52
+ viewport: UIView,
53
+ minimum: number,
54
+ maximum: number,
55
+ doubleTap: number,
56
+ dismiss: boolean,
57
+ reduced: boolean,
58
+ backdrop: UIView | null,
59
+ chrome: UIView | null,
60
+ onDismiss: (
61
+ x: number,
62
+ y: number,
63
+ scale: number,
64
+ velocity: number,
65
+ progress: number,
66
+ ) => void,
67
+ onSettled: (x: number, y: number, scale: number) => void,
68
+ ): JoltPhotoZoom
69
+ readonly x: number
70
+ readonly y: number
71
+ readonly scale: number
72
+ readonly dismissProgress: number
73
+ readonly dismissPending: boolean
74
+ zoomFocusXFocusYAnimated(scale: number, x: number, y: number, animated: boolean): void
75
+ reset(animated: boolean): void
76
+ requestDismiss(): void
77
+ setReduced(value: boolean): void
78
+ setActive(value: boolean): void
79
+ layout(): void
80
+ stop(): void
81
+ }