@rafaelaugusto/jolt 0.1.0-beta.5 → 0.1.0-beta.7

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 (184) hide show
  1. package/README.md +122 -5
  2. package/dist/animate.d.ts +4 -2
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +48 -11
  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/driver/android.d.ts.map +1 -1
  11. package/dist/driver/android.js +7 -0
  12. package/dist/driver/android.js.map +1 -1
  13. package/dist/driver/index.d.ts +2 -0
  14. package/dist/driver/index.d.ts.map +1 -1
  15. package/dist/driver/index.js.map +1 -1
  16. package/dist/driver/ios-color.js +1 -1
  17. package/dist/driver/ios-color.js.map +1 -1
  18. package/dist/driver/ios-decoration.js +1 -1
  19. package/dist/driver/ios-decoration.js.map +1 -1
  20. package/dist/driver/ios-playback.d.ts +1 -1
  21. package/dist/driver/ios-playback.d.ts.map +1 -1
  22. package/dist/driver/ios-playback.js +14 -0
  23. package/dist/driver/ios-playback.js.map +1 -1
  24. package/dist/driver/ios.d.ts.map +1 -1
  25. package/dist/driver/ios.js +1 -0
  26. package/dist/driver/ios.js.map +1 -1
  27. package/dist/engine.d.ts +16 -1
  28. package/dist/engine.d.ts.map +1 -1
  29. package/dist/engine.js +33 -1
  30. package/dist/engine.js.map +1 -1
  31. package/dist/image-effect-native.d.ts +26 -0
  32. package/dist/image-effect-native.d.ts.map +1 -0
  33. package/dist/image-effect-native.js +73 -0
  34. package/dist/image-effect-native.js.map +1 -0
  35. package/dist/image-effect.d.ts +50 -0
  36. package/dist/image-effect.d.ts.map +1 -0
  37. package/dist/image-effect.js +290 -0
  38. package/dist/image-effect.js.map +1 -0
  39. package/dist/image-owner.d.ts +3 -0
  40. package/dist/image-owner.d.ts.map +1 -0
  41. package/dist/image-owner.js +11 -0
  42. package/dist/image-owner.js.map +1 -0
  43. package/dist/index.d.ts +15 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +7 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/items.d.ts +20 -0
  48. package/dist/items.d.ts.map +1 -0
  49. package/dist/items.js +109 -0
  50. package/dist/items.js.map +1 -0
  51. package/dist/keyboard.d.ts +17 -0
  52. package/dist/keyboard.d.ts.map +1 -0
  53. package/dist/keyboard.js +116 -0
  54. package/dist/keyboard.js.map +1 -0
  55. package/dist/label.d.ts +7 -0
  56. package/dist/label.d.ts.map +1 -0
  57. package/dist/label.js +60 -0
  58. package/dist/label.js.map +1 -0
  59. package/dist/liquid.d.ts.map +1 -1
  60. package/dist/liquid.js +6 -1
  61. package/dist/liquid.js.map +1 -1
  62. package/dist/loader.d.ts.map +1 -1
  63. package/dist/loader.js +2 -1
  64. package/dist/loader.js.map +1 -1
  65. package/dist/particles.d.ts.map +1 -1
  66. package/dist/particles.js +4 -3
  67. package/dist/particles.js.map +1 -1
  68. package/dist/path.d.ts +12 -0
  69. package/dist/path.d.ts.map +1 -0
  70. package/dist/path.js +90 -0
  71. package/dist/path.js.map +1 -0
  72. package/dist/playback-group.d.ts.map +1 -1
  73. package/dist/playback-group.js +29 -1
  74. package/dist/playback-group.js.map +1 -1
  75. package/dist/press.d.ts.map +1 -1
  76. package/dist/press.js +3 -3
  77. package/dist/press.js.map +1 -1
  78. package/dist/reveal.d.ts.map +1 -1
  79. package/dist/reveal.js +3 -2
  80. package/dist/reveal.js.map +1 -1
  81. package/dist/roll-diff.d.ts +22 -0
  82. package/dist/roll-diff.d.ts.map +1 -0
  83. package/dist/roll-diff.js +36 -0
  84. package/dist/roll-diff.js.map +1 -0
  85. package/dist/roll.d.ts +29 -0
  86. package/dist/roll.d.ts.map +1 -0
  87. package/dist/roll.js +442 -0
  88. package/dist/roll.js.map +1 -0
  89. package/dist/scroll-linked.d.ts +10 -0
  90. package/dist/scroll-linked.d.ts.map +1 -0
  91. package/dist/scroll-linked.js +58 -0
  92. package/dist/scroll-linked.js.map +1 -0
  93. package/dist/sequence.d.ts +10 -5
  94. package/dist/sequence.d.ts.map +1 -1
  95. package/dist/sequence.js +202 -14
  96. package/dist/sequence.js.map +1 -1
  97. package/dist/shimmer.d.ts.map +1 -1
  98. package/dist/shimmer.js +2 -1
  99. package/dist/shimmer.js.map +1 -1
  100. package/dist/svg-path.d.ts +14 -0
  101. package/dist/svg-path.d.ts.map +1 -0
  102. package/dist/svg-path.js +281 -0
  103. package/dist/svg-path.js.map +1 -0
  104. package/dist/symbol-effect.d.ts.map +1 -1
  105. package/dist/symbol-effect.js +4 -3
  106. package/dist/symbol-effect.js.map +1 -1
  107. package/dist/text-layout.d.ts +8 -0
  108. package/dist/text-layout.d.ts.map +1 -1
  109. package/dist/text-layout.js +8 -2
  110. package/dist/text-layout.js.map +1 -1
  111. package/dist/text.d.ts +1 -1
  112. package/dist/text.d.ts.map +1 -1
  113. package/dist/text.js +60 -64
  114. package/dist/text.js.map +1 -1
  115. package/dist/track.d.ts +2 -0
  116. package/dist/track.d.ts.map +1 -1
  117. package/dist/track.js +62 -15
  118. package/dist/track.js.map +1 -1
  119. package/dist/types.d.ts +7 -1
  120. package/dist/types.d.ts.map +1 -1
  121. package/dist/vue/index.d.ts +89 -0
  122. package/dist/vue/index.d.ts.map +1 -0
  123. package/dist/vue/index.js +134 -0
  124. package/dist/vue/index.js.map +1 -0
  125. package/dist/zoom-native.d.ts +30 -0
  126. package/dist/zoom-native.d.ts.map +1 -0
  127. package/dist/zoom-native.js +56 -0
  128. package/dist/zoom-native.js.map +1 -0
  129. package/dist/zoomable.d.ts +48 -0
  130. package/dist/zoomable.d.ts.map +1 -0
  131. package/dist/zoomable.js +249 -0
  132. package/dist/zoomable.js.map +1 -0
  133. package/package.json +16 -3
  134. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  135. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  136. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  137. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  138. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  139. package/platforms/android/proguard-rules.pro +10 -0
  140. package/platforms/ios/build.xcconfig +1 -0
  141. package/platforms/ios/src/JoltImageEffect.h +25 -0
  142. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  143. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  144. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  145. package/platforms/ios/src/module.modulemap +5 -0
  146. package/src/android-plugin.d.ts +142 -0
  147. package/src/animate.ts +80 -30
  148. package/src/config.ts +46 -0
  149. package/src/driver/android.ts +14 -0
  150. package/src/driver/index.ts +2 -0
  151. package/src/driver/ios-color.ts +1 -1
  152. package/src/driver/ios-decoration.ts +1 -1
  153. package/src/driver/ios-playback.ts +15 -1
  154. package/src/driver/ios.ts +2 -0
  155. package/src/engine.ts +47 -2
  156. package/src/image-effect-native.ts +129 -0
  157. package/src/image-effect.ts +336 -0
  158. package/src/image-owner.ts +12 -0
  159. package/src/index.ts +20 -1
  160. package/src/ios-plugin.d.ts +59 -0
  161. package/src/items.ts +140 -0
  162. package/src/keyboard.ts +132 -0
  163. package/src/label.ts +76 -0
  164. package/src/liquid.ts +6 -1
  165. package/src/loader.ts +2 -1
  166. package/src/particles.ts +4 -4
  167. package/src/path.ts +124 -0
  168. package/src/playback-group.ts +34 -1
  169. package/src/press.ts +3 -5
  170. package/src/reveal.ts +3 -3
  171. package/src/roll-diff.ts +57 -0
  172. package/src/roll.ts +515 -0
  173. package/src/scroll-linked.ts +73 -0
  174. package/src/sequence.ts +199 -14
  175. package/src/shimmer.ts +2 -1
  176. package/src/svg-path.ts +355 -0
  177. package/src/symbol-effect.ts +4 -3
  178. package/src/text-layout.ts +14 -2
  179. package/src/text.ts +62 -81
  180. package/src/track.ts +89 -24
  181. package/src/types.ts +8 -1
  182. package/src/vue/index.ts +189 -0
  183. package/src/zoom-native.ts +124 -0
  184. package/src/zoomable.ts +330 -0
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'
@@ -9,18 +15,31 @@ export { shimmer } from './shimmer.js'
9
15
  export type { ShimmerOptions } from './shimmer.js'
