@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
package/src/sequence.ts CHANGED
@@ -1,22 +1,36 @@
1
+ import { timeScale } from './config.js'
2
+ import { getDriver } from './driver/index.js'
3
+ import { isDecorationProperty, validateDecorationTarget } from './decoration.js'
1
4
  import type { View } from '@nativescript/core'
2
5
  import { animate, startingValues, toViews } from './animate.js'
3
- import { arrivalOf, setArrival, stopView, type Animation } from './engine.js'
6
+ import {
7
+ arrivalOf,
8
+ arrivalTimingOf,
9
+ setArrival,
10
+ stopView,
11
+ type Animation,
12
+ } from './engine.js'
13
+ import { createClock } from './playback.js'
4
14
  import type { PresetName } from './presets.js'
5
15
  import { writeValue, type Property } from './properties.js'
6
16
  import type { SpringName } from './transition.js'
7
17
  import type { AnimateOptions, Keyframes, Target } from './types.js'
8
18
 
19
+ /** Absolute milliseconds, a signed offset from the previous arrival, or its start. */
20
+ export type SequencePosition = number | '<' | `${'+' | '-'}${number}`
21
+ export type SequenceOptions = AnimateOptions & { at?: SequencePosition }
22
+
9
23
  export type Step =
10
24
  | readonly [
11
25
  target: Target | readonly Target[],
12
26
  animation: Keyframes | PresetName,
13
- options?: AnimateOptions | SpringName,
27
+ options?: SequenceOptions | SpringName,
14
28
  ]
15
29
  | (() => Animation)
16
30
 
17
31
  /**
18
- * Plays steps one after the other. A step starts once the previous one looks
19
- * done: a spring is close to its target, even if it hasn't fully settled.
32
+ * By default a step starts once the previous one looks done. `at` can overlap
33
+ * that arrival, share the previous start, or select an absolute timeline time.
20
34
  * Views in later steps take their first absolute keyframe right away, so a title
21
35
  * that fades in third doesn't show up before its turn. Relative and null keyframes
22
36
  * read the current value when their step starts.
@@ -24,26 +38,180 @@ export type Step =
24
38
  * ```ts
25
39
  * await sequence([
26
40
  * [logo, 'popIn'],
27
- * [title, 'fadeInUp'],
28
- * [buttons, 'fadeInUp', { stagger: 50 }],
41
+ * [title, 'fadeInUp', { at: '-150' }],
42
+ * [buttons, 'fadeInUp', { stagger: 50, at: '<' }],
29
43
  * ])
30
44
  * ```
31
45
  */
