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

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 (91) hide show
  1. package/README.md +11 -4
  2. package/dist/drag.js +1 -1
  3. package/dist/drag.js.map +1 -1
  4. package/dist/driver/android.d.ts.map +1 -1
  5. package/dist/driver/android.js +2 -0
  6. package/dist/driver/android.js.map +1 -1
  7. package/dist/driver/ios-scroll-delegate.d.ts +4 -0
  8. package/dist/driver/ios-scroll-delegate.d.ts.map +1 -0
  9. package/dist/driver/ios-scroll-delegate.js +26 -0
  10. package/dist/driver/ios-scroll-delegate.js.map +1 -0
  11. package/dist/driver/ios.d.ts.map +1 -1
  12. package/dist/driver/ios.js +2 -0
  13. package/dist/driver/ios.js.map +1 -1
  14. package/dist/gesture-native.d.ts +27 -0
  15. package/dist/gesture-native.d.ts.map +1 -0
  16. package/dist/gesture-native.js +55 -0
  17. package/dist/gesture-native.js.map +1 -0
  18. package/dist/gestures/index.d.ts +65 -0
  19. package/dist/gestures/index.d.ts.map +1 -0
  20. package/dist/gestures/index.js +251 -0
  21. package/dist/gestures/index.js.map +1 -0
  22. package/dist/index.d.ts +4 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/live-spring-driver.d.ts +52 -0
  27. package/dist/live-spring-driver.d.ts.map +1 -0
  28. package/dist/live-spring-driver.js +104 -0
  29. package/dist/live-spring-driver.js.map +1 -0
  30. package/dist/mixer.d.ts +6 -0
  31. package/dist/mixer.d.ts.map +1 -1
  32. package/dist/mixer.js +41 -1
  33. package/dist/mixer.js.map +1 -1
  34. package/dist/motion-scope.d.ts +2 -0
  35. package/dist/motion-scope.d.ts.map +1 -1
  36. package/dist/motion-scope.js +6 -0
  37. package/dist/motion-scope.js.map +1 -1
  38. package/dist/motion-value.d.ts +34 -2
  39. package/dist/motion-value.d.ts.map +1 -1
  40. package/dist/motion-value.js +277 -10
  41. package/dist/motion-value.js.map +1 -1
  42. package/dist/native/index.d.ts +1 -0
  43. package/dist/native/index.d.ts.map +1 -1
  44. package/dist/native/index.js +1 -0
  45. package/dist/native/index.js.map +1 -1
  46. package/dist/scene-camera.d.ts +5 -0
  47. package/dist/scene-camera.d.ts.map +1 -0
  48. package/dist/scene-camera.js +23 -0
  49. package/dist/scene-camera.js.map +1 -0
  50. package/dist/size-layout.d.ts +19 -0
  51. package/dist/size-layout.d.ts.map +1 -0
  52. package/dist/size-layout.js +216 -0
  53. package/dist/size-layout.js.map +1 -0
  54. package/dist/size.d.ts +19 -0
  55. package/dist/size.d.ts.map +1 -0
  56. package/dist/size.js +251 -0
  57. package/dist/size.js.map +1 -0
  58. package/dist/value-observers.d.ts +3 -1
  59. package/dist/value-observers.d.ts.map +1 -1
  60. package/dist/value-observers.js +10 -2
  61. package/dist/value-observers.js.map +1 -1
  62. package/package.json +5 -1
  63. package/platforms/android/include.gradle +9 -0
  64. package/platforms/android/java/org/rafaelaugusto/jolt/GestureSession.java +193 -0
  65. package/platforms/android/java/org/rafaelaugusto/jolt/GestureSurface.java +53 -0
  66. package/platforms/android/java/org/rafaelaugusto/jolt/LiveSpring.java +270 -0
  67. package/platforms/android/java/org/rafaelaugusto/jolt/SpringState.java +39 -0
  68. package/platforms/ios/src/JoltGestureSession.h +17 -0
  69. package/platforms/ios/src/JoltGestureSession.m +217 -0
  70. package/platforms/ios/src/JoltLiveSpring.h +28 -0
  71. package/platforms/ios/src/JoltLiveSpring.m +236 -0
  72. package/platforms/ios/src/JoltSpringState.h +35 -0
  73. package/platforms/ios/src/module.modulemap +2 -0
  74. package/src/android-plugin.d.ts +59 -0
  75. package/src/drag.ts +1 -1
  76. package/src/driver/android.ts +2 -0
  77. package/src/driver/ios-scroll-delegate.ts +23 -0
  78. package/src/driver/ios.ts +2 -0
  79. package/src/gesture-native.ts +124 -0
  80. package/src/gestures/index.ts +350 -0
  81. package/src/index.ts +5 -1
  82. package/src/ios-plugin.d.ts +61 -0
  83. package/src/live-spring-driver.ts +181 -0
  84. package/src/mixer.ts +43 -1
  85. package/src/motion-scope.ts +14 -0
  86. package/src/motion-value.ts +318 -11
  87. package/src/native/index.ts +1 -0
  88. package/src/scene-camera.ts +30 -0
  89. package/src/size-layout.ts +237 -0
  90. package/src/size.ts +302 -0
  91. package/src/value-observers.ts +12 -2
