@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
@@ -0,0 +1,73 @@
1
+ import { ScrollView, type EventData, type ScrollEventData } from '@nativescript/core'
2
+ import { animate, toView } from './animate.js'
3
+ import type { Keyframes, Target } from './types.js'
4
+
5
+ export interface ScrollLinkedOptions {
6
+ /** Scroll offsets in DIPs, mapped to progress 0 and 1. Either order is allowed. */
7
+ range: readonly [number, number]
8
+ /** Offset to read. Defaults to the vertical axis. */
9
+ axis?: 'x' | 'y'
10
+ }
11
+
12
+ /** Builds one paused animation and scrubs it synchronously on scroll. */
13
+ export function scrollLinked(
14
+ scrollView: Target,
15
+ target: Target,
16
+ keyframes: Keyframes,
17
+ options: ScrollLinkedOptions,
18
+ ): () => void {
19
+ const source = toView(scrollView)
20
+ if (!(source instanceof ScrollView))
21
+ throw new TypeError('scrollLinked requires a NativeScript ScrollView.')
22
+ const view = toView(target)
23
+ const range = options?.range
24
+ if (
25
+ !Array.isArray(range) ||
26
+ range.length !== 2 ||
27
+ !range.every(Number.isFinite) ||
28
+ !Number.isFinite(range[1]! - range[0]!) ||
29
+ range[0] === range[1]
30
+ )
31
+ throw new RangeError('Scroll range requires two distinct finite offsets in DIPs.')
32
+ const [start, end] = range as [number, number]
33
+ const axis = options.axis ?? 'y'
34
+ if (axis !== 'x' && axis !== 'y') throw new TypeError('Scroll axis must be "x" or "y".')
35
+
36
+ const animation = animate(view, keyframes, { duration: 1000, ease: 'linear' })
37
+ animation.pause()
38
+ let disposed = false
39
+ let previous: number | undefined
40
+
41
+ const update = (offset: number) => {
42
+ if (disposed || !Number.isFinite(offset)) return
43
+ const progress = Math.max(0, Math.min(1, (offset - start) / (end - start)))
44
+ if (progress === previous) return
45
+ animation.progress = progress
46
+ previous = progress
47
+ }
48
+ const current = () =>
49
+ update(axis === 'x' ? source.horizontalOffset : source.verticalOffset)
50
+ const onScroll = (event: EventData) => {
51
+ const scroll = event as ScrollEventData
52
+ update(axis === 'x' ? scroll.scrollX : scroll.scrollY)
53
+ }
54
+ const cleanup = () => {
55
+ if (disposed) return
56
+ disposed = true
57
+ source.off(ScrollView.scrollEvent, onScroll)
58
+ source.off('loaded', current)
59
+ source.off('unloaded', cleanup)
60
+ view.off('loaded', current)
61
+ view.off('unloaded', cleanup)
62
+ animation.stop()
63
+ }
64
+
65
+ source.on(ScrollView.scrollEvent, onScroll)
66
+ source.on('loaded', current)
67
+ source.on('unloaded', cleanup)
68
+ view.on('loaded', current)
69
+ view.on('unloaded', cleanup)
70
+ void animation.finished.then(cleanup)
71
+ current()
72
+ return cleanup
73
+ }
package/src/sequence.ts CHANGED
@@ -1,24 +1,36 @@
1
+ import { timeScale } from './config.js'
1
2
  import { getDriver } from './driver/index.js'
2
3
  import { isDecorationProperty, validateDecorationTarget } from './decoration.js'
3
4
  import type { View } from '@nativescript/core'
4
5
  import { animate, startingValues, toViews } from './animate.js'
5
- 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'
6
14
  import type { PresetName } from './presets.js'
7
15
  import { writeValue, type Property } from './properties.js'
8
16
  import type { SpringName } from './transition.js'
9
17
  import type { AnimateOptions, Keyframes, Target } from './types.js'
10
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
+
11
23
  export type Step =
12
24
  | readonly [
13
25
  target: Target | readonly Target[],
14
26
  animation: Keyframes | PresetName,
15
- options?: AnimateOptions | SpringName,
27
+ options?: SequenceOptions | SpringName,
16
28
  ]
17
29
  | (() => Animation)
18
30
 
19
31
  /**
20
- * Plays steps one after the other. A step starts once the previous one looks
21
- * 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.
22
34
  * Views in later steps take their first absolute keyframe right away, so a title
23
35
  * that fades in third doesn't show up before its turn. Relative and null keyframes
24
36
  * read the current value when their step starts.
@@ -26,26 +38,180 @@ export type Step =
26
38
  * ```ts
27
39
  * await sequence([
28
40
  * [logo, 'popIn'],
29
- * [title, 'fadeInUp'],
30
- * [buttons, 'fadeInUp', { stagger: 50 }],
41
+ * [title, 'fadeInUp', { at: '-150' }],
42
+ * [buttons, 'fadeInUp', { stagger: 50, at: '<' }],
31
43
  * ])
32
44
  * ```
33
45
  */
34
46
  export function sequence(steps: readonly Step[]): Animation {
35
- 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))
36
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
37
64
  const started: Animation[] = []
65
+ const cancellations = new Set<() => void>()
38
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
+ }
39
133
 
40
- const arrived = (async () => {
41
- for (const step of steps) {
42
- if (stopped) return
43
- const animation = typeof step === 'function' ? step() : animate(...step)
44
- started.push(animation)
45
- 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)
46
210
  }
47
- })()
211
+ }
212
+ if (pending === 0) resolveScheduled()
48
213
 
214
+ const arrived = scheduled.then(() => Promise.all(started.map(arrivalOf)))
49
215
  const finished = arrived.then(async () => {
50
216
  const results = await Promise.all(started.map((animation) => animation.finished))
51
217
  return !stopped && results.length === steps.length && results.every(Boolean)
@@ -54,7 +220,11 @@ export function sequence(steps: readonly Step[]): Animation {
54
220
  const animation: Animation = {
55
221
  finished,
56
222
  stop() {
223
+ if (stopped) return
57
224
  stopped = true
225
+ cancelSchedule()
226
+ states.forEach((state) => state.onStart.clear())
227
+ resolveScheduled()
58
228
  started.forEach((animation) => animation.stop())
59
229
  },
60
230
  then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
@@ -63,6 +233,21 @@ export function sequence(steps: readonly Step[]): Animation {
63
233
  return animation
64
234
  }
65
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
+
66
251
  function applyStartingValues(steps: readonly Step[]) {
67
252
  const claimed = new Map<View, Set<Property>>()
68
253
  for (const step of steps) {
package/src/shimmer.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Application, 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 ShimmerOptions {
6
7
  /** Milliseconds per sweep. Defaults to 1500. */
@@ -98,7 +99,7 @@ class Shimmer {
98
99
  UIAccessibilityReduceMotionStatusDidChangeNotification,
99
100
  this.update,
100
101
  )
101
- if (UIAccessibilityIsReduceMotionEnabled()) return this.remove()
102
+ if (reducedMotion()) return this.remove()
102
103
  const bounds = native.layer.bounds
103
104
  if (bounds.size.width <= 0 || bounds.size.height <= 0) return this.remove()
104
105
  if (this.native && this.native !== native) this.remove()