@rafaelaugusto/jolt 0.1.0-beta.12 → 0.1.0-beta.14

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 (151) hide show
  1. package/README.md +9 -6
  2. package/dist/animate.d.ts +6 -4
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +3 -1
  5. package/dist/animate.js.map +1 -1
  6. package/dist/drag.js +3 -3
  7. package/dist/drag.js.map +1 -1
  8. package/dist/driver/android-gradient.d.ts.map +1 -1
  9. package/dist/driver/android-gradient.js +11 -1
  10. package/dist/driver/android-gradient.js.map +1 -1
  11. package/dist/driver/android-particles.d.ts +6 -0
  12. package/dist/driver/android-particles.d.ts.map +1 -0
  13. package/dist/driver/android-particles.js +93 -0
  14. package/dist/driver/android-particles.js.map +1 -0
  15. package/dist/driver/android.d.ts.map +1 -1
  16. package/dist/driver/android.js +2 -0
  17. package/dist/driver/android.js.map +1 -1
  18. package/dist/driver/frame.d.ts +3 -0
  19. package/dist/driver/frame.d.ts.map +1 -0
  20. package/dist/driver/frame.js +7 -0
  21. package/dist/driver/frame.js.map +1 -0
  22. package/dist/driver/index.d.ts +2 -0
  23. package/dist/driver/index.d.ts.map +1 -1
  24. package/dist/driver/index.js.map +1 -1
  25. package/dist/driver/ios-gradient.d.ts.map +1 -1
  26. package/dist/driver/ios-gradient.js +24 -2
  27. package/dist/driver/ios-gradient.js.map +1 -1
  28. package/dist/driver/ios.d.ts.map +1 -1
  29. package/dist/driver/ios.js +2 -0
  30. package/dist/driver/ios.js.map +1 -1
  31. package/dist/engine.d.ts +2 -1
  32. package/dist/engine.d.ts.map +1 -1
  33. package/dist/engine.js +32 -5
  34. package/dist/engine.js.map +1 -1
  35. package/dist/gradient-binding.d.ts +38 -0
  36. package/dist/gradient-binding.d.ts.map +1 -0
  37. package/dist/gradient-binding.js +128 -0
  38. package/dist/gradient-binding.js.map +1 -0
  39. package/dist/gradient.d.ts +19 -4
  40. package/dist/gradient.d.ts.map +1 -1
  41. package/dist/gradient.js +331 -70
  42. package/dist/gradient.js.map +1 -1
  43. package/dist/haptics.d.ts.map +1 -1
  44. package/dist/haptics.js +101 -28
  45. package/dist/haptics.js.map +1 -1
  46. package/dist/image-effect.d.ts +1 -0
  47. package/dist/image-effect.d.ts.map +1 -1
  48. package/dist/image-effect.js +10 -2
  49. package/dist/image-effect.js.map +1 -1
  50. package/dist/index.d.ts +6 -2
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/motion-scope.d.ts +3 -2
  55. package/dist/motion-scope.d.ts.map +1 -1
  56. package/dist/motion-scope.js +12 -2
  57. package/dist/motion-scope.js.map +1 -1
  58. package/dist/motion-value.d.ts +62 -8
  59. package/dist/motion-value.d.ts.map +1 -1
  60. package/dist/motion-value.js +327 -91
  61. package/dist/motion-value.js.map +1 -1
  62. package/dist/native/index.d.ts +7 -0
  63. package/dist/native/index.d.ts.map +1 -0
  64. package/dist/native/index.js +4 -0
  65. package/dist/native/index.js.map +1 -0
  66. package/dist/native-properties.d.ts +28 -0
  67. package/dist/native-properties.d.ts.map +1 -0
  68. package/dist/native-properties.js +154 -0
  69. package/dist/native-properties.js.map +1 -0
  70. package/dist/native-property-driver.d.ts +19 -0
  71. package/dist/native-property-driver.d.ts.map +1 -0
  72. package/dist/native-property-driver.js +69 -0
  73. package/dist/native-property-driver.js.map +1 -0
  74. package/dist/native-property.d.ts +35 -0
  75. package/dist/native-property.d.ts.map +1 -0
  76. package/dist/native-property.js +277 -0
  77. package/dist/native-property.js.map +1 -0
  78. package/dist/particles.d.ts +5 -2
  79. package/dist/particles.d.ts.map +1 -1
  80. package/dist/particles.js +16 -10
  81. package/dist/particles.js.map +1 -1
  82. package/dist/playback-group.d.ts.map +1 -1
  83. package/dist/playback-group.js +17 -1
  84. package/dist/playback-group.js.map +1 -1
  85. package/dist/playback.d.ts +15 -0
  86. package/dist/playback.d.ts.map +1 -1
  87. package/dist/playback.js +23 -0
  88. package/dist/playback.js.map +1 -1
  89. package/dist/scene-appearance.d.ts +14 -0
  90. package/dist/scene-appearance.d.ts.map +1 -0
  91. package/dist/scene-appearance.js +68 -0
  92. package/dist/scene-appearance.js.map +1 -0
  93. package/dist/sequence.d.ts +24 -0
  94. package/dist/sequence.d.ts.map +1 -1
  95. package/dist/sequence.js +181 -19
  96. package/dist/sequence.js.map +1 -1
  97. package/dist/timed-animation.d.ts +38 -0
  98. package/dist/timed-animation.d.ts.map +1 -0
  99. package/dist/timed-animation.js +177 -0
  100. package/dist/timed-animation.js.map +1 -0
  101. package/dist/types.d.ts +1 -1
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/value-observers.d.ts +19 -0
  104. package/dist/value-observers.d.ts.map +1 -0
  105. package/dist/value-observers.js +111 -0
  106. package/dist/value-observers.js.map +1 -0
  107. package/dist/value-track.d.ts +13 -0
  108. package/dist/value-track.d.ts.map +1 -0
  109. package/dist/value-track.js +59 -0
  110. package/dist/value-track.js.map +1 -0
  111. package/dist/vue/index.d.ts.map +1 -1
  112. package/dist/vue/index.js +28 -5
  113. package/dist/vue/index.js.map +1 -1
  114. package/package.json +6 -2
  115. package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +30 -1
  116. package/platforms/android/java/org/rafaelaugusto/jolt/ParticleEmitter.java +192 -0
  117. package/platforms/android/java/org/rafaelaugusto/jolt/ParticleSimulation.java +114 -0
  118. package/platforms/android/proguard-rules.pro +2 -0
  119. package/src/android-plugin.d.ts +93 -0
  120. package/src/animate.ts +13 -7
  121. package/src/drag.ts +3 -3
  122. package/src/driver/android-gradient.ts +8 -1
  123. package/src/driver/android-particles.ts +118 -0
  124. package/src/driver/android.ts +2 -0
  125. package/src/driver/frame.ts +10 -0
  126. package/src/driver/index.ts +2 -0
  127. package/src/driver/ios-gradient.ts +24 -2
  128. package/src/driver/ios.ts +2 -0
  129. package/src/engine.ts +33 -6
  130. package/src/gradient-binding.ts +158 -0
  131. package/src/gradient.ts +376 -91
  132. package/src/haptics.ts +106 -28
  133. package/src/image-effect.ts +10 -2
  134. package/src/index.ts +6 -0
  135. package/src/ios-plugin.d.ts +40 -0
  136. package/src/motion-scope.ts +14 -3
  137. package/src/motion-value.ts +374 -99
  138. package/src/native/index.ts +23 -0
  139. package/src/native-properties.ts +188 -0
  140. package/src/native-property-driver.ts +123 -0
  141. package/src/native-property.ts +353 -0
  142. package/src/particles.ts +18 -17
  143. package/src/playback-group.ts +17 -1
  144. package/src/playback.ts +41 -0
  145. package/src/scene-appearance.ts +92 -0
  146. package/src/sequence.ts +209 -20
  147. package/src/timed-animation.ts +192 -0
  148. package/src/types.ts +1 -1
  149. package/src/value-observers.ts +133 -0
  150. package/src/value-track.ts +71 -0
  151. package/src/vue/index.ts +29 -5