10
16
  export { liquid } from './liquid.js'
11
17
  export type { LiquidOptions, LiquidEffect } from './liquid.js'
18
+ export { imageEffect } from './image-effect.js'
19
+ export type {
20
+ ImageEffect,
21
+ ImageEffectName,
22
+ ImageEffectOptions,
23
+ ImageEffectDiagnostics,
24
+ } from './image-effect.js'
25
+ export { zoomable } from './zoomable.js'
26
+ export type { Zoomable, ZoomableOptions, ZoomState, PhotoDismissal } from './zoomable.js'
12
27
  export { loader } from './loader.js'
13
28
  export type { LoaderOptions, LoaderStyle } from './loader.js'
14
29
  export { reveal, conceal } from './reveal.js'
15
30
  export type { RevealOptions, ConcealOptions } from './reveal.js'
16
31
  export { animateText } from './text.js'
17
32
  export type { TextOptions } from './text.js'
33
+ export { rollTo } from './roll.js'
34
+ export type { RollOptions } from './roll.js'
18
35
  export { symbolEffect } from './symbol-effect.js'
19
36
  export type {
20
37
  SymbolEffect,
21
38
  SymbolEffectName,
22
39
  SymbolEffectOptions,
23
40
  } from './symbol-effect.js'
41
+ export { configure } from './config.js'
42
+ export type { JoltConfig } from './config.js'
24
43
  export { definePreset, presets } from './presets.js'
