@rafaelaugusto/jolt 0.1.0-beta.4 → 0.1.0-beta.6

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 (201) hide show
  1. package/README.md +134 -4
  2. package/dist/animate.d.ts +7 -4
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +58 -13
  5. package/dist/animate.js.map +1 -1
  6. package/dist/config.d.ts +22 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +34 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/decoration.d.ts +18 -0
  11. package/dist/decoration.d.ts.map +1 -0
  12. package/dist/decoration.js +155 -0
  13. package/dist/decoration.js.map +1 -0
  14. package/dist/driver/android-color.d.ts +3 -0
  15. package/dist/driver/android-color.d.ts.map +1 -1
  16. package/dist/driver/android-color.js +20 -18
  17. package/dist/driver/android-color.js.map +1 -1
  18. package/dist/driver/android-decoration.d.ts +22 -0
  19. package/dist/driver/android-decoration.d.ts.map +1 -0
  20. package/dist/driver/android-decoration.js +139 -0
  21. package/dist/driver/android-decoration.js.map +1 -0
  22. package/dist/driver/android-playback.d.ts +13 -0
  23. package/dist/driver/android-playback.d.ts.map +1 -0
  24. package/dist/driver/android-playback.js +141 -0
  25. package/dist/driver/android-playback.js.map +1 -0
  26. package/dist/driver/android.d.ts.map +1 -1
  27. package/dist/driver/android.js +30 -36
  28. package/dist/driver/android.js.map +1 -1
  29. package/dist/driver/index.d.ts +9 -0
  30. package/dist/driver/index.d.ts.map +1 -1
  31. package/dist/driver/index.js.map +1 -1
  32. package/dist/driver/ios-color.d.ts +2 -0
  33. package/dist/driver/ios-color.d.ts.map +1 -1
  34. package/dist/driver/ios-color.js +19 -8
  35. package/dist/driver/ios-color.js.map +1 -1
  36. package/dist/driver/ios-decoration.d.ts +17 -0
  37. package/dist/driver/ios-decoration.d.ts.map +1 -0
  38. package/dist/driver/ios-decoration.js +271 -0
  39. package/dist/driver/ios-decoration.js.map +1 -0
  40. package/dist/driver/ios-playback.d.ts +10 -0
  41. package/dist/driver/ios-playback.d.ts.map +1 -0
  42. package/dist/driver/ios-playback.js +47 -0
  43. package/dist/driver/ios-playback.js.map +1 -0
  44. package/dist/driver/ios-shadow.d.ts +6 -0
  45. package/dist/driver/ios-shadow.d.ts.map +1 -0
  46. package/dist/driver/ios-shadow.js +52 -0
  47. package/dist/driver/ios-shadow.js.map +1 -0
  48. package/dist/driver/ios.d.ts.map +1 -1
  49. package/dist/driver/ios.js +76 -40
  50. package/dist/driver/ios.js.map +1 -1
  51. package/dist/engine.d.ts +64 -3
  52. package/dist/engine.d.ts.map +1 -1
  53. package/dist/engine.js +243 -19
  54. package/dist/engine.js.map +1 -1
  55. package/dist/haptics.d.ts +4 -1
  56. package/dist/haptics.d.ts.map +1 -1
  57. package/dist/haptics.js +27 -3
  58. package/dist/haptics.js.map +1 -1
  59. package/dist/index.d.ts +12 -1
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +5 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/items.d.ts +20 -0
  64. package/dist/items.d.ts.map +1 -0
  65. package/dist/items.js +109 -0
  66. package/dist/items.js.map +1 -0
  67. package/dist/keyboard.d.ts +17 -0
  68. package/dist/keyboard.d.ts.map +1 -0
  69. package/dist/keyboard.js +116 -0
  70. package/dist/keyboard.js.map +1 -0
  71. package/dist/label.d.ts +7 -0
  72. package/dist/label.d.ts.map +1 -0
  73. package/dist/label.js +60 -0
  74. package/dist/label.js.map +1 -0
  75. package/dist/liquid.d.ts.map +1 -1
  76. package/dist/liquid.js +2 -1
  77. package/dist/liquid.js.map +1 -1
  78. package/dist/loader.d.ts.map +1 -1
  79. package/dist/loader.js +2 -1
  80. package/dist/loader.js.map +1 -1
  81. package/dist/particles.d.ts.map +1 -1
  82. package/dist/particles.js +4 -3
  83. package/dist/particles.js.map +1 -1
  84. package/dist/path.d.ts +12 -0
  85. package/dist/path.d.ts.map +1 -0
  86. package/dist/path.js +90 -0
  87. package/dist/path.js.map +1 -0
  88. package/dist/playback-group.d.ts +6 -0
  89. package/dist/playback-group.d.ts.map +1 -0
  90. package/dist/playback-group.js +141 -0
  91. package/dist/playback-group.js.map +1 -0
  92. package/dist/playback.d.ts +25 -0
  93. package/dist/playback.d.ts.map +1 -0
  94. package/dist/playback.js +35 -0
  95. package/dist/playback.js.map +1 -0
  96. package/dist/press.d.ts.map +1 -1
  97. package/dist/press.js +3 -3
  98. package/dist/press.js.map +1 -1
  99. package/dist/properties.d.ts +2 -1
  100. package/dist/properties.d.ts.map +1 -1
  101. package/dist/properties.js +14 -1
  102. package/dist/properties.js.map +1 -1
  103. package/dist/reveal.d.ts.map +1 -1
  104. package/dist/reveal.js +3 -2
  105. package/dist/reveal.js.map +1 -1
  106. package/dist/roll-diff.d.ts +22 -0
  107. package/dist/roll-diff.d.ts.map +1 -0
  108. package/dist/roll-diff.js +36 -0
  109. package/dist/roll-diff.js.map +1 -0
  110. package/dist/roll.d.ts +29 -0
  111. package/dist/roll.d.ts.map +1 -0
  112. package/dist/roll.js +442 -0
  113. package/dist/roll.js.map +1 -0
  114. package/dist/scroll-linked.d.ts +10 -0
  115. package/dist/scroll-linked.d.ts.map +1 -0
  116. package/dist/scroll-linked.js +58 -0
  117. package/dist/scroll-linked.js.map +1 -0
  118. package/dist/sequence.d.ts +10 -5
  119. package/dist/sequence.d.ts.map +1 -1
  120. package/dist/sequence.js +208 -14
  121. package/dist/sequence.js.map +1 -1
  122. package/dist/shimmer.d.ts.map +1 -1
  123. package/dist/shimmer.js +2 -1
  124. package/dist/shimmer.js.map +1 -1
  125. package/dist/svg-path.d.ts +14 -0
  126. package/dist/svg-path.d.ts.map +1 -0
  127. package/dist/svg-path.js +281 -0
  128. package/dist/svg-path.js.map +1 -0
  129. package/dist/symbol-effect.d.ts.map +1 -1
  130. package/dist/symbol-effect.js +4 -3
  131. package/dist/symbol-effect.js.map +1 -1
  132. package/dist/text-layout.d.ts +8 -0
  133. package/dist/text-layout.d.ts.map +1 -1
  134. package/dist/text-layout.js +8 -2
  135. package/dist/text-layout.js.map +1 -1
  136. package/dist/text.d.ts +1 -1
  137. package/dist/text.d.ts.map +1 -1
  138. package/dist/text.js +60 -64
  139. package/dist/text.js.map +1 -1
  140. package/dist/track.d.ts +2 -0
  141. package/dist/track.d.ts.map +1 -1
  142. package/dist/track.js +72 -16
  143. package/dist/track.js.map +1 -1
  144. package/dist/transition.d.ts.map +1 -1
  145. package/dist/transition.js +4 -1
  146. package/dist/transition.js.map +1 -1
  147. package/dist/types.d.ts +22 -1
  148. package/dist/types.d.ts.map +1 -1
  149. package/dist/vue/index.d.ts +89 -0
  150. package/dist/vue/index.d.ts.map +1 -0
  151. package/dist/vue/index.js +134 -0
  152. package/dist/vue/index.js.map +1 -0
  153. package/package.json +15 -3
  154. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +2 -2
  155. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  156. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  157. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  158. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  159. package/platforms/android/proguard-rules.pro +7 -0
  160. package/src/android-plugin.d.ts +64 -0
  161. package/src/animate.ts +96 -33
  162. package/src/config.ts +46 -0
  163. package/src/decoration.ts +180 -0
  164. package/src/driver/android-color.ts +22 -20
  165. package/src/driver/android-decoration.ts +182 -0
  166. package/src/driver/android-playback.ts +157 -0
  167. package/src/driver/android.ts +57 -43
  168. package/src/driver/index.ts +15 -0
  169. package/src/driver/ios-color.ts +19 -10
  170. package/src/driver/ios-decoration.ts +332 -0
  171. package/src/driver/ios-playback.ts +51 -0
  172. package/src/driver/ios-shadow.ts +50 -0
  173. package/src/driver/ios.ts +87 -46
  174. package/src/engine.ts +267 -22
  175. package/src/haptics.ts +25 -2
  176. package/src/index.ts +12 -1
  177. package/src/items.ts +140 -0
  178. package/src/keyboard.ts +132 -0
  179. package/src/label.ts +76 -0
  180. package/src/liquid.ts +2 -1
  181. package/src/loader.ts +2 -1
  182. package/src/particles.ts +4 -4
  183. package/src/path.ts +124 -0
  184. package/src/playback-group.ts +143 -0
  185. package/src/playback.ts +62 -0
  186. package/src/press.ts +3 -5
  187. package/src/properties.ts +18 -2
  188. package/src/reveal.ts +3 -3
  189. package/src/roll-diff.ts +57 -0
  190. package/src/roll.ts +515 -0
  191. package/src/scroll-linked.ts +73 -0
  192. package/src/sequence.ts +204 -14
  193. package/src/shimmer.ts +2 -1
  194. package/src/svg-path.ts +355 -0
  195. package/src/symbol-effect.ts +4 -3
  196. package/src/text-layout.ts +14 -2
  197. package/src/text.ts +62 -81
  198. package/src/track.ts +98 -25
  199. package/src/transition.ts +4 -1
  200. package/src/types.ts +23 -1
  201. package/src/vue/index.ts +189 -0