32
46
  export function sequence(steps: readonly Step[]): Animation {
33
- applyStartingValues(steps)
47
+ const prepared = steps.map((step) => {
48
+ if (typeof step === 'function') return { step, position: undefined }
49
+ const [target, animation, options] = step
50
+ if (typeof options !== 'object' || options === null)
51
+ return { step, position: undefined }
52
+ const { at, ...transition } = options
53
+ const position = parsePosition(at)
54
+ return { step: [target, animation, transition] as Step, position }
55
+ })
56
+ applyStartingValues(prepared.map(({ step }) => step))
34
57
 
58
+ const driver = getDriver()
59
+ const slowdown = 1 / timeScale()
60
+ // Absolute positions share the platform clock, including Android duration scale.
61
+ const clock = prepared.some(({ position }) => typeof position === 'number')
62
+ ? (driver.clock?.(Infinity) ?? createClock(() => driver.now()))
63
+ : undefined
35
64
  const started: Animation[] = []
65
+ const cancellations = new Set<() => void>()
36
66
  let stopped = false
67
+ let resolveScheduled!: () => void
68
+ let rejectScheduled!: (reason: unknown) => void
69
+ const scheduled = new Promise<void>((resolve, reject) => {
70
+ resolveScheduled = resolve
71
+ rejectScheduled = reject
72
+ })
73
+ let pending = prepared.length
74
+ const states = prepared.map(({ step, position }) => {
75
+ return {
76
+ step,
77
+ position,
78
+ onStart: new Set<() => void>(),
79
+ animation: undefined as Animation | undefined,
80
+ }
81
+ })
82
+ const cancelSchedule = () => {
83
+ for (const cancel of cancellations) cancel()
84
+ cancellations.clear()
85
+ clock?.cancel()
86
+ }
87
+ const fail = (error: unknown) => {
88
+ if (stopped) return
89
+ stopped = true
90
+ cancelSchedule()
91
+ states.forEach((state) => state.onStart.clear())
92
+ started.forEach((child) => child.stop())
93
+ rejectScheduled(error)
94
+ }
95
+ const launch = (index: number) => {
96
+ const state = states[index]!
97
+ if (stopped || state.animation) return
98
+ try {
99
+ const step = state.step
100
+ const child = typeof step === 'function' ? step() : animate(...step)
101
+ state.animation = child
102
+ started.push(child)
103
+ // Observe setup failures immediately, even while other steps are still
104
+ // scheduled. Nested sequences can reject before this one reaches arrival.
105
+ void child.finished.catch(fail)
106
+ pending--
107
+ for (const callback of [...state.onStart]) callback()
108
+ state.onStart.clear()
109
+ if (pending === 0) {
110
+ cancelSchedule()
111
+ resolveScheduled()
112
+ }
113
+ } catch (error) {
114
+ fail(error)
115
+ }
116
+ }
117
+ const wait = (ms: number, callback: () => void) => {
118
+ if (stopped) return
119
+ if (ms <= 0) {
120
+ callback()
121
+ return
122
+ }
123
+ let cancel = () => {}
124
+ let fired = false
125
+ const off = driver.after(ms, () => {
126
+ fired = true
127
+ cancellations.delete(cancel)
128
+ if (!stopped) callback()
129
+ })
130
+ cancel = off
131
+ if (!fired) cancellations.add(cancel)
132
+ }
37
133
 
38
- const arrived = (async () => {
39
- for (const step of steps) {
40
- if (stopped) return
41
- const animation = typeof step === 'function' ? step() : animate(...step)
42
- started.push(animation)
43
- await arrivalOf(animation)
134
+ for (let index = 0; index < states.length; index++) {
135
+ const { position } = states[index]!
136
+ const begin = () => launch(index)
137
+ if (typeof position === 'number') wait(position * slowdown - clock!.time(), begin)
138
+ else if (index === 0)
139
+ wait(
140
+ typeof position === 'object' ? Math.max(0, position.offset) * slowdown : 0,
141
+ begin,
142
+ )
143
+ else {
144
+ const previous = states[index - 1]!
145
+ const follow = () => {
146
+ if (stopped || !previous.animation) return
147
+ if (position === '<') {
148
+ begin()
149
+ return
150
+ }
151
+ const offset = (position?.offset ?? 0) * slowdown
152
+ if (offset < 0) {
153
+ const timing = arrivalTimingOf(previous.animation)
154
+ if (!timing) {
155
+ fail(
156
+ new TypeError(
157
+ 'A negative at offset needs an animate() or decay() arrival forecast.',
158
+ ),
159
+ )
160
+ return
161
+ }
162
+ let off = () => {}
163
+ let cancelTimer = () => {}
164
+ let fired = false
165
+ let disposed = false
166
+ let queued = false
167
+ const dispose = () => {
168
+ disposed = true
169
+ off()
170
+ cancelTimer()
171
+ cancellations.delete(dispose)
172
+ }
173
+ const update = () => {
174
+ cancelTimer()
175
+ if (stopped || fired || disposed) return
176
+ const state = timing.read()
177
+ if (!state.arrived && (state.paused || state.remaining === undefined)) return
178
+ const remaining = state.arrived ? 0 : state.remaining! + offset
179
+ if (!Number.isFinite(remaining)) return
180
+ const fire = () => {
181
+ if (stopped || fired || disposed) return
182
+ fired = true
183
+ dispose()
184
+ begin()
185
+ }
186
+ if (remaining <= 0) fire()
187
+ else {
188
+ const cancel = driver.after(remaining, fire)
189
+ if (disposed) cancel()
190
+ else cancelTimer = cancel
191
+ }
192
+ }
193
+ off = timing.subscribe(() => {
194
+ cancelTimer()
195
+ // An interruption can notify from inside another animation's
196
+ // constructor. Start followers after its ownership changes finish.
197
+ if (queued || disposed) return
198
+ queued = true
199
+ void Promise.resolve().then(() => {
200
+ queued = false
201
+ update()
202
+ })
203
+ })
204
+ cancellations.add(dispose)
205
+ update()
206
+ } else void arrivalOf(previous.animation).then(() => wait(offset, begin), fail)
207
+ }
208
+ if (previous.animation) follow()
209
+ else previous.onStart.add(follow)
44
210
  }
45
- })()
211
+ }
212
+ if (pending === 0) resolveScheduled()
46
213
 
214
+ const arrived = scheduled.then(() => Promise.all(started.map(arrivalOf)))
47
215
  const finished = arrived.then(async () => {
48
216
  const results = await Promise.all(started.map((animation) => animation.finished))
49
217
  return !stopped && results.length === steps.length && results.every(Boolean)
@@ -52,7 +220,11 @@ export function sequence(steps: readonly Step[]): Animation {
52
220
  const animation: Animation = {
53
221
  finished,
54
222
  stop() {
223
+ if (stopped) return
55
224
  stopped = true
225
+ cancelSchedule()
226
+ states.forEach((state) => state.onStart.clear())
227
+ resolveScheduled()
56
228
  started.forEach((animation) => animation.stop())
57
229
  },
58
230
  then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
@@ -61,6 +233,21 @@ export function sequence(steps: readonly Step[]): Animation {
61
233
  return animation
62
234
  }
63
235
 
236
+ type Position = number | '<' | { offset: number } | undefined
237
+ function parsePosition(at: SequencePosition | undefined): Position {
238
+ if (at === undefined || at === '<') return at
239
+ if (typeof at === 'number' && Number.isFinite(at) && at >= 0) return at
240
+ if (
241
+ typeof at === 'string' &&
242
+ /^[+-](?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(at) &&
243
+ Number.isFinite(Number(at))
244
+ )
245
+ return { offset: Number(at) }
246
+ throw new RangeError(
247
+ 'Sequence at must be a nonnegative finite time, "<", or a signed finite offset.',
248
+ )
249
+ }
250
+
64
251
  function applyStartingValues(steps: readonly Step[]) {
65
252
  const claimed = new Map<View, Set<Property>>()
66
253
  for (const step of steps) {
@@ -69,6 +256,8 @@ function applyStartingValues(steps: readonly Step[]) {
69
256
  const starts = startingValues(animation, options)
70
257
 
71
258
  for (const view of toViews(target)) {
259
+ for (const property of starts.keys())
260
+ if (isDecorationProperty(property)) validateDecorationTarget(view, property)
72
261
  let properties = claimed.get(view)
73
262
  if (!properties) claimed.set(view, (properties = new Set()))
74
263
 
@@ -82,6 +271,7 @@ function applyStartingValues(steps: readonly Step[]) {
82
271
  for (const [property, value] of fresh) {
83
272
  if (value !== null) writeValue(view, property, value)
84
273
  }
274
+ getDriver().sync?.(view)
85
275
  for (const property of starts.keys()) properties.add(property)
86
276
  }
87
277
  }
package/src/shimmer.ts ADDED
@@ -0,0 +1,180 @@
1
+ import { Application, 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 interface ShimmerOptions {
7
+ /** Milliseconds per sweep. Defaults to 1500. */
8
+ duration?: number
9
+ }
10
+
11
+ const running = new WeakMap<View, () => void>()
12
+ const key = 'jolt:shimmer'
13
+
14
+ // NativeScript saves a mask here while applying clip-path or non-uniform borders.
15
+ type MaskedView = UIView & { originalMask?: CALayer | null }
16
+
17
+ /**
18
+ * Sweeps a native gradient mask across an iOS placeholder until stopped.
19
+ * Waits for layout, respects Reduce Motion and pauses while the view is unloaded.
20
+ * Android is a no-op. Returns an idempotent cleanup function.
21
+ */
22
+ export function shimmer(target: Target, options: ShimmerOptions = {}): () => void {
23
+ const view = toView(target)
24
+ const duration = options.duration ?? 1500
25
+ if (!Number.isFinite(duration) || duration <= 0)
26
+ throw new RangeError('Shimmer duration must be a positive finite number.')
27
+ running.get(view)?.()
28
+ if (!isIOS) return () => {}
29
+ return new Shimmer(view, duration).stop
30
+ }
31
+
32
+ class Shimmer {
33
+ private stopped = false
34
+ private queued = false
35
+ private waitingForLoad: boolean
36
+ private native: MaskedView | undefined
37
+ private owner: CALayer | undefined
38
+ private gradient: CAGradientLayer | undefined
39
+ private observer: ReturnType<typeof Application.ios.addNotificationObserver> | undefined
40
+
41
+ constructor(
42
+ private readonly view: View,
43
+ private readonly duration: number,
44
+ ) {
45
+ this.waitingForLoad = !view.isLoaded
46
+ running.set(view, this.stop)
47
+ view.on('loaded', this.loaded)
48
+ view.on('layoutChanged', this.schedule)
49
+ view.on('unloaded', this.unloaded)
50
+ view.style.on('backgroundInternalChange', this.schedule)
51
+ this.update()
52
+ }
53
+
54
+ stop = () => {
55
+ if (this.stopped) return
56
+ this.stopped = true
57
+ const { view } = this
58
+ view.off('loaded', this.loaded)
59
+ view.off('layoutChanged', this.schedule)
60
+ view.off('unloaded', this.unloaded)
61
+ view.style.off('backgroundInternalChange', this.schedule)
62
+ if (running.get(view) === this.stop) running.delete(view)
63
+ this.unloaded()
64
+ }
65
+
66
+ private loaded = () => {
67
+ this.waitingForLoad = false
68
+ this.schedule()
69
+ }
70
+
71
+ private unloaded = () => {
72
+ this.waitingForLoad = true
73
+ if (this.observer) {
74
+ Application.ios.removeNotificationObserver(
75
+ this.observer,
76
+ UIAccessibilityReduceMotionStatusDidChangeNotification,
77
+ )
78
+ this.observer = undefined
79
+ }
80
+ this.remove()
81
+ }
82
+
83
+ private schedule = () => {
84
+ if (this.stopped || this.queued) return
85
+ this.queued = true
86
+ // Core raises layoutChanged before redrawing the background and its mask.
87
+ // Attach after that pass, coalescing events from the same layout.
88
+ void Promise.resolve().then(() => {
89
+ this.queued = false
90
+ this.update()
91
+ })
92
+ }
93
+
94
+ private update = () => {
95
+ if (this.stopped || this.waitingForLoad || !this.view.isLoaded) return
96
+ const native = this.view.nativeViewProtected as MaskedView | undefined
97
+ if (!native) return
98
+ this.observer ??= Application.ios.addNotificationObserver(
99
+ UIAccessibilityReduceMotionStatusDidChangeNotification,
100
+ this.update,
101
+ )
102
+ if (reducedMotion()) return this.remove()
103
+ const bounds = native.layer.bounds
104
+ if (bounds.size.width <= 0 || bounds.size.height <= 0) return this.remove()
105
+ if (this.native && this.native !== native) this.remove()
106
+
107
+ CATransaction.begin()
108
+ CATransaction.setDisableActions(true)
109
+ try {
110
+ this.native = native
111
+ const gradient = (this.gradient ??= this.createGradient())
112
+ // Leave Core's mask at the root so it can keep updating its path on resize.
113
+ let owner = native.layer
114
+ while (owner.mask && owner.mask !== gradient) owner = owner.mask
115
+ if (this.owner && this.owner !== owner && this.owner.mask === gradient)
116
+ this.owner.mask = null
117
+ this.owner = owner
118
+ owner.mask = gradient
119
+ // Don't let a later background redraw resurrect our mask after cleanup.
120
+ if (native.originalMask === gradient) native.originalMask = null
121
+
122
+ // Core's CAShapeLayer masks draw a path with zero bounds. In that case the
123
+ // path uses the view's coordinates; other masks have their own bounds.
124
+ const area =
125
+ owner.bounds.size.width > 0 && owner.bounds.size.height > 0
126
+ ? owner.bounds
127
+ : bounds
128
+ gradient.frame = CGRectMake(
129
+ area.origin.x - area.size.width,
130
+ area.origin.y,
131
+ area.size.width * 3,
132
+ area.size.height,
133
+ )
134
+ } finally {
135
+ CATransaction.commit()
136
+ }
137
+ }
138
+
139
+ private createGradient(): CAGradientLayer {
140
+ const gradient = CAGradientLayer.new()
141
+ const dim = UIColor.colorWithWhiteAlpha(1, 0.35).CGColor
142
+ gradient.colors = NSArray.arrayWithArray([dim, UIColor.whiteColor.CGColor, dim])
143
+ gradient.startPoint = CGPointMake(0, 0.5)
144
+ gradient.endPoint = CGPointMake(1, 0.5)
145
+
146
+ // A three-width mask lets the band enter and leave while every stop stays
147
+ // within the documented 0...1 range for CAGradientLayer.locations.
148
+ const from = NSArray.arrayWithArray([0, 0.125, 0.25])
149
+ const to = NSArray.arrayWithArray([0.75, 0.875, 1])
150
+ gradient.locations = from
151
+ const animation = CABasicAnimation.animationWithKeyPath('locations')
152
+ animation.fromValue = from
153
+ animation.toValue = to
154
+ animation.duration = this.duration / 1000
155
+ animation.repeatCount = Infinity
156
+ animation.removedOnCompletion = false
157
+ animation.timingFunction = CAMediaTimingFunction.functionWithName(
158
+ kCAMediaTimingFunctionLinear,
159
+ )
160
+ gradient.addAnimationForKey(animation, key)
161
+ return gradient
162
+ }
163
+
164
+ private remove() {
165
+ const { gradient, owner, native } = this
166
+ if (!gradient) return
167
+ CATransaction.begin()
168
+ CATransaction.setDisableActions(true)
169
+ try {
170
+ gradient.removeAnimationForKey(key)
171
+ if (owner?.mask === gradient) owner.mask = null
172
+ if (native?.originalMask === gradient) native.originalMask = null
173
+ } finally {
174
+ CATransaction.commit()
175
+ }
176
+ this.owner = undefined
177
+ this.gradient = undefined
178
+ this.native = undefined
179
+ }
180
+ }