@@ -0,0 +1,350 @@
1
+ import { Application, GridLayout, ScrollView, Trace, isAndroid } from '@nativescript/core'
2
+ import {
3
+ createGestureDriver,
4
+ type GestureDriver,
5
+ type GestureSettings,
6
+ } from '../gesture-native.js'
7
+ import {
8
+ motionValue,
9
+ springValue,
10
+ type DerivedValue,
11
+ type SpringValueOptions,
12
+ } from '../motion-value.js'
13
+
14
+ /** A GridLayout that provides native gesture dispatch and nested-scroll consumption. */
15
+ export class GestureSurface extends GridLayout {
16
+ override createNativeView() {
17
+ if (isAndroid) {
18
+ if (typeof org.rafaelaugusto.jolt.GestureSurface !== 'function')
19
+ throw new Error('Rebuild the native app to use GestureSurface.')
20
+ return new org.rafaelaugusto.jolt.GestureSurface(this._context)
21
+ }
22
+ return super.createNativeView()
23
+ }
24
+ }
25
+ export interface GesturePosition {
26
+ x: number
27
+ y: number
28
+ scale: number
29
+ rotate: number
30
+ }
31
+ export interface GestureEvent extends GesturePosition {
32
+ velocityX: number
33
+ velocityY: number
34
+ }
35
+ export interface GestureBounds {
36
+ left?: number
37
+ right?: number
38
+ top?: number
39
+ bottom?: number
40
+ }
41
+ export interface GesturesOptions {
42
+ pan?:
43
+ | boolean
44
+ | {
45
+ axis?: 'x' | 'y' | 'both'
46
+ threshold?: number
47
+ failOffset?: number
48
+ bounds?: GestureBounds
49
+ }
50
+ pinch?: boolean | { min?: number; max?: number }
51
+ rotate?: boolean
52
+ initial?: Partial<GesturePosition>
53
+ spring?: Pick<SpringValueOptions, 'stiffness' | 'damping' | 'mass'>
54
+ /** A descendant, vertical NativeScript ScrollView. Other list plugins are not supported. */
55
+ scrollView?: ScrollView
56
+ onStart?: (event: GestureEvent) => void
57
+ onEnd?: (event: GestureEvent) => void
58
+ onCancel?: (event: GestureEvent) => void
59
+ onTap?: (event: GestureEvent) => void
60
+ onDoubleTap?: (event: GestureEvent) => void
61
+ }
62
+ export interface Gestures {
63
+ readonly x: DerivedValue
64
+ readonly y: DerivedValue
65
+ readonly scale: DerivedValue
66
+ readonly rotate: DerivedValue
67
+ readonly position: GesturePosition
68
+ readonly active: boolean
69
+ readonly paused: boolean
70
+ readonly disposed: boolean
71
+ /** Spring toward a new position, carrying the last contact's velocity. */
72
+ settleTo(position: Partial<GesturePosition>): void
73
+ jumpTo(position: Partial<GesturePosition>): void
74
+ updateBounds(bounds: GestureBounds): void
75
+ cancel(): void
76
+ pause(): void
77
+ play(): void
78
+ dispose(): void
79
+ }
80
+ const owners = new WeakMap<GestureSurface, Gestures>()
81
+ const LIMIT = 1e9
82
+ function bounds(input: GestureBounds = {}) {
83
+ if (!input || typeof input !== 'object')
84
+ throw new TypeError('Gesture bounds must be an object.')
85
+ const result = {
86
+ minX: input.left ?? -LIMIT,
87
+ maxX: input.right ?? LIMIT,
88
+ minY: input.top ?? -LIMIT,
89
+ maxY: input.bottom ?? LIMIT,
90
+ }
91
+ if (
92
+ Object.values(result).some((value) => !Number.isFinite(value)) ||
93
+ result.minX > result.maxX ||
94
+ result.minY > result.maxY
95
+ )
96
+ throw new RangeError('Gesture bounds must be finite and ordered.')
97
+ return result
98
+ }
99
+ function settings(options: GesturesOptions): GestureSettings {
100
+ for (const key of ['pan', 'pinch'] as const)
101
+ if (
102
+ options[key] !== undefined &&
103
+ typeof options[key] !== 'boolean' &&
104
+ (!options[key] || typeof options[key] !== 'object')
105
+ )
106
+ throw new TypeError(`Gesture ${key} must be boolean or an options object.`)
107
+ if (options.rotate !== undefined && typeof options.rotate !== 'boolean')
108
+ throw new TypeError('Gesture rotate must be boolean.')
109
+ for (const key of ['onStart', 'onEnd', 'onCancel', 'onTap', 'onDoubleTap'] as const)
110
+ if (options[key] !== undefined && typeof options[key] !== 'function')
111
+ throw new TypeError(`Gesture ${key} must be a function.`)
112
+ const pan = typeof options.pan === 'object' ? options.pan : {}
113
+ const pinch = typeof options.pinch === 'object' ? options.pinch : {}
114
+ const axis = pan.axis ?? 'both'
115
+ if (!['x', 'y', 'both'].includes(axis))
116
+ throw new TypeError('Gesture pan axis must be x, y or both.')
117
+ const result = {
118
+ ...bounds(pan.bounds),
119
+ axis: options.pan ? (axis === 'x' ? 1 : axis === 'y' ? 2 : 3) : 0,
120
+ threshold: pan.threshold ?? 8,
121
+ failOffset: pan.failOffset ?? 12,
122
+ pinch: !!options.pinch,
123
+ rotate: !!options.rotate,
124
+ tap: !!options.onTap,
125
+ doubleTap: !!options.onDoubleTap,
126
+ minScale: pinch.min ?? 0.25,
127
+ maxScale: pinch.max ?? 4,
128
+ }
129
+ if (
130
+ ![result.threshold, result.failOffset, result.minScale, result.maxScale].every(
131
+ (value) => Number.isFinite(value) && value > 0,
132
+ ) ||
133
+ result.minScale > result.maxScale
134
+ )
135
+ throw new RangeError(
136
+ 'Gesture thresholds and scale limits must be positive and ordered.',
137
+ )
138
+ if (
139
+ options.scrollView &&
140
+ (!(options.scrollView instanceof ScrollView) ||
141
+ options.scrollView.orientation !== 'vertical' ||
142
+ result.axis !== 2)
143
+ )
144
+ throw new TypeError('Scroll handoff needs a vertical ScrollView and a y-axis pan.')
145
+ if (
146
+ options.scrollView &&
147
+ (pan.bounds?.top === undefined || pan.bounds?.bottom === undefined)
148
+ )
149
+ throw new TypeError('Scroll handoff needs explicit top and bottom pan bounds.')
150
+ return result
151
+ }
152
+
153
+ /** Compose native pan/pinch/rotation and exclusive single/double taps on one surface. */
154
+ export function gestures(
155
+ surface: GestureSurface,
156
+ options: GesturesOptions = {},
157
+ ): Gestures {
158
+ if (!(surface instanceof GestureSurface))
159
+ throw new TypeError('gestures() requires a GestureSurface.')
160
+ if (!options || typeof options !== 'object')
161
+ throw new TypeError('Gesture options must be an object.')
162
+ let config = settings(options)
163
+ if (options.scrollView) {
164
+ let ancestor = options.scrollView.parent
165
+ while (ancestor && ancestor !== surface) ancestor = ancestor.parent
166
+ if (ancestor !== surface)
167
+ throw new TypeError('The ScrollView must be inside the GestureSurface.')
168
+ }
169
+ const initial = { x: 0, y: 0, scale: 1, rotate: 0, ...options.initial }
170
+ if (Object.values(initial).some((value) => !Number.isFinite(value)))
171
+ throw new RangeError('Initial gesture values must be finite.')
172
+ if (
173
+ initial.x < config.minX ||
174
+ initial.x > config.maxX ||
175
+ initial.y < config.minY ||
176
+ initial.y > config.maxY ||
177
+ initial.scale < config.minScale ||
178
+ initial.scale > config.maxScale
179
+ )
180
+ throw new RangeError('Initial gesture values must be within their bounds.')
181
+ const keys = ['x', 'y', 'scale', 'rotate'] as const
182
+ const values = keys.map((key) => springValue(motionValue(initial[key]), options.spring))
183
+ let driver: GestureDriver | undefined,
184
+ ended = false,
185
+ held = false,
186
+ suspended = false,
187
+ disposing = false
188
+ const position = (): GesturePosition => ({
189
+ x: values[0]!.get(),
190
+ y: values[1]!.get(),
191
+ scale: values[2]!.get(),
192
+ rotate: values[3]!.get(),
193
+ })
194
+ const callback = (state: number, velocityX: number, velocityY: number) => {
195
+ if (ended) return
196
+ const event = { ...position(), velocityX, velocityY }
197
+ const handler = [
198
+ undefined,
199
+ options.onStart,
200
+ options.onEnd,
201
+ options.onCancel,
202
+ options.onTap,
203
+ options.onDoubleTap,
204
+ ][state]
205
+ try {
206
+ handler?.(event)
207
+ } catch (error) {
208
+ Trace.write(
209
+ `Jolt gesture callback failed: ${String(error)}`,
210
+ Trace.categories.Error,
211
+ Trace.messageType.error,
212
+ )
213
+ }
214
+ }
215
+ const detach = () => {
216
+ const previous = driver
217
+ driver = undefined
218
+ previous?.dispose()
219
+ }
220
+ const refreshEnabled = () =>
221
+ driver?.enabled(
222
+ !held &&
223
+ !suspended &&
224
+ surface.isEnabled !== false &&
225
+ surface.isUserInteractionEnabled !== false,
226
+ )
227
+ const attach = () => {
228
+ if (
229
+ ended ||
230
+ driver ||
231
+ !surface.isLoaded ||
232
+ !surface.nativeViewProtected ||
233
+ (options.scrollView && !options.scrollView.nativeViewProtected)
234
+ )
235
+ return
236
+ try {
237
+ driver = createGestureDriver(surface, options.scrollView, values, config, callback)
238
+ refreshEnabled()
239
+ } catch (error) {
240
+ control.dispose()
241
+ throw error
242
+ }
243
+ }
244
+ const suspend = () => {
245
+ suspended = true
246
+ driver?.enabled(false)
247
+ }
248
+ const resume = () => {
249
+ suspended = false
250
+ refreshEnabled()
251
+ }
252
+ const move = (next: Partial<GesturePosition>, animated: boolean) => {
253
+ if (ended) return
254
+ if (
255
+ !next ||
256
+ typeof next !== 'object' ||
257
+ Object.entries(next).some(
258
+ ([key, value]) =>
259
+ !keys.includes(key as (typeof keys)[number]) || !Number.isFinite(value),
260
+ )
261
+ )
262
+ throw new TypeError(
263
+ 'Gesture positions must contain finite x, y, scale or rotate values.',
264
+ )
265
+ if (!driver)
266
+ throw new Error('Load the GestureSurface before moving its gesture values.')
267
+ const target = { ...position(), ...next }
268
+ driver.settle(
269
+ keys.map((key) => target[key]),
270
+ animated,
271
+ )
272
+ }
273
+ const control: Gestures = {
274
+ x: values[0]!,
275
+ y: values[1]!,
276
+ scale: values[2]!,
277
+ rotate: values[3]!,
278
+ get position() {
279
+ return position()
280
+ },
281
+ get active() {
282
+ return driver?.active ?? false
283
+ },
284
+ get paused() {
285
+ return held
286
+ },
287
+ get disposed() {
288
+ return ended
289
+ },
290
+ settleTo: (next) => move(next, true),
291
+ jumpTo: (next) => move(next, false),
292
+ updateBounds(next) {
293
+ config = {
294
+ ...config,
295
+ ...bounds({
296
+ left: config.minX,
297
+ right: config.maxX,
298
+ top: config.minY,
299
+ bottom: config.maxY,
300
+ ...next,
301
+ }),
302
+ }
303
+ driver?.bounds(config)
304
+ },
305
+ cancel() {
306
+ driver?.cancel()
307
+ },
308
+ pause() {
309
+ held = true
310
+ driver?.enabled(false)
311
+ },
312
+ play() {
313
+ held = false
314
+ refreshEnabled()
315
+ },
316
+ dispose() {
317
+ if (ended || disposing) return
318
+ disposing = true
319
+ detach()
320
+ ended = true
321
+ surface.off('loaded', attach)
322
+ surface.off('unloaded', detach)
323
+ surface.off('isEnabledChange', refreshEnabled)
324
+ surface.off('isUserInteractionEnabledChange', refreshEnabled)
325
+ options.scrollView?.off('loaded', attach)
326
+ options.scrollView?.off('unloaded', detach)
327
+ Application.off(Application.suspendEvent, suspend)
328
+ Application.off(Application.resumeEvent, resume)
329
+ values.forEach((value) => value.dispose())
330
+ if (owners.get(surface) === control) owners.delete(surface)
331
+ },
332
+ }
333
+ owners.get(surface)?.dispose()
334
+ owners.set(surface, control)
335
+ surface.on('loaded', attach)
336
+ surface.on('unloaded', detach)
337
+ surface.on('isEnabledChange', refreshEnabled)
338
+ surface.on('isUserInteractionEnabledChange', refreshEnabled)
339
+ options.scrollView?.on('loaded', attach)
340
+ options.scrollView?.on('unloaded', detach)
341
+ Application.on(Application.suspendEvent, suspend)
342
+ Application.on(Application.resumeEvent, resume)
343
+ try {
344
+ attach()
345
+ } catch (error) {
346
+ control.dispose()
347
+ throw error
348
+ }
349
+ return control
350
+ }
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ export { timeline } from './sequence.js'
2
2
  export type { TimelineAnimation, TimelineStep, PlayableAnimation } from './sequence.js'