package/src/haptics.ts CHANGED
@@ -48,6 +48,7 @@ export function playHaptics(
48
48
  options: HapticOptions | undefined,
49
49
  driver: Driver,
50
50
  startedAt: number,
51
+ elapsedTime = () => driver.now() - startedAt,
51
52
  ) {
52
53
  if (!isIOS || !options || typeof UIImpactFeedbackGenerator === 'undefined') return
53
54
  const moving = tracks.filter(
@@ -69,6 +70,7 @@ export function playHaptics(
69
70
  let generator: UIImpactFeedbackGenerator | undefined =
70
71
  UIImpactFeedbackGenerator.alloc().initWithStyle(styles[options.style])
71
72
  const native = view.nativeViewProtected as UIView
73
+ let paused = false
72
74
  let next = 0
73
75
  let cancelPrepare: (() => void) | undefined
74
76
  let cancelImpact: (() => void) | undefined
@@ -79,6 +81,11 @@ export function playHaptics(
79
81
  !!native.window &&
80
82
  UIApplication.sharedApplication.applicationState === 0 // UIApplicationState.Active
81
83
 
84
+ const cancelTimers = () => {
85
+ cancelPrepare?.()
86
+ cancelImpact?.()
87
+ cancelPrepare = cancelImpact = undefined
88
+ }
82
89
  const cancel = () => {
83
90
  cancelPrepare?.()
84
91
  cancelImpact?.()
@@ -88,7 +95,7 @@ export function playHaptics(
88
95
  }
89
96
 
90
97
  const fire = (tolerance = 0) => {
91
- const elapsed = driver.now() - startedAt
98
+ const elapsed = elapsedTime()
92
99
  // A busy UI thread can delay timers. Drop stale impacts instead of replaying
93
100
  // several old taps at once when it becomes responsive again.
94
101
  while (next < offsets.length && offsets[next]! <= elapsed + tolerance) {
@@ -98,8 +105,9 @@ export function playHaptics(
98
105
  }
99
106
 
100
107
  const schedule = () => {
108
+ if (paused) return
101
109
  if (!generator || next === offsets.length) return cancel()
102
- const remaining = startedAt + offsets[next]! - driver.now()
110
+ const remaining = offsets[next]! - elapsedTime()
103
111
  if (active()) generator.prepare()
104
112
  if (remaining > PREPARE_AHEAD) {
105
113
  cancelPrepare = driver.after(remaining - PREPARE_AHEAD, () => {
@@ -116,6 +124,21 @@ export function playHaptics(
116
124
  schedule()
117
125
  return {
118
126
  cancel,
127
+ pause() {
128
+ paused = true
129
+ cancelTimers()
130
+ },
131
+ play() {
132
+ if (paused) {
133
+ paused = false
134
+ schedule()
135
+ }
136
+ },
137
+ seek() {
138
+ cancelTimers()
139
+ while (next < offsets.length && offsets[next]! <= elapsedTime()) next++
140
+ schedule()
141
+ },
119
142
  finish() {
120
143
  // Native completion may beat the timer at the final keyframe by a frame.
121
144
  fire(COMPLETION_TOLERANCE)
package/src/index.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  export { animate, decay, 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 { scrollLinked } from './scroll-linked.js'
6
+ export type { ScrollLinkedOptions } from './scroll-linked.js'
7
+ export { animateItems } from './items.js'
8
+ export type { ItemsOptions } from './items.js'
9
+ export { followKeyboard } from './keyboard.js'
10
+ export type { KeyboardOptions } from './keyboard.js'
5
11
  export type { Haptic, HapticOptions, HapticStyle } from './haptics.js'
6
12
  export { confetti, particles } from './particles.js'
7
13
  export type { ConfettiOptions, ParticleOptions } from './particles.js'
@@ -15,18 +21,23 @@ export { reveal, conceal } from './reveal.js'
15
21
  export type { RevealOptions, ConcealOptions } from './reveal.js'
16
22
  export { animateText } from './text.js'
17
23
  export type { TextOptions } from './text.js'
24
+ export { rollTo } from './roll.js'
25
+ export type { RollOptions } from './roll.js'
18
26
  export { symbolEffect } from './symbol-effect.js'
19
27
  export type {
20
28
  SymbolEffect,
21
29
  SymbolEffectName,
22
30
  SymbolEffectOptions,
23
31
  } from './symbol-effect.js'
32
+ export { configure } from './config.js'
33
+ export type { JoltConfig } from './config.js'
24
34
  export { definePreset, presets } from './presets.js'
25
35
  export type { Preset, PresetName, Presets } from './presets.js'
26
36
  export { springs } from './transition.js'
27
37
  export type { Spring, SpringName, Timing, Transition, Tween } from './transition.js'
28
38
  export { cubicBezier } from './easing.js'
29
39
  export type { CubicBezier, Easing, EasingFunction, EasingName } from './easing.js'
40
+ export type { PlaybackAnimation } from './playback.js'
30
41
  export type { Animation } from './engine.js'
31
42
  export type { ColorValue, ColorKeyframes } from './color.js'
32
43
  export type * from './types.js'
package/src/items.ts ADDED
@@ -0,0 +1,140 @@
1
+ import { ListView, View, type EventData, type ItemEventData } from '@nativescript/core'
2
+ import { animate, endingValues, startingValues, toView } from './animate.js'
3
+ import { getDriver } from './driver/index.js'
4
+ import { stopView } from './engine.js'
5
+ import type { PresetName } from './presets.js'
6
+ import { writeValue } from './properties.js'
7
+ import type { SpringName } from './transition.js'
8
+ import type { AnimateOptions, Keyframes, Target } from './types.js'
9
+
10
+ export type ItemsOptions = AnimateOptions & {
11
+ /** Animate each data item only the first time it shows up. Defaults to false. */
12
+ once?: boolean
13
+ }
14
+
15
+ interface Pending {
16
+ index: number
17
+ section: number | undefined
18
+ item: unknown
19
+ key: string
20
+ }
21
+
22
+ type Row = View & {
23
+ bindingContext?: unknown
24
+ _listViewItemIndex?: number
25
+ _listViewSectionIndex?: number
26
+ }
27
+
28
+ interface Source extends View {
29
+ items?: unknown
30
+ _getDataItem?(index: number): unknown
31
+ _getDataItemInSection?(section: number, index: number): unknown
32
+ }
33
+
34
+ /**
35
+ * Plays an animation on each row of a list as it comes on screen. Rows are
36
+ * recycled, so a row that is reused for another item starts over, or settles
37
+ * at the end values when that item was already shown and `once` is set.
38
+ * Returns a function that removes the effect.
39
+ *
40
+ * ```ts
41
+ * animateItems(listView, 'fadeInUp')
42
+ * animateItems(listView, 'fadeInUp', { once: true })
43
+ * ```
44
+ */
45
+ export function animateItems(
46
+ list: Target,
47
+ animation: Keyframes | PresetName,
48
+ options?: ItemsOptions | SpringName,
49
+ ): () => void {
50
+ const source = toView(list) as Source
51
+ const { once = false, ...rest } = typeof options === 'object' ? options : {}
52
+ if (rest.stagger !== undefined)
53
+ throw new TypeError('animateItems animates one row at a time; stagger has no effect.')
54
+ const transition: AnimateOptions | SpringName | undefined =
55
+ typeof options === 'string' ? options : rest
56
+ const properties = [...startingValues(animation, transition).keys()]
57
+ const ends = endingValues(animation, transition)
58
+
59
+ const rows = new Set<Row>()
60
+ const seenItems = new WeakSet<object>()
61
+ const seenKeys = new Set<string>()
62
+ const pending: Pending[] = []
63
+ let queued = false
64
+ let disposed = false
65
+
66
+ const settle = (row: Row) => {
67
+ stopView(row, properties)
68
+ for (const [property, value] of ends)
69
+ if (value !== null) writeValue(row, property, value)
70
+ getDriver().sync?.(row)
71
+ }
72
+ const show = (row: Row, { item, key }: Pending) => {
73
+ rows.add(row)
74
+ const tracked = typeof item === 'object' && item !== null
75
+ const seen = tracked ? seenItems.has(item) : seenKeys.has(key)
76
+ if (tracked) seenItems.add(item)
77
+ else seenKeys.add(key)
78
+ if (once && seen) settle(row)
79
+ else {
80
+ stopView(row, properties)
81
+ animate(row, animation, transition)
82
+ }
83
+ }
84
+ const flush = () => {
85
+ queued = false
86
+ if (disposed) return
87
+ const current = new Set<Row>()
88
+ const fresh: Row[] = []
89
+ source.eachChildView((child: Row) => {
90
+ current.add(child)
91
+ if (!rows.has(child)) fresh.push(child)
92
+ return true
93
+ })
94
+ for (const row of rows) if (!current.has(row)) rows.delete(row)
95
+ for (const entry of pending.splice(0)) {
96
+ const row =
97
+ fresh.find(
98
+ (candidate) =>
99
+ candidate._listViewItemIndex === entry.index &&
100
+ candidate._listViewSectionIndex === entry.section,
101
+ ) ??
102
+ fresh.find(
103
+ (candidate) =>
104
+ entry.item !== undefined && candidate.bindingContext === entry.item,
105
+ )
106
+ if (!row) continue
107
+ fresh.splice(fresh.indexOf(row), 1)
108
+ show(row, entry)
109
+ }
110
+ }
111
+ const onItemLoading = (event: EventData) => {
112
+ const { index, section, view } = event as ItemEventData
113
+ const item =
114
+ section === undefined
115
+ ? source._getDataItem?.(index)
116
+ : source._getDataItemInSection?.(section, index)
117
+ const entry: Pending = { index, section, item, key: `${section ?? ''}:${index}` }
118
+ if (view instanceof View) {
119
+ show(view, entry)
120
+ return
121
+ }
122
+ pending.push(entry)
123
+ if (queued) return
124
+ queued = true
125
+ void Promise.resolve().then(flush)
126
+ }
127
+ const cleanup = () => {
128
+ if (disposed) return
129
+ disposed = true
130
+ source.off(ListView.itemLoadingEvent, onItemLoading)
131
+ source.off('unloaded', cleanup)
132
+ pending.length = 0
133
+ for (const row of rows) settle(row)
134
+ rows.clear()
135
+ }
136
+
137
+ source.on(ListView.itemLoadingEvent, onItemLoading)
138
+ source.on('unloaded', cleanup)
139
+ return cleanup
140
+ }
@@ -0,0 +1,132 @@
1
+ import { Application, Utils, isAndroid, isIOS, type View } from '@nativescript/core'
2
+ import { stop, toView } from './animate.js'
3
+ import type { Target } from './types.js'
4
+
5
+ export interface KeyboardOptions {
6
+ /** DIPs kept between the view's bottom edge and the keyboard. Defaults to zero. */
7
+ offset?: number
8
+ }
9
+
10
+ const BEGIN_FROM_CURRENT_STATE = 4
11
+
12
+ /**
13
+ * Lifts the view by its overlap with the software keyboard, at the keyboard's
14
+ * own speed and curve, and lets it back down when the keyboard hides.
15
+ * Returns a function that removes the effect.
16
+ *
17
+ * ```ts
18
+ * const release = followKeyboard(composer)
19
+ * followKeyboard(sendButton, { offset: 12 })
20
+ * ```
21
+ */
22
+ export function followKeyboard(
23
+ target: Target,
24
+ options: KeyboardOptions = {},
25
+ ): () => void {
26
+ const view = toView(target)
27
+ const offset = options.offset ?? 0
28
+ if (!Number.isFinite(offset)) throw new RangeError('Keyboard offset must be finite.')
29
+ if (isIOS) return followIOS(view, offset)
30
+ if (isAndroid) return followAndroid(view, offset)
31
+ return () => {}
32
+ }
33
+
34
+ function followIOS(view: View, offset: number) {
35
+ let disposed = false
36
+ let current = 0
37
+ const onKeyboard = (notification: NSNotification) => {
38
+ if (disposed || !view.isLoaded) return
39
+ const native = view.nativeViewProtected as UIView | undefined
40
+ const window = native?.window
41
+ const info = notification.userInfo
42
+ if (!native || !window || !info) return
43
+ const end = info.objectForKey(UIKeyboardFrameEndUserInfoKey)?.CGRectValue as
44
+ CGRect | undefined
45
+ if (!end) return
46
+ const keyboard = window.convertRectFromWindow(end, null)
47
+ const hidden =
48
+ keyboard.size.height <= 0 || keyboard.origin.y >= window.bounds.size.height
49
+ const frame = native.convertRectToView(native.bounds, null)
50
+ const bottom = frame.origin.y + frame.size.height - view.translateY
51
+ const overlap = hidden ? 0 : Math.max(0, bottom + offset - keyboard.origin.y)
52
+ if (overlap === current) return
53
+ current = overlap
54
+ stop(view, ['y'])
55
+ const apply = () => {
56
+ view.translateY = overlap ? -overlap : 0
57
+ }
58
+ const duration = info.objectForKey(UIKeyboardAnimationDurationUserInfoKey)
59
+ ?.doubleValue as number | undefined
60
+ if (!duration) return apply()
61
+ const curve =
62
+ (info.objectForKey(UIKeyboardAnimationCurveUserInfoKey)?.integerValue as
63
+ number | undefined) ?? 0
64
+ // The keyboard's curve is private; shifted into the options it still applies.
65
+ UIView.animateWithDurationDelayOptionsAnimationsCompletion(
66
+ duration,
67
+ 0,
68
+ ((curve << 16) | BEGIN_FROM_CURRENT_STATE) as UIViewAnimationOptions,
69
+ apply,
70
+ () => {},
71
+ )
72
+ }
73
+ const observer = Application.ios.addNotificationObserver(
74
+ UIKeyboardWillChangeFrameNotification,
75
+ onKeyboard,
76
+ )
77
+ const cleanup = () => {
78
+ if (disposed) return
79
+ disposed = true
80
+ Application.ios.removeNotificationObserver(
81
+ observer,
82
+ UIKeyboardWillChangeFrameNotification,
83
+ )
84
+ view.off('unloaded', cleanup)
85
+ if (current) view.translateY = 0
86
+ }
87
+ view.on('unloaded', cleanup)
88
+ return cleanup
89
+ }
90
+
91
+ function followAndroid(view: View, offset: number) {
92
+ if (android.os.Build.VERSION.SDK_INT < 30) return () => {}
93
+ const density = Utils.layout.getDisplayDensity()
94
+ let disposed = false
95
+ let follower: org.rafaelaugusto.jolt.KeyboardFollower | undefined
96
+ const attach = () => {
97
+ if (disposed || follower) return
98
+ const native = view.nativeViewProtected as android.view.View | undefined
99
+ if (!native) return
100
+ try {
101
+ follower = new org.rafaelaugusto.jolt.KeyboardFollower(
102
+ native,
103
+ offset * density,
104
+ new org.rafaelaugusto.jolt.KeyboardFollower.Listener({
105
+ onStart: () => stop(view, ['y']),
106
+ onSettled: (translation) => {
107
+ view.translateY = translation / density
108
+ },
109
+ }),
110
+ )
111
+ } catch {
112
+ // A JS-only update cannot install the native plugin. Leave the platform's resize in place.
113
+ return
114
+ }
115
+ follower.attach()
116
+ }
117
+ const cleanup = () => {
118
+ if (disposed) return
119
+ disposed = true
120
+ view.off('loaded', attach)
121
+ view.off('unloaded', cleanup)
122
+ if (follower) {
123
+ follower.detach()
124
+ follower = undefined
125
+ view.translateY = 0
126
+ }
127
+ }
128
+ view.on('loaded', attach)
129
+ view.on('unloaded', cleanup)
130
+ if (view.isLoaded) attach()
131
+ return cleanup
132
+ }
package/src/label.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type { Label } from '@nativescript/core'
2
+
3
+ const callbacks = new WeakMap<NSObject, (finished: boolean) => void>()
4
+ let Delegate: typeof NSObject | undefined
5
+ const styleEvents = [
6
+ 'fontInternalChange',
7
+ 'colorChange',
8
+ 'textAlignmentChange',
9
+ 'textDecorationChange',
10
+ 'textTransformChange',
11
+ 'letterSpacingChange',
12
+ 'lineHeightChange',
13
+ 'textShadowChange',
14
+ 'textStrokeChange',
15
+ 'whiteSpaceChange',
16
+ 'textOverflowChange',
17
+ 'maxLinesChange',
18
+ 'paddingLeftChange',
19
+ 'paddingRightChange',
20
+ 'paddingTopChange',
21
+ 'paddingBottomChange',
22
+ 'visibilityChange',
23
+ 'directionChange',
24
+ 'borderLeftWidthChange',
25
+ 'borderRightWidthChange',
26
+ 'borderTopWidthChange',
27
+ 'borderBottomWidthChange',
28
+ ]
29
+
30
+ /** Reports text, formatted text and style changes of a Label until the returned function is called. */
31
+ export function watchLabel(view: Label, onChange: () => void): () => void {
32
+ const formatted = view.formattedText
33
+ view.on('textChange', onChange)
34
+ view.on('formattedTextChange', onChange)
35
+ formatted?.on('propertyChange', onChange)
36
+ for (const event of styleEvents) view.style.on(event, onChange)
37
+ return () => {
38
+ view.off('textChange', onChange)
39
+ view.off('formattedTextChange', onChange)
40
+ formatted?.off('propertyChange', onChange)
41
+ for (const event of styleEvents) view.style.off(event, onChange)
42
+ }
43
+ }
44
+
45
+ /** A CAAnimationDelegate that reports when its animation stops. Release it with `forgetCompletion`. */
46
+ export function completion(
47
+ callback: (finished: boolean) => void,
48
+ ): NSObject & CAAnimationDelegate {
49
+ if (!Delegate) {
50
+ const base = NSObject as typeof NSObject & {
51
+ extend(
52
+ methods: object,
53
+ options: { protocols: unknown[]; name: string },
54
+ ): typeof NSObject
55
+ }
56
+ Delegate = base.extend(
57
+ {
58
+ animationDidStopFinished(
59
+ this: NSObject,
60
+ _animation: CAAnimation,
61
+ finished: boolean,
62
+ ) {
63
+ callbacks.get(this)?.(finished)
64
+ },
65
+ },
66
+ { protocols: [CAAnimationDelegate], name: 'JoltLabelAnimationDelegate' },
67
+ )
68
+ }
69
+ const delegate = Delegate.new()
70
+ callbacks.set(delegate, callback)
71
+ return delegate
72
+ }
73
+
74
+ export function forgetCompletion(delegate: NSObject) {
75
+ callbacks.delete(delegate)
76
+ }
package/src/liquid.ts CHANGED
@@ -3,6 +3,7 @@ import { tintColorProperty } from '@nativescript/core/ui/image'
3
3
  import { toView } from './animate.js'
4
4
  import { liquidGrid } from './liquid-grid.js'
5
5
  import type { Target } from './types.js'
6
+ import { reducedMotion } from './config.js'
6
7
 
7
8
  export interface LiquidOptions {
8
9
  /** Amount of internal deformation, from 0 to 1. Defaults to 0.7. */
@@ -134,7 +135,7 @@ class Liquid implements LiquidEffect {
134
135
  !this.image.isLoaded ||
135
136
  this.image.visibility !== 'visible' ||
136
137
  this.image.tintColor ||
137
- UIAccessibilityIsReduceMotionEnabled()
138
+ reducedMotion()
138
139
  )
139
140
  return this.remove()
140
141
  const native = this.image.nativeViewProtected as UIImageView | undefined
package/src/loader.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Application, Color, isIOS, type View } from '@nativescript/core'
2
2
  import { toView } from './animate.js'
3
3
  import type { Target } from './types.js'
4
+ import { reducedMotion } from './config.js'
4
5
 
5
6
  export type LoaderStyle = 'dots' | 'bars' | 'ring'
6
7
 
@@ -160,7 +161,7 @@ class Loader {
160
161
  0,
161
162
  )
162
163
  }
163
- const reduced = UIAccessibilityIsReduceMotionEnabled()
164
+ const reduced = reducedMotion()
164
165
  item.opacity = style === 'ring' ? 0.35 : 1
165
166
  if (reduced && this.animating) {
166
167
  item.removeAnimationForKey(key)
package/src/particles.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Application, Color, isIOS, type View } from '@nativescript/core'
2
2
  import { toView } from './animate.js'
3
3
  import type { Target } from './types.js'
4
+ import { reducedMotion } from './config.js'
4
5
 
5
6
  export interface ConfettiOptions {
6
7
  /** Approximate number of particles in the burst. Defaults to 80. */
@@ -78,8 +79,7 @@ function start(target: Target, settings: Settings, overlay: boolean): () => void
78
79
  const view = toView(target)
79
80
  const running = overlay ? bursts : emitters
80
81
  running.get(view)?.()
81
- if (!isIOS || settings.rate === 0 || UIAccessibilityIsReduceMotionEnabled())
82
- return () => {}
82
+ if (!isIOS || settings.rate === 0 || reducedMotion()) return () => {}
83
83
  return new Emitter(view, settings, overlay, running).stop
84
84
  }
85
85
 
@@ -191,12 +191,12 @@ class Emitter {
191
191
  }
192
192
 
193
193
  private motionChanged = () => {
194
- if (UIAccessibilityIsReduceMotionEnabled()) this.stop()
194
+ if (reducedMotion()) this.stop()
195
195
  }
196
196
 
197
197
  private update() {
198
198
  if (!this.view.isLoaded) return
199
- if (UIAccessibilityIsReduceMotionEnabled()) return this.stop()
199
+ if (reducedMotion()) return this.stop()
200
200
  const native = this.view.nativeViewProtected as UIView | undefined
201
201
  const window = native?.window
202
202
  if (this.layer && (!native || !window)) return this.stop()
package/src/path.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { parsePath, type MotionPath } from './svg-path.js'
2
+ import { reducedMotion, timeScale } from './config.js'
3
+ import { createTrack, instantTrack, type Track } from './track.js'
4
+ import {
5
+ mergeTransitions,
6
+ resolveTransition,
7
+ type Transition,
8
+ type ResolvedTransition,
9
+ } from './transition.js'
10
+ import type { Keyframes, KeyframeProperty } from './types.js'
11
+
12
+ export type PathProperty = 'x' | 'y' | 'rotate'
13
+ export interface PreparedPath {
14
+ readonly properties: readonly PathProperty[]
15
+ readonly starts: ReadonlyMap<PathProperty, number>
16
+ readonly transition: ResolvedTransition
17
+ track(property: PathProperty, offset: number, delay: number): Track
18
+ }
19
+
20
+ export function preparePath(
21
+ keyframes: Keyframes,
22
+ base: Transition,
23
+ overrides: Partial<Record<KeyframeProperty | 'path', Transition>>,
24
+ ): PreparedPath | undefined {
25
+ if (keyframes.orient !== undefined && typeof keyframes.orient !== 'boolean')
26
+ throw new TypeError('Path orient must be a boolean.')
27
+ if (keyframes.path === undefined) {
28
+ if (keyframes.orient !== undefined) throw new TypeError('orient requires a path.')
29
+ return undefined
30
+ }
31
+ if (keyframes.x !== undefined || keyframes.y !== undefined)
32
+ throw new TypeError('A path owns x and y; omit explicit x/y keyframes.')
33
+ if (keyframes.orient && keyframes.rotate !== undefined)
34
+ throw new TypeError('An oriented path owns rotate; omit explicit rotate keyframes.')
35
+ if (overrides.x || overrides.y || (keyframes.orient && overrides.rotate))
36
+ throw new TypeError('Use the path transition to time its coordinates together.')
37
+ const geometry = parsePath(keyframes.path)
38
+ const orient = keyframes.orient === true
39
+ const transition = resolveTransition(
40
+ overrides.path ? mergeTransitions(base, overrides.path) : base,
41
+ 'opacity',
42
+ 2,
43
+ )
44
+ if (
45
+ !Number.isFinite(transition.curve.type === 'tween' ? transition.curve.duration : 0) ||
46
+ !Number.isFinite(transition.delay)
47
+ )
48
+ throw new RangeError('Path duration and delay must be finite.')
49
+ const properties: readonly PathProperty[] = orient ? ['x', 'y', 'rotate'] : ['x', 'y']
50
+ const first = geometry.at(0)
51
+ let motion: ReturnType<typeof sample> | undefined
52
+ return {
53
+ properties,
54
+ starts: new Map(properties.map((property) => [property, first[property]])),
55
+ transition,
56
+ track(property, offset, requested) {
57
+ motion ??= sample(geometry, transition, orient)
58
+ const { progress, times, frames } = motion
59
+ const to = geometry.at(progress.to)[property] + offset
60
+ if (reducedMotion()) return instantTrack(property, to)
61
+ const delay = requested / timeScale()
62
+ const shift = delay - progress.delay
63
+ const pose = (time: number) =>
64
+ geometry.at(progress.valueAt(time - shift))[property] + offset
65
+ return {
66
+ ...progress,
67
+ property,
68
+ delay,
69
+ end: progress.end + shift,
70
+ arrival: progress.arrival + shift,
71
+ times,
72
+ values: offset
73
+ ? frames[property].map((value) => value + offset)
74
+ : frames[property],
75
+ to,
76
+ valueAt: pose,
77
+ velocityAt(time) {
78
+ if (time <= delay || time >= progress.end + shift) return 0
79
+ const position = progress.valueAt(time - shift)
80
+ if (position < 0 || position > 1) return 0
81
+ const epsilon = Math.min(1e-5, 0.01 / Math.max(1, geometry.length))
82
+ const a = Math.max(0, position - epsilon),
83
+ b = Math.min(1, position + epsilon)
84
+ return (
85
+ ((geometry.at(b)[property] - geometry.at(a)[property]) / (b - a)) *
86
+ progress.velocityAt(time - shift)
87
+ )
88
+ },
89
+ }
90
+ },
91
+ }
92
+ }
93
+
94
+ /** Sample scalar timing once, then project all coordinates through the same path. */
95
+ function sample(geometry: MotionPath, transition: ResolvedTransition, orient: boolean) {
96
+ const progress = createTrack('opacity', [0, 1], transition, 0)
97
+ const count = Math.max(
98
+ 1,
99
+ Math.min(
100
+ 6000,
101
+ Math.max(
102
+ Math.ceil((progress.duration / 1000) * 60),
103
+ Math.ceil(geometry.length / 2),
104
+ ),
105
+ ),
106
+ )
107
+ const times = Array.from({ length: count + 1 }, (_, i) => i / count)
108
+ const frames: Record<PathProperty, number[]> = { x: [], y: [], rotate: [] }
109
+ for (const time of times) {
110
+ const position =
111
+ time === 0
112
+ ? 0
113
+ : time === 1
114
+ ? 1
115
+ : progress.valueAt(progress.delay + time * progress.duration)
116
+ if (!Number.isFinite(position))
117
+ throw new RangeError('Path easing must return finite progress.')
118
+ const pose = geometry.at(position)
119
+ frames.x.push(pose.x)
120
+ frames.y.push(pose.y)
121
+ if (orient) frames.rotate.push(pose.rotate)
122
+ }
123
+ return { progress, times, frames }
124
+ }