package/src/haptics.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isIOS, type View } from '@nativescript/core'
1
+ import { isAndroid, isIOS, type View } from '@nativescript/core'
2
2
  import type { Driver } from './driver/index.js'
3
3
  import type { Track } from './track.js'
4
4
 
@@ -39,7 +39,69 @@ export function resolveHaptic(haptic: Haptic | undefined): HapticOptions | undef
39
39
 
40
40
  const PREPARE_AHEAD = 100
41
41
  const MAX_LATENESS = 100
42
- const COMPLETION_TOLERANCE = 20
42
+
43
+ interface Feedback {
44
+ active(): boolean
45
+ enabled(): boolean
46
+ prepare?(): void
47
+ fire(): void
48
+ }
49
+
50
+ function feedbackFor(
51
+ view: View,
52
+ style: HapticStyle,
53
+ driver: Driver,
54
+ ): Feedback | undefined {
55
+ if (isAndroid) {
56
+ if (typeof android === 'undefined' || driver.reducedMotion?.()) return
57
+ const native = view.nativeViewProtected as android.view.View | undefined
58
+ if (!native) return
59
+ const sdk = android.os.Build.VERSION.SDK_INT
60
+ const constants = android.view.HapticFeedbackConstants
61
+ const effect =
62
+ style === 'heavy'
63
+ ? constants.LONG_PRESS
64
+ : style === 'medium' && sdk >= 23
65
+ ? constants.CONTEXT_CLICK
66
+ : style === 'light' && sdk >= 21
67
+ ? constants.CLOCK_TICK
68
+ : constants.KEYBOARD_TAP
69
+ return {
70
+ // Animator scale zero must also silence opacity/color feedback when
71
+ // configure({ reducedMotion: 'never' }) keeps a logical track alive.
72
+ enabled: () => !driver.reducedMotion?.(),
73
+ active: () =>
74
+ view.isLoaded &&
75
+ view.nativeViewProtected === native &&
76
+ !!native.getWindowToken() &&
77
+ native.getWindowVisibility() === 0 &&
78
+ native.isShown() &&
79
+ native.hasWindowFocus(),
80
+ // No ignore-setting flags, forced vibration or permission. A false result
81
+ // means the system declined this request; do not retry with a stronger effect.
82
+ fire: () => {
83
+ native.performHapticFeedback(effect)
84
+ },
85
+ }
86
+ }
87
+ if (!isIOS || typeof UIImpactFeedbackGenerator === 'undefined') return
88
+ const styles = { light: 0, medium: 1, heavy: 2 } satisfies Record<
89
+ HapticStyle,
90
+ UIImpactFeedbackStyle
91
+ >
92
+ const generator = UIImpactFeedbackGenerator.alloc().initWithStyle(styles[style])
93
+ const native = view.nativeViewProtected as UIView
94
+ return {
95
+ enabled: () => true,
96
+ active: () =>
97
+ view.isLoaded &&
98
+ view.nativeViewProtected === native &&
99
+ !!native.window &&
100
+ UIApplication.sharedApplication.applicationState === 0,
101
+ prepare: () => generator.prepare(),
102
+ fire: () => generator.impactOccurred(),
103
+ }
104
+ }
43
105
 