3
3
  export { playbackProgress } from './motion-value.js'
4
4
  export { animate, decay, set, stop } from './animate.js'
5
- export { bind, motionValue, scrollValue } from './motion-value.js'
5
+ export { bind, motionValue, scrollValue, springValue } from './motion-value.js'
6
6
  export type {
7
7
  Bindings,
8
8
  DerivedColor,
@@ -10,6 +10,8 @@ export type {
10
10
  MapOptions,
11
11
  MotionValue,
12
12
  ScrollValue,
13
+ SpringValue,
14
+ SpringValueOptions,
13
15
  ValueAnimationOptions,
14
16
  } from './motion-value.js'
15
17
  export type { SubscribeOptions, ValueListener } from './value-observers.js'
@@ -104,3 +106,5 @@ export type {
104
106
 
105
107
  export { animateLayout } from './layout.js'
106
108
  export type { LayoutOptions } from './layout.js'
109
+ export { animateSize } from './size.js'
110
+ export type { SizeAnimation, SizeDimensions, SizeOptions } from './size.js'
@@ -1,3 +1,61 @@
1
+ declare class JoltGestureSession extends NSObject {
2
+ static attachScrollXYScaleRotationOptionsOnState(
3
+ surface: UIView,
4
+ scroll: UIScrollView | null,
5
+ x: JoltLiveSpring,
6
+ y: JoltLiveSpring,
7
+ scale: JoltLiveSpring,
8
+ rotation: JoltLiveSpring,
9
+ options: string,
10
+ onState: (state: number, velocityX: number, velocityY: number) => void,
11
+ ): JoltGestureSession
12
+ readonly active: boolean
13
+ cancel(): void
14
+ refreshScrollDelegate(): void
15
+ setEnabled(value: boolean): void
16
+ settleXYScaleRotationAnimated(
17
+ x: number,
18
+ y: number,
19
+ scale: number,
20
+ rotation: number,
21
+ animated: boolean,
22
+ ): void
23
+ updateLeftRightTopBottom(left: number, right: number, top: number, bottom: number): void
24
+ dispose(): void
25
+ }
26
+ declare class JoltLiveSpring extends NSObject {
27
+ static createStiffnessDampingMassRestDeltaRestSpeedReduced(
28
+ initial: number,
29
+ stiffness: number,
30
+ damping: number,
31
+ mass: number,
32
+ restDelta: number,
33
+ restSpeed: number,
34
+ reduced: boolean,
35
+ ): JoltLiveSpring
36
+ static activeCount(): number
37
+ static nodeCount(): number
38
+ static sync(view: UIView): void
39
+ readonly position: number
40
+ readonly velocity: number
41
+ readonly moving: boolean
42
+ followMappings(source: JoltLiveSpring, mappings: string): void
43
+ updateInput(snapshot: string): void
44
+ setTarget(value: number): void
45
+ setReduced(value: boolean): void
46
+ setPaused(value: boolean): void
47
+ bindPropertyMappings(
48
+ view: UIView,
49
+ property: string,
50
+ mappings: string,
51
+ ): JoltLiveSpringBinding
52
+ dispose(): void
53
+ }
54
+ declare class JoltLiveSpringBinding extends NSObject {
55
+ readonly value: number
56
+ setLayers(inputs: string): void
57
+ cancel(): void
58
+ }
1
59
  declare class JoltInteractiveAnimation extends NSObject {
2
60
  static startViewLayersKeyPathValuesTimesDurationDelayRepeatReverseFinalValueStartedAtCompletion(
3
61
  view: UIView,
@@ -108,6 +166,9 @@ declare class JoltPropertyPlayer extends NSObject {
108
166
  cancel(): void
109
167
  }
110
168
  declare class JoltReferenceProperties extends NSObject {
169
+ static sceneFieldOfView(camera: NSObject): JoltPropertySink
170
+ static sceneCameraSinkCount(): number
171
+ static sceneCameraWriteCount(): number
111
172
  static mapCoordinate(target: NSObject): JoltPropertySink
112
173
  static mapCamera(target: NSObject): JoltPropertySink
113
174
  static scenePose(target: NSObject): JoltPropertySink
@@ -0,0 +1,181 @@
1
+ import { isAndroid, type View } from '@nativescript/core'
2
+ import type { NumericProperty } from './properties.js'
3
+ import { retainInteractiveLayout } from './driver/ios-layout.js'
4
+
5
+ export interface LiveMapping {
6
+ input: readonly number[]
7
+ output: readonly number[]
8
+ clamp: boolean
9
+ }
10
+ export interface LiveSpringPhysics {
11
+ stiffness: number
12
+ damping: number
13
+ mass: number
14
+ restDelta: number
15
+ restSpeed: number
16
+ }
17
+ export interface LiveInput {
18
+ value: number
19
+ running: boolean
20
+ elapsed: number
21
+ duration: number
22
+ delay: number
23
+ repeat: number
24
+ reverse: boolean
25
+ to: number
26
+ times: readonly number[]
27
+ values: readonly number[]
28
+ }
29
+ export interface LiveBinding {
30
+ value(): number
31
+ layers(inputs: readonly LiveInput[]): void
32
+ cancel(): void
33
+ }
34
+ export interface LiveNode {
35
+ position(): number
36
+ velocity(): number
37
+ moving(): boolean
38
+ follow(source: LiveNode, maps: readonly LiveMapping[]): void
39
+ update(input: LiveInput): void
40
+ reduced(value: boolean): void
41
+ paused(value: boolean): void
42
+ bind(view: View, property: NumericProperty, maps: readonly LiveMapping[]): LiveBinding
43
+ dispose(): void
44
+ }
45
+ export interface LiveSpringDriver {
46
+ create(initial: number, physics: LiveSpringPhysics, reduced: boolean): LiveNode
47
+ sync(view: View): void
48
+ }
49
+ let override: LiveSpringDriver | undefined
50
+ export function setLiveSpringDriver(driver: LiveSpringDriver | undefined) {
51
+ override = driver
52
+ }
53
+ export function liveSpringDriver(): LiveSpringDriver {
54
+ if (override) return override
55
+ if (!available())
56
+ throw new Error(
57
+ 'springValue() needs the Jolt native classes. Rebuild the native app after upgrading Jolt.',
58
+ )
59
+ return nativeDriver
60
+ }
61
+ function available() {
62
+ return isAndroid
63
+ ? typeof org !== 'undefined' &&
64
+ typeof org.rafaelaugusto.jolt.LiveSpring === 'function'
65
+ : typeof JoltLiveSpring !== 'undefined'
66
+ }
67
+ export function syncLiveSprings(view: View) {
68
+ if (override) override.sync(view)
69
+ else if (available() && view.nativeViewProtected) nativeDriver.sync(view)
70
+ }
71
+ const handles = new WeakMap<
72
+ LiveNode,
73
+ org.rafaelaugusto.jolt.LiveSpring | JoltLiveSpring
74
+ >()
75
+ /** Internal native input connection; never exposes handles through the public value API. */
76
+ export function nativeLiveNode(node: LiveNode) {
77
+ const handle = handles.get(node)
78
+ if (!handle) throw new Error('A gesture needs a native live value node.')
79
+ return handle
80
+ }
81
+ const nativeDriver: LiveSpringDriver = {
82
+ create(initial, physics, reduced) {
83
+ const { stiffness, damping, mass, restDelta, restSpeed } = physics
84
+ if (isAndroid) {
85
+ const native = new org.rafaelaugusto.jolt.LiveSpring(
86
+ initial,
87
+ stiffness,
88
+ damping,
89
+ mass,
90
+ restDelta,
91
+ restSpeed,
92
+ reduced,
93
+ )
94
+ const node: LiveNode = {
95
+ position: () => native.getPosition(),
96
+ velocity: () => native.getVelocity(),
97
+ moving: () => native.isMoving(),
98
+ follow: (source, maps) =>
99
+ native.follow(
100
+ handles.get(source) as org.rafaelaugusto.jolt.LiveSpring,
101
+ JSON.stringify(maps),
102
+ ),
103
+ update: (input) =>
104
+ input.running
105
+ ? native.updateInput(JSON.stringify(input))
106
+ : native.setTarget(input.value),
107
+ reduced: (value) => native.setReduced(value),
108
+ paused: (value) => native.setPaused(value),
109
+ bind(view, property, maps) {
110
+ const binding = native.bind(
111
+ view.nativeViewProtected,
112
+ property,
113
+ JSON.stringify(maps),
114
+ )
115
+ return {
116
+ value: () => binding.getValue(),
117
+ layers: (inputs) => binding.setLayers(JSON.stringify(inputs)),
118
+ cancel: () => binding.cancel(),
119
+ }
120
+ },
121
+ dispose: () => native.dispose(),
122
+ }
123
+ handles.set(node, native)
124
+ return node
125
+ }
126
+ const native = JoltLiveSpring.createStiffnessDampingMassRestDeltaRestSpeedReduced(
127
+ initial,
128
+ stiffness,
129
+ damping,
130
+ mass,
131
+ restDelta,
132
+ restSpeed,
133
+ reduced,
134
+ )
135
+ const node: LiveNode = {
136
+ position: () => native.position,
137
+ velocity: () => native.velocity,
138
+ moving: () => native.moving,
139
+ follow: (source, maps) =>
140
+ native.followMappings(
141
+ handles.get(source) as JoltLiveSpring,
142
+ JSON.stringify(maps),
143
+ ),
144
+ update: (input) =>
145
+ input.running
146
+ ? native.updateInput(JSON.stringify(input))
147
+ : native.setTarget(input.value),
148
+ reduced: (value) => native.setReduced(value),
149
+ paused: (value) => native.setPaused(value),
150
+ bind(view, property, maps) {
151
+ const releaseLayout = retainInteractiveLayout(view)
152
+ let binding: JoltLiveSpringBinding
153
+ try {
154
+ binding = native.bindPropertyMappings(
155
+ view.nativeViewProtected,
156
+ property,
157
+ JSON.stringify(maps),
158
+ )
159
+ } catch (error) {
160
+ releaseLayout()
161
+ throw error
162
+ }
163
+ return {
164
+ value: () => binding.value,
165
+ layers: (inputs) => binding.setLayers(JSON.stringify(inputs)),
166
+ cancel: () => {
167
+ binding.cancel()
168
+ releaseLayout()
169
+ },
170
+ }
171
+ },
172
+ dispose: () => native.dispose(),
173
+ }
174
+ handles.set(node, native)
175
+ return node
176
+ },
177
+ sync(view) {
178
+ if (isAndroid) org.rafaelaugusto.jolt.LiveSpring.sync(view.nativeViewProtected)
179
+ else JoltLiveSpring.sync(view.nativeViewProtected)
180
+ },
181
+ }
package/src/mixer.ts CHANGED
@@ -2,6 +2,7 @@ import type { View } from '@nativescript/core'
2
2
  import { getDriver } from './driver/index.js'
3
3
  import { readValue, writeValue, type Property } from './properties.js'
4
4
  import type { Track } from './track.js'
5
+ import type { LiveInput } from './live-spring-driver.js'
5
6
 
6
7
  /** The channel of regular animations; layers are named next to it. */
7
8
  export const BASE = ''
@@ -67,6 +68,7 @@ export function mixFor(view: View, property: Property): Mix {
67
68
 
68
69
  /** One property of one view: a base and named layers, played natively as one composite track. */
69
70
  export class Mix {
71
+ private live: { read(): number; layers(inputs: readonly LiveInput[]): void } | undefined
70
72
  private readonly rests = new Map<string, number>()
71
73
  private readonly channels = new Map<string, Channel>()
72
74
  private native: unknown
@@ -80,12 +82,27 @@ export class Mix {
80
82
  }
81
83
 
82
84
  rest(key: string): number {
85
+ if (key === BASE && this.live) return this.live.read()
83
86
  return (
84
87
  this.rests.get(key) ??
85
88
  (key === BASE ? readValue(this.view, this.property) : identity(this.property))
86
89
  )
87
90
  }
88
91
 
92
+ attachLive(base: {
93
+ read(): number
94
+ layers(inputs: readonly LiveInput[]): void
95
+ }): () => void {
96
+ this.live = base
97
+ this.compose()
98
+ return () => {
99
+ if (this.live !== base) return
100
+ this.rests.set(BASE, base.read())
101
+ this.live = undefined
102
+ this.compose()
103
+ }
104
+ }
105
+
89
106
  setRest(key: string, value: number) {
90
107
  this.rests.set(key, value)
91
108
  this.compose()
@@ -192,6 +209,31 @@ export class Mix {
192
209
  const previous = this.native
193
210
  this.native = undefined
194
211
 
212
+ if (this.live) {
213
+ if (previous !== undefined) driver.stop(this.view, previous)
214
+ const layers: LiveInput[] = []
215
+ for (const key of new Set([...this.rests.keys(), ...this.channels.keys()])) {
216
+ if (key === BASE) continue
217
+ const channel = this.channels.get(key)
218
+ const value = this.part(key, now, now)
219
+ const track = channel?.track
220
+ layers.push({
221
+ value,
222
+ running: !!channel && !channel.paused,
223
+ elapsed: this.elapsed(key, now),
224
+ duration: track?.duration ?? 0,
225
+ delay: track?.delay ?? 0,
226
+ repeat: track ? (Number.isFinite(track.repeat) ? track.repeat : -1) : 0,
227
+ reverse: track?.reverse ?? false,
228
+ to: track?.to ?? value,
229
+ times: track?.times ?? [],
230
+ values: track?.values ?? [],
231
+ })
232
+ }
233
+ this.live.layers(layers)
234
+ return
235
+ }
236
+
195
237
  let finite = 0
196
238
  const periods: number[] = []
197
239
  for (const [key, channel] of this.channels) {
@@ -245,7 +287,7 @@ export class Mix {
245
287
  }
246
288
 
247
289
  private dissolve() {
248
- if (this.channels.size) return
290
+ if (this.live || this.channels.size) return
249
291
  for (const [key, value] of this.rests)
250
292
  if (key !== BASE && value !== identity(this.property)) return
251
293
  const properties = mixes.get(this.view)