@rafaelaugusto/jolt 0.1.0-beta.15 → 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.
- package/README.md +3 -1
- package/dist/drag.js +1 -1
- package/dist/drag.js.map +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +2 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/ios-scroll-delegate.d.ts +4 -0
- package/dist/driver/ios-scroll-delegate.d.ts.map +1 -0
- package/dist/driver/ios-scroll-delegate.js +26 -0
- package/dist/driver/ios-scroll-delegate.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +2 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/gesture-native.d.ts +27 -0
- package/dist/gesture-native.d.ts.map +1 -0
- package/dist/gesture-native.js +55 -0
- package/dist/gesture-native.js.map +1 -0
- package/dist/gestures/index.d.ts +65 -0
- package/dist/gestures/index.d.ts.map +1 -0
- package/dist/gestures/index.js +251 -0
- package/dist/gestures/index.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/live-spring-driver.d.ts +52 -0
- package/dist/live-spring-driver.d.ts.map +1 -0
- package/dist/live-spring-driver.js +104 -0
- package/dist/live-spring-driver.js.map +1 -0
- package/dist/mixer.d.ts +6 -0
- package/dist/mixer.d.ts.map +1 -1
- package/dist/mixer.js +41 -1
- package/dist/mixer.js.map +1 -1
- package/dist/motion-value.d.ts +34 -2
- package/dist/motion-value.d.ts.map +1 -1
- package/dist/motion-value.js +277 -10
- package/dist/motion-value.js.map +1 -1
- package/dist/value-observers.d.ts +3 -1
- package/dist/value-observers.d.ts.map +1 -1
- package/dist/value-observers.js +10 -2
- package/dist/value-observers.js.map +1 -1
- package/package.json +5 -1
- package/platforms/android/include.gradle +9 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/GestureSession.java +193 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/GestureSurface.java +53 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/LiveSpring.java +270 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/SpringState.java +39 -0
- package/platforms/ios/src/JoltGestureSession.h +17 -0
- package/platforms/ios/src/JoltGestureSession.m +217 -0
- package/platforms/ios/src/JoltLiveSpring.h +28 -0
- package/platforms/ios/src/JoltLiveSpring.m +236 -0
- package/platforms/ios/src/JoltSpringState.h +35 -0
- package/platforms/ios/src/module.modulemap +2 -0
- package/src/android-plugin.d.ts +59 -0
- package/src/drag.ts +1 -1
- package/src/driver/android.ts +2 -0
- package/src/driver/ios-scroll-delegate.ts +23 -0
- package/src/driver/ios.ts +2 -0
- package/src/gesture-native.ts +124 -0
- package/src/gestures/index.ts +350 -0
- package/src/index.ts +3 -1
- package/src/ios-plugin.d.ts +58 -0
- package/src/live-spring-driver.ts +181 -0
- package/src/mixer.ts +43 -1
- package/src/motion-value.ts +318 -11
- 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'
|
package/src/ios-plugin.d.ts
CHANGED
|
@@ -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,
|
|
@@ -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)
|