25
44
  export type { Preset, PresetName, Presets } from './presets.js'
26
45
  export { springs } from './transition.js'
@@ -0,0 +1,59 @@
1
+ declare class JoltImageEffect extends NSObject {
2
+ stopPreservingImage(): void
3
+ static createWithViewDestinationModeOriginXOriginYAmplitudeRadiusDurationSeedListener(
4
+ image: UIImageView,
5
+ destination: UIImage | null,
6
+ mode: number,
7
+ x: number,
8
+ y: number,
9
+ amplitude: number,
10
+ radius: number,
11
+ duration: number,
12
+ seed: number,
13
+ listener: (completed: boolean, progress: number) => void,
14
+ ): JoltImageEffect | null
15
+ readonly progress: number
16
+ readonly paused: boolean
17
+ readonly frames: number
18
+ readonly longestFrame: number
19
+ readonly textureBytes: number
20
+ play(endpoint: number): void
21
+ pause(): void
22
+ resume(): void
23
+ seek(progress: number): void
24
+ finish(endpoint: number): void
25
+ stop(): void
26
+ }
27
+ declare class JoltPhotoZoom extends NSObject {
28
+ static attachImageViewportMinimumMaximumDoubleTapDismissReducedBackdropChromeOnDismissOnSettled(
29
+ image: UIImageView,
30
+ viewport: UIView,
31
+ minimum: number,
32
+ maximum: number,
33
+ doubleTap: number,
34
+ dismiss: boolean,
35
+ reduced: boolean,
36
+ backdrop: UIView | null,
37
+ chrome: UIView | null,
38
+ onDismiss: (
39
+ x: number,
40
+ y: number,
41
+ scale: number,
42
+ velocity: number,
43
+ progress: number,
44
+ ) => void,
45
+ onSettled: (x: number, y: number, scale: number) => void,
46
+ ): JoltPhotoZoom
47
+ readonly x: number
48
+ readonly y: number
49
+ readonly scale: number
50
+ readonly dismissProgress: number
51
+ readonly dismissPending: boolean
52
+ zoomFocusXFocusYAnimated(scale: number, x: number, y: number, animated: boolean): void
53
+ reset(animated: boolean): void
54
+ requestDismiss(): void
55
+ setReduced(value: boolean): void
56
+ setActive(value: boolean): void
57
+ layout(): void
58
+ stop(): void
59
+ }
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,8 @@ 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'
7
+ import { ownImage } from './image-owner.js'
6
8
 
7
9
  export interface LiquidOptions {
8
10
  /** Amount of internal deformation, from 0 to 1. Defaults to 0.7. */
@@ -49,6 +51,7 @@ export function liquid(target: Target, options: LiquidOptions = {}): LiquidEffec
49
51
  }
50
52
 
51
53
  class Liquid implements LiquidEffect {
54
+ private releaseOwnership: (() => void) | undefined
52
55
  private stopped = false
53
56
  private queued = false
54
57
  private pausedByUser = false
@@ -68,6 +71,7 @@ class Liquid implements LiquidEffect {
68
71
  private readonly image: Image,
69
72
  private readonly settings: Required<LiquidOptions>,
70
73
  ) {
74
+ this.releaseOwnership = ownImage(image, this.stop)
71
75
  running.set(image, this)
72
76
  for (const event of ['loaded', 'layoutChanged', 'imageSourceChange', 'stretchChange'])
73
77
  image.on(event, this.schedule)
@@ -92,6 +96,7 @@ class Liquid implements LiquidEffect {
92
96
  stop = () => {
93
97
  if (this.stopped) return
94
98
  this.stopped = true
99
+ this.releaseOwnership?.()
95
100
  for (const event of ['loaded', 'layoutChanged', 'imageSourceChange', 'stretchChange'])
96
101
  this.image.off(event, this.schedule)
97
102
  this.image.off('unloaded', this.unloaded)
@@ -134,7 +139,7 @@ class Liquid implements LiquidEffect {
134
139
  !this.image.isLoaded ||
135
140
  this.image.visibility !== 'visible' ||
136
141
  this.image.tintColor ||
137
- UIAccessibilityIsReduceMotionEnabled()
142
+ reducedMotion()
138
143
  )
139
144
  return this.remove()
140
145
  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
+ }