44
106
  /** Discrete feedback only: animation playback remains entirely native. */
45
107
  export function playHaptics(
@@ -50,7 +112,7 @@ export function playHaptics(
50
112
  startedAt: number,
51
113
  elapsedTime = () => driver.now() - startedAt,
52
114
  ) {
53
- if (!isIOS || !options || typeof UIImpactFeedbackGenerator === 'undefined') return
115
+ if (!options) return
54
116
  const moving = tracks.filter(
55
117
  (track) =>
56
118
  track.duration > 0 && track.values.some((value) => value !== track.values[0]),
@@ -62,25 +124,13 @@ export function playHaptics(
62
124
  const offsets = options.times
63
125
  ? options.times.map((time) => delay + (end - delay) * time)
64
126
  : [Math.max(...tracks.map((track) => track.arrival))]
65
- const styles = {
66
- light: 0,
67
- medium: 1,
68
- heavy: 2,
69
- } satisfies Record<HapticStyle, UIImpactFeedbackStyle>
70
- let generator: UIImpactFeedbackGenerator | undefined =
71
- UIImpactFeedbackGenerator.alloc().initWithStyle(styles[options.style])
72
- const native = view.nativeViewProtected as UIView
127
+ let feedback = feedbackFor(view, options.style, driver)
128
+ if (!feedback) return
73
129
  let paused = false
74
130
  let next = 0
75
131
  let cancelPrepare: (() => void) | undefined
76
132
  let cancelImpact: (() => void) | undefined
77
133
 
78
- const active = () =>
79
- view.isLoaded &&
80
- view.nativeViewProtected === native &&
81
- !!native.window &&
82
- UIApplication.sharedApplication.applicationState === 0 // UIApplicationState.Active
83
-
84
134
  const cancelTimers = () => {
85
135
  cancelPrepare?.()
86
136
  cancelImpact?.()
@@ -90,28 +140,54 @@ export function playHaptics(
90
140
  cancelPrepare?.()
91
141
  cancelImpact?.()
92
142
  cancelPrepare = cancelImpact = undefined
93
- generator = undefined
143
+ feedback = undefined
94
144
  view.off('unloaded', cancel)
95
145
  }
96
146
 
97
- const fire = (tolerance = 0) => {
98
- const elapsed = elapsedTime()
147
+ const fire = (elapsed = elapsedTime()) => {
99
148
  // A busy UI thread can delay timers. Drop stale impacts instead of replaying
100
149
  // several old taps at once when it becomes responsive again.
101
- while (next < offsets.length && offsets[next]! <= elapsed + tolerance) {
150
+ while (next < offsets.length && offsets[next]! <= elapsed) {
102
151
  const offset = offsets[next++]!
103
- if (active() && elapsed - offset <= MAX_LATENESS) generator?.impactOccurred()
152
+ if (feedback?.enabled() && feedback.active() && elapsed - offset <= MAX_LATENESS)
153
+ feedback.fire()
104
154
  }
105
155
  }
106
156
 
107
157
  const schedule = () => {
108
158
  if (paused) return
109
- if (!generator || next === offsets.length) return cancel()
159
+ if (!feedback?.enabled() || next === offsets.length) return cancel()
160
+ if (isAndroid) {
161
+ // Start pending markers together with the visual animators. Chaining a
162
+ // new ValueAnimator from each impact adds another first-frame startup
163
+ // delay, which can wrongly make subsequent points stale at fast scales.
164
+ const markers = new Set<() => void>()
165
+ cancelImpact = () => {
166
+ for (const release of markers) release()
167
+ markers.clear()
168
+ }
169
+ const arm = (index: number) => {
170
+ if (!feedback?.enabled()) return cancel()
171
+ // Native durations are integer milliseconds; zero completes synchronously.
172
+ const remaining = Math.max(1, offsets[index]! - elapsedTime())
173
+ const release = driver.after(remaining, () => {
174
+ markers.delete(release)
175
+ if (!feedback || paused) return
176
+ if (!feedback.enabled()) return cancel()
177
+ fire()
178
+ if (next === offsets.length) cancel()
179
+ else if (next <= index) arm(index)
180
+ })
181
+ markers.add(release)
182
+ }
183
+ for (let index = next; index < offsets.length; index++) arm(index)
184
+ return
185
+ }
110
186
  const remaining = offsets[next]! - elapsedTime()
111
- if (active()) generator.prepare()
112
- if (remaining > PREPARE_AHEAD) {
187
+ if (feedback.active()) feedback.prepare?.()
188
+ if (feedback.prepare && remaining > PREPARE_AHEAD) {
113
189
  cancelPrepare = driver.after(remaining - PREPARE_AHEAD, () => {
114
- if (active()) generator?.prepare()
190
+ if (feedback?.active()) feedback.prepare?.()
115
191
  })
116
192
  }
117
193
  cancelImpact = driver.after(Math.max(0, remaining), () => {
@@ -140,8 +216,10 @@ export function playHaptics(
140
216
  schedule()
141
217
  },
142
218
  finish() {
143
- // Native completion may beat the timer at the final keyframe by a frame.
144
- fire(COMPLETION_TOLERANCE)
219
+ // Native completion proves that the final frame was reached, even when
220
+ // a scaled/resumed clock sample is one frame behind. Earlier impacts are
221
+ // still judged against the end position, so stale points are not replayed.
222
+ fire(Math.max(end, elapsedTime()))
145
223
  cancel()
146
224
  },
147
225
  }
@@ -43,6 +43,7 @@ export interface ImageEffectDiagnostics {
43
43
  export interface ImageEffect extends Animation {
44
44
  readonly effect: ImageEffectName
45
45
  readonly paused: boolean
46
+ readonly duration: number
46
47
  /** Finite progress; distortion rejects seeking because it follows native touch input. */
47
48
  progress: number
48
49
  readonly diagnostics: Readonly<ImageEffectDiagnostics>
@@ -157,7 +158,7 @@ class Effect implements ImageEffect {
157
158
  for (const event of ['loaded', 'layoutChanged']) image.on(event, this.schedule)
158
159
  for (const event of ['imageSourceChange', 'stretchChange'])
159
160
  image.on(event, this.changed)
160
- image.on('unloaded', this.stop)
161
+ image.on('unloaded', this.unloaded)
161
162
  image.style.on('visibilityChange', this.visible)
162
163
  for (const event of surfaceChanges) image.style.on(event, this.changed)
163
164
  Application.on(Application.suspendEvent, this.suspend)
@@ -178,6 +179,9 @@ class Effect implements ImageEffect {
178
179
  get paused() {
179
180
  return this.userPaused
180
181
  }
182
+ get duration() {
183
+ return this.effect === 'distortion' ? Infinity : this.options.duration
184
+ }
181
185
  get progress() {
182
186
  return this.renderer?.progress ?? this.position
183
187
  }
@@ -240,7 +244,7 @@ class Effect implements ImageEffect {
240
244
  for (const event of ['loaded', 'layoutChanged']) this.image.off(event, this.schedule)
241
245
  for (const event of ['imageSourceChange', 'stretchChange'])
242
246
  this.image.off(event, this.changed)
243
- this.image.off('unloaded', this.stop)
247
+ this.image.off('unloaded', this.unloaded)
244
248
  this.image.style.off('visibilityChange', this.visible)
245
249
  for (const event of surfaceChanges) this.image.style.off(event, this.changed)
246
250
  Application.off(Application.suspendEvent, this.suspend)
@@ -272,6 +276,10 @@ class Effect implements ImageEffect {
272
276
  this.suspended = true
273
277
  this.renderer?.pause()
274
278
  }
279
+ private unloaded = () => {
280
+ if (Application.suspended && this.userPaused) return
281
+ this.stop()
282
+ }
275
283
  private resume = () => {
276
284
  this.suspended = false
277
285
  this.schedule()
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ export { timeline } from './sequence.js'
2
+ export type { TimelineAnimation, TimelineStep, PlayableAnimation } from './sequence.js'
3
+ export { playbackProgress } from './motion-value.js'
1
4
  export { animate, decay, set, stop } from './animate.js'
2
5
  export { bind, motionValue, scrollValue } from './motion-value.js'
3
6
  export type {
@@ -7,7 +10,9 @@ export type {
7
10
  MapOptions,
8
11
  MotionValue,
9
12
  ScrollValue,
13
+ ValueAnimationOptions,
10
14
  } from './motion-value.js'
15
+ export type { SubscribeOptions, ValueListener } from './value-observers.js'
11
16
  export { sequence } from './sequence.js'
12
17
  export type { Step, SequenceOptions, SequencePosition } from './sequence.js'
13
18
  export { press } from './press.js'
@@ -64,6 +69,7 @@ export type {
64
69
  GradientOptions,
65
70
  GradientType,
66
71
  GradientValues,
72
+ GradientBindings,
67
73
  } from './gradient.js'
68
74
  export { configure, onReducedMotion, reducedMotion } from './config.js'
69
75
  export type { JoltConfig } from './config.js'
@@ -79,3 +79,43 @@ declare class JoltPhotoZoom extends NSObject {
79
79
  layout(): void
80
80
  stop(): void
81
81
  }
82
+
83
+ declare class JoltPropertySink extends NSObject {
84
+ static scalarTargetKeyPath(target: NSObject, keyPath: string): JoltPropertySink
85
+ readonly dimensions: number
86
+ readonly alive: boolean
87
+ readonly compiled: boolean
88
+ read(): NSArray<number>
89
+ write(values: NSArray<number>): void
90
+ dispose(): void
91
+ }
92
+ declare class JoltPropertyPlayer extends NSObject {
93
+ static startSinkTimesValuesFinalValueDurationDelayRepeatReverseCompletion(
94
+ sink: JoltPropertySink,
95
+ times: NSArray<number>,
96
+ values: NSArray<number>,
97
+ finalValue: NSArray<number>,
98
+ duration: number,
99
+ delay: number,
100
+ repeat: number,
101
+ reverse: boolean,
102
+ completion: (completed: boolean) => void,
103
+ ): JoltPropertyPlayer
104
+ readonly elapsed: number
105
+ readonly frames: number
106
+ static activeCount(): number
107
+ controlPaused(time: number, paused: boolean): void
108
+ cancel(): void
109
+ }
110
+ declare class JoltReferenceProperties extends NSObject {
111
+ static mapCoordinate(target: NSObject): JoltPropertySink
112
+ static mapCamera(target: NSObject): JoltPropertySink
113
+ static scenePose(target: NSObject): JoltPropertySink
114
+ static sceneLightIntensity(target: NSObject): JoltPropertySink
115
+ static sceneLightColor(target: NSObject): JoltPropertySink
116
+ static sceneMaterialTransparency(target: NSObject): JoltPropertySink
117
+ static sceneMaterialDiffuse(target: NSObject): JoltPropertySink
118
+ static sceneMaterialEmission(target: NSObject): JoltPropertySink
119
+ static sceneAppearanceSinkCount(): number
120
+ static sceneAppearanceWriteCount(): number
121
+ }
@@ -4,7 +4,7 @@ import { drag } from './drag.js'
4
4
  import { gradient } from './gradient.js'
5
5
  import { animateLayout, type LayoutOptions } from './layout.js'
6
6
  import type { SpringName } from './transition.js'
7
- import { sequence } from './sequence.js'
7
+ import { sequence, timeline } from './sequence.js'
8
8
  import {
9
9
  motionStates,
10
10
  type MotionState,
@@ -38,9 +38,10 @@ export interface MotionScope {
38
38
  animate: typeof animate
39
39
  decay: typeof decay
40
40
  sequence: typeof sequence
41
+ timeline: typeof timeline
41
42
  press: typeof press
42
43
  drag: typeof drag
43
- /** Removed with the scope; suspending it stops a gradient animation where it is. */
44
+ /** Removed with the scope; suspension holds bindings until resume and stops concrete playback. */
44
45
  gradient: typeof gradient
45
46
  states<
46
47
  T extends Record<string, Target | null>,
@@ -197,6 +198,9 @@ export function motionScope(): MotionScope {
197
198
  decay(...args) {
198
199
  return startOwned(() => decay(...args))
199
200
  },
201
+ timeline(...args) {
202
+ return startOwned(() => timeline(...args))
203
+ },
200
204
  sequence(...args) {
201
205
  return startOwned(() => sequence(...args))
202
206
  },
@@ -209,7 +213,14 @@ export function motionScope(): MotionScope {
209
213
  gradient(target, options) {
210
214
  assertActive()
211
215
  const made = gradient(target, options)
212
- scope.add({ pause: () => made.stop(), play() {}, dispose: () => made.remove() })
216
+ scope.add({
217
+ get paused() {
218
+ return made.suspended
219
+ },
220
+ pause: () => made.suspend(),
221
+ play: () => made.resume(),
222
+ dispose: () => made.remove(),
223
+ })
213
224
  return made
214
225
  },
215
226
  states(targets, definitions, options) {