@rafaelaugusto/jolt 0.1.0-beta.7 → 0.1.0-beta.8
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 +9 -2
- package/dist/animate.d.ts +2 -0
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +7 -2
- package/dist/animate.js.map +1 -1
- package/dist/drag-input.d.ts +15 -0
- package/dist/drag-input.d.ts.map +1 -0
- package/dist/drag-input.js +66 -0
- package/dist/drag-input.js.map +1 -0
- package/dist/drag-physics.d.ts +20 -0
- package/dist/drag-physics.d.ts.map +1 -0
- package/dist/drag-physics.js +75 -0
- package/dist/drag-physics.js.map +1 -0
- package/dist/drag.d.ts +58 -0
- package/dist/drag.d.ts.map +1 -0
- package/dist/drag.js +324 -0
- package/dist/drag.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +38 -6
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +9 -0
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/interaction-surface.d.ts +12 -0
- package/dist/interaction-surface.d.ts.map +1 -0
- package/dist/interaction-surface.js +36 -0
- package/dist/interaction-surface.js.map +1 -0
- package/dist/motion-scope.d.ts +37 -0
- package/dist/motion-scope.d.ts.map +1 -0
- package/dist/motion-scope.js +232 -0
- package/dist/motion-scope.js.map +1 -0
- package/dist/motion-states.d.ts +36 -0
- package/dist/motion-states.d.ts.map +1 -0
- package/dist/motion-states.js +165 -0
- package/dist/motion-states.js.map +1 -0
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +14 -3
- package/dist/press.js.map +1 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +21 -4
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +2 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js.map +1 -1
- package/dist/vue/index.d.ts +3 -1
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +17 -4
- package/dist/vue/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
- package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
- package/platforms/ios/src/module.modulemap +1 -0
- package/src/animate.ts +15 -8
- package/src/drag-input.ts +74 -0
- package/src/drag-physics.ts +110 -0
- package/src/drag.ts +395 -0
- package/src/driver/ios.ts +57 -8
- package/src/engine.ts +11 -0
- package/src/index.ts +14 -0
- package/src/interaction-surface.ts +41 -0
- package/src/ios-plugin.d.ts +22 -0
- package/src/motion-scope.ts +265 -0
- package/src/motion-states.ts +215 -0
- package/src/press.ts +15 -3
- package/src/track.ts +25 -4
- package/src/transition.ts +2 -0
- package/src/vue/index.ts +20 -3
package/src/drag.ts
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Application,
|
|
3
|
+
type EventData,
|
|
4
|
+
type PanGestureEventData,
|
|
5
|
+
type TouchGestureEventData,
|
|
6
|
+
type View,
|
|
7
|
+
} from '@nativescript/core'
|
|
8
|
+
import { stop, toView } from './animate.js'
|
|
9
|
+
import { DEFAULT_FRICTION } from './decay.js'
|
|
10
|
+
import { DragInput } from './drag-input.js'
|
|
11
|
+
import {
|
|
12
|
+
clampDrag,
|
|
13
|
+
resistDrag,
|
|
14
|
+
resumeDrag,
|
|
15
|
+
solveDragRelease,
|
|
16
|
+
type DragCurve,
|
|
17
|
+
} from './drag-physics.js'
|
|
18
|
+
import { currentValue, ViewAnimation, type Plan } from './engine.js'
|
|
19
|
+
import type { PlaybackAnimation } from './playback.js'
|
|
20
|
+
import { readValue, writeValue } from './properties.js'
|
|
21
|
+
import type { Target } from './types.js'
|
|
22
|
+
|
|
23
|
+
export interface DragPoint {
|
|
24
|
+
readonly x: number
|
|
25
|
+
readonly y: number
|
|
26
|
+
}
|
|
27
|
+
export interface DragEvent extends DragPoint {
|
|
28
|
+
/** DIPs per second, in the same coordinates as the translation. */
|
|
29
|
+
readonly velocityX: number
|
|
30
|
+
readonly velocityY: number
|
|
31
|
+
}
|
|
32
|
+
export interface DragRelease extends DragEvent {
|
|
33
|
+
readonly target: DragPoint
|
|
34
|
+
/** False on interruption/cancellation. Check before applying completion effects. */
|
|
35
|
+
readonly animation: PlaybackAnimation
|
|
36
|
+
}
|
|
37
|
+
export interface DragOptions {
|
|
38
|
+
/** Defaults to both. One finger only. */
|
|
39
|
+
axis?: 'x' | 'y' | 'both'
|
|
40
|
+
/** Absolute translateX/translateY limits in DIPs; omitted edges are unbounded. */
|
|
41
|
+
bounds?: { left?: number; right?: number; top?: number; bottom?: number }
|
|
42
|
+
/** Closest stop to the projected landing. Arrays apply to every enabled axis. */
|
|
43
|
+
snap?: readonly number[] | { x?: readonly number[]; y?: readonly number[] }
|
|
44
|
+
/** Resist beyond bounds and spring back. Defaults to false (hard limits). */
|
|
45
|
+
rubberband?: boolean
|
|
46
|
+
/** Decay retention per millisecond. Defaults to 0.998. */
|
|
47
|
+
friction?: number
|
|
48
|
+
onStart?: (event: DragEvent) => void
|
|
49
|
+
onMove?: (event: DragEvent) => void
|
|
50
|
+
onRelease?: (event: DragRelease) => void
|
|
51
|
+
onCancel?: (event: DragEvent) => void
|
|
52
|
+
onSettle?: (position: DragPoint) => void
|
|
53
|
+
}
|
|
54
|
+
export interface Drag {
|
|
55
|
+
readonly dragging: boolean
|
|
56
|
+
readonly settling: boolean
|
|
57
|
+
readonly paused: boolean
|
|
58
|
+
readonly disposed: boolean
|
|
59
|
+
/** Current presentation, including a native release animation in flight. */
|
|
60
|
+
readonly position: DragPoint
|
|
61
|
+
/** Cancel contact/release at its visible position, keeping the subscription. */
|
|
62
|
+
cancel(): void
|
|
63
|
+
/** Cancel and detach input. play() accepts new gestures; cancelled work stays cancelled. */
|
|
64
|
+
pause(): void
|
|
65
|
+
play(): void
|
|
66
|
+
dispose(): void
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type Axis = 'x' | 'y'
|
|
70
|
+
interface Settings {
|
|
71
|
+
axes: Axis[]
|
|
72
|
+
curves: Record<Axis, DragCurve>
|
|
73
|
+
callbacks: Pick<
|
|
74
|
+
DragOptions,
|
|
75
|
+
'onStart' | 'onMove' | 'onRelease' | 'onCancel' | 'onSettle'
|
|
76
|
+
>
|
|
77
|
+
}
|
|
78
|
+
const owners = new WeakMap<View, Drag>()
|
|
79
|
+
|
|
80
|
+
function settings(options: DragOptions): Settings {
|
|
81
|
+
if (!options || typeof options !== 'object')
|
|
82
|
+
throw new TypeError('Drag options must be an object.')
|
|
83
|
+
const axis = options.axis ?? 'both'
|
|
84
|
+
if (!['x', 'y', 'both'].includes(axis))
|
|
85
|
+
throw new TypeError('Drag axis must be x, y or both.')
|
|
86
|
+
const friction = options.friction ?? DEFAULT_FRICTION
|
|
87
|
+
if (!Number.isFinite(friction) || friction <= 0 || friction >= 1)
|
|
88
|
+
throw new RangeError('Drag friction must be between 0 and 1 exclusively.')
|
|
89
|
+
if (options.rubberband !== undefined && typeof options.rubberband !== 'boolean')
|
|
90
|
+
throw new TypeError('Drag rubberband must be boolean.')
|
|
91
|
+
const bounds = options.bounds ?? {}
|
|
92
|
+
if (
|
|
93
|
+
typeof bounds !== 'object' ||
|
|
94
|
+
Array.isArray(bounds) ||
|
|
95
|
+
Object.entries(bounds).some(
|
|
96
|
+
([key, value]) =>
|
|
97
|
+
!['left', 'right', 'top', 'bottom'].includes(key) ||
|
|
98
|
+
(value !== undefined && !Number.isFinite(value)),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
throw new RangeError('Drag bounds must have finite left/right/top/bottom edges.')
|
|
102
|
+
const x: readonly [number, number] = [
|
|
103
|
+
bounds.left ?? -Infinity,
|
|
104
|
+
bounds.right ?? Infinity,
|
|
105
|
+
]
|
|
106
|
+
const y: readonly [number, number] = [
|
|
107
|
+
bounds.top ?? -Infinity,
|
|
108
|
+
bounds.bottom ?? Infinity,
|
|
109
|
+
]
|
|
110
|
+
if (x[0] > x[1] || y[0] > y[1]) throw new RangeError('Drag bounds must be ordered.')
|
|
111
|
+
const snap = options.snap
|
|
112
|
+
const shared = Array.isArray(snap)
|
|
113
|
+
if (
|
|
114
|
+
snap !== undefined &&
|
|
115
|
+
!shared &&
|
|
116
|
+
(!snap ||
|
|
117
|
+
typeof snap !== 'object' ||
|
|
118
|
+
Object.keys(snap).some((key) => key !== 'x' && key !== 'y'))
|
|
119
|
+
)
|
|
120
|
+
throw new TypeError('Drag snap must be an array or x/y arrays.')
|
|
121
|
+
const stops = (
|
|
122
|
+
value: readonly number[] | undefined,
|
|
123
|
+
range: readonly [number, number],
|
|
124
|
+
) => {
|
|
125
|
+
if (value === undefined) return undefined
|
|
126
|
+
if (
|
|
127
|
+
!Array.isArray(value) ||
|
|
128
|
+
!value.length ||
|
|
129
|
+
![...value].every((n) => Number.isFinite(n) && n >= range[0] && n <= range[1])
|
|
130
|
+
)
|
|
131
|
+
throw new RangeError('Drag snap needs nonempty finite positions within bounds.')
|
|
132
|
+
return [...value]
|
|
133
|
+
}
|
|
134
|
+
const perAxis = snap as { x?: readonly number[]; y?: readonly number[] } | undefined
|
|
135
|
+
const axes: Axis[] = axis === 'both' ? ['x', 'y'] : [axis]
|
|
136
|
+
const curves = Object.fromEntries(
|
|
137
|
+
(['x', 'y'] as const).map((key) => [
|
|
138
|
+
key,
|
|
139
|
+
{
|
|
140
|
+
type: 'drag',
|
|
141
|
+
velocity: 0,
|
|
142
|
+
friction,
|
|
143
|
+
bounds: key === 'x' ? x : y,
|
|
144
|
+
snap: stops(
|
|
145
|
+
shared && axes.includes(key) ? snap : perAxis?.[key],
|
|
146
|
+
key === 'x' ? x : y,
|
|
147
|
+
),
|
|
148
|
+
rubberband: options.rubberband ?? false,
|
|
149
|
+
},
|
|
150
|
+
]),
|
|
151
|
+
) as Record<Axis, DragCurve>
|
|
152
|
+
const callbacks = {
|
|
153
|
+
onStart: options.onStart,
|
|
154
|
+
onMove: options.onMove,
|
|
155
|
+
onRelease: options.onRelease,
|
|
156
|
+
onCancel: options.onCancel,
|
|
157
|
+
onSettle: options.onSettle,
|
|
158
|
+
}
|
|
159
|
+
for (const callback of Object.values(callbacks))
|
|
160
|
+
if (callback !== undefined && typeof callback !== 'function')
|
|
161
|
+
throw new TypeError('Drag callbacks must be functions.')
|
|
162
|
+
return { axes, curves, callbacks }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Main-thread pan updates; release physics play through Jolt's native animation drivers. */
|
|
166
|
+
export function drag(target: Target, options: DragOptions = {}): Drag {
|
|
167
|
+
const view = toView(target)
|
|
168
|
+
const config = settings(options) // Invalid replacements leave the existing owner intact.
|
|
169
|
+
const previous = owners.get(view)
|
|
170
|
+
const controller = new DragController(view, config)
|
|
171
|
+
owners.set(view, controller)
|
|
172
|
+
// onCancel may synchronously install another drag. The most recent owner wins.
|
|
173
|
+
try {
|
|
174
|
+
previous?.dispose()
|
|
175
|
+
} catch (error) {
|
|
176
|
+
controller.dispose()
|
|
177
|
+
throw error
|
|
178
|
+
}
|
|
179
|
+
return controller
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
class DragController implements Drag {
|
|
183
|
+
private active = false
|
|
184
|
+
private disabled = false
|
|
185
|
+
private destroyed = false
|
|
186
|
+
private background = false
|
|
187
|
+
private attached = false
|
|
188
|
+
private blocked = false
|
|
189
|
+
private generation = 0
|
|
190
|
+
private release: PlaybackAnimation | undefined
|
|
191
|
+
private input = new DragInput()
|
|
192
|
+
private origin = { x: 0, y: 0 }
|
|
193
|
+
private last: DragEvent = { x: 0, y: 0, velocityX: 0, velocityY: 0 }
|
|
194
|
+
private lengths = { x: 200, y: 200 }
|
|
195
|
+
constructor(
|
|
196
|
+
private view: View,
|
|
197
|
+
private config: Settings,
|
|
198
|
+
) {
|
|
199
|
+
view.on('loaded', this.attach)
|
|
200
|
+
view.on('unloaded', this.unload)
|
|
201
|
+
Application.on(Application.suspendEvent, this.suspend)
|
|
202
|
+
Application.on(Application.resumeEvent, this.resume)
|
|
203
|
+
this.attach()
|
|
204
|
+
}
|
|
205
|
+
get dragging() {
|
|
206
|
+
return this.active
|
|
207
|
+
}
|
|
208
|
+
get settling() {
|
|
209
|
+
return !!this.release
|
|
210
|
+
}
|
|
211
|
+
get paused() {
|
|
212
|
+
return this.disabled
|
|
213
|
+
}
|
|
214
|
+
get disposed() {
|
|
215
|
+
return this.destroyed
|
|
216
|
+
}
|
|
217
|
+
get position(): DragPoint {
|
|
218
|
+
return { x: currentValue(this.view, 'x'), y: currentValue(this.view, 'y') }
|
|
219
|
+
}
|
|
220
|
+
cancel = () => {
|
|
221
|
+
const wasActive = this.active
|
|
222
|
+
this.active = false
|
|
223
|
+
this.blocked = true
|
|
224
|
+
this.generation++
|
|
225
|
+
const release = this.release
|
|
226
|
+
this.release = undefined
|
|
227
|
+
release?.stop()
|
|
228
|
+
this.input.reset()
|
|
229
|
+
if (wasActive)
|
|
230
|
+
this.config.callbacks.onCancel?.({ ...this.last, velocityX: 0, velocityY: 0 })
|
|
231
|
+
}
|
|
232
|
+
pause = () => {
|
|
233
|
+
if (this.destroyed || this.disabled) return
|
|
234
|
+
this.disabled = true
|
|
235
|
+
this.detach()
|
|
236
|
+
}
|
|
237
|
+
play = () => {
|
|
238
|
+
if (this.destroyed) return
|
|
239
|
+
this.disabled = false
|
|
240
|
+
this.attach()
|
|
241
|
+
}
|
|
242
|
+
dispose = () => {
|
|
243
|
+
if (this.destroyed) return
|
|
244
|
+
this.destroyed = true
|
|
245
|
+
this.view.off('loaded', this.attach)
|
|
246
|
+
this.view.off('unloaded', this.unload)
|
|
247
|
+
Application.off(Application.suspendEvent, this.suspend)
|
|
248
|
+
Application.off(Application.resumeEvent, this.resume)
|
|
249
|
+
if (owners.get(this.view) === this) owners.delete(this.view)
|
|
250
|
+
this.detach()
|
|
251
|
+
}
|
|
252
|
+
private attach = () => {
|
|
253
|
+
if (
|
|
254
|
+
this.attached ||
|
|
255
|
+
this.destroyed ||
|
|
256
|
+
this.disabled ||
|
|
257
|
+
this.background ||
|
|
258
|
+
!this.view.isLoaded
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
this.blocked = false
|
|
262
|
+
this.attached = true
|
|
263
|
+
this.view.on('touch', this.touch)
|
|
264
|
+
this.view.on('pan', this.pan)
|
|
265
|
+
}
|
|
266
|
+
private detach = () => {
|
|
267
|
+
this.attached = false
|
|
268
|
+
this.view.off('touch', this.touch)
|
|
269
|
+
this.view.off('pan', this.pan)
|
|
270
|
+
this.cancel()
|
|
271
|
+
}
|
|
272
|
+
private unload = () => this.detach()
|
|
273
|
+
private suspend = () => {
|
|
274
|
+
this.background = true
|
|
275
|
+
this.detach()
|
|
276
|
+
}
|
|
277
|
+
private resume = () => {
|
|
278
|
+
this.background = false
|
|
279
|
+
this.attach()
|
|
280
|
+
}
|
|
281
|
+
private touch = (data: EventData) => {
|
|
282
|
+
const event = data as TouchGestureEventData
|
|
283
|
+
if (event.action === 'down') {
|
|
284
|
+
this.cancel()
|
|
285
|
+
this.blocked = false
|
|
286
|
+
} else if (event.action === 'cancel') this.cancel()
|
|
287
|
+
}
|
|
288
|
+
private pan = (data: EventData) => {
|
|
289
|
+
const event = data as PanGestureEventData
|
|
290
|
+
if (!this.attached || this.destroyed || this.disabled || this.background) return
|
|
291
|
+
if (this.view.isEnabled === false) {
|
|
292
|
+
this.cancel()
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
if (event.state === 1) this.input.reset()
|
|
296
|
+
const input = this.input.read(event, this.view)
|
|
297
|
+
if (input.cancelled) {
|
|
298
|
+
this.cancel()
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
if (this.blocked) return
|
|
302
|
+
if (event.state === 1) {
|
|
303
|
+
this.cancel()
|
|
304
|
+
if (!this.attached || this.destroyed || this.disabled) return
|
|
305
|
+
this.blocked = false
|
|
306
|
+
// Claim just the enabled axes, preserving the native presentation on takeover.
|
|
307
|
+
stop(this.view, this.config.axes)
|
|
308
|
+
this.origin = { x: readValue(this.view, 'x'), y: readValue(this.view, 'y') }
|
|
309
|
+
const size =
|
|
310
|
+
(this.view.parent as View | undefined)?.getActualSize() ??
|
|
311
|
+
this.view.getActualSize()
|
|
312
|
+
this.lengths = {
|
|
313
|
+
x: Math.max(1, size.width || 200),
|
|
314
|
+
y: Math.max(1, size.height || 200),
|
|
315
|
+
}
|
|
316
|
+
this.active = true
|
|
317
|
+
this.last = { ...this.origin, velocityX: 0, velocityY: 0 }
|
|
318
|
+
for (const axis of this.config.axes) {
|
|
319
|
+
const curve = this.config.curves[axis]
|
|
320
|
+
if (!curve.rubberband) continue
|
|
321
|
+
const resumed = resumeDrag(this.origin[axis], curve.bounds, this.lengths[axis])
|
|
322
|
+
this.origin[axis] = resumed.origin
|
|
323
|
+
this.lengths[axis] = resumed.length
|
|
324
|
+
}
|
|
325
|
+
this.input.read(event, this.view)
|
|
326
|
+
this.emit(() => this.config.callbacks.onStart?.({ ...this.last }))
|
|
327
|
+
}
|
|
328
|
+
if (!this.active) return
|
|
329
|
+
const generation = this.generation
|
|
330
|
+
if (Number.isFinite(input.x) && Number.isFinite(input.y)) {
|
|
331
|
+
const position = { x: this.last.x, y: this.last.y }
|
|
332
|
+
let vx = 0,
|
|
333
|
+
vy = 0
|
|
334
|
+
for (const axis of this.config.axes) {
|
|
335
|
+
const curve = this.config.curves[axis]
|
|
336
|
+
const raw = this.origin[axis] + input[axis]
|
|
337
|
+
const distance = raw - clampDrag(raw, curve.bounds)
|
|
338
|
+
const resistance =
|
|
339
|
+
distance === 0
|
|
340
|
+
? 1
|
|
341
|
+
: curve.rubberband
|
|
342
|
+
? 0.55 / (1 + Math.abs(distance) / this.lengths[axis]) ** 2
|
|
343
|
+
: 0
|
|
344
|
+
position[axis] = curve.rubberband
|
|
345
|
+
? resistDrag(raw, curve.bounds, this.lengths[axis])
|
|
346
|
+
: clampDrag(raw, curve.bounds)
|
|
347
|
+
if (axis === 'x') vx = input.velocityX * resistance
|
|
348
|
+
else vy = input.velocityY * resistance
|
|
349
|
+
stop(this.view, [axis])
|
|
350
|
+
writeValue(this.view, axis, position[axis])
|
|
351
|
+
}
|
|
352
|
+
this.last = { ...position, velocityX: vx, velocityY: vy }
|
|
353
|
+
if (event.state !== 3)
|
|
354
|
+
this.emit(() => this.config.callbacks.onMove?.({ ...this.last }))
|
|
355
|
+
}
|
|
356
|
+
if (!this.active || generation !== this.generation) return
|
|
357
|
+
if (event.state === 3) this.emit(() => this.finish())
|
|
358
|
+
}
|
|
359
|
+
private emit(callback: () => void) {
|
|
360
|
+
try {
|
|
361
|
+
callback()
|
|
362
|
+
} catch (error) {
|
|
363
|
+
// A failing application callback must not leave live input or an orphaned fling.
|
|
364
|
+
this.active = false
|
|
365
|
+
this.dispose()
|
|
366
|
+
throw error
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
private finish() {
|
|
370
|
+
this.active = false
|
|
371
|
+
const generation = ++this.generation
|
|
372
|
+
const event = { ...this.last }
|
|
373
|
+
const target = { x: event.x, y: event.y }
|
|
374
|
+
const plans: Plan[] = this.config.axes.map((axis) => {
|
|
375
|
+
const curve = {
|
|
376
|
+
...this.config.curves[axis],
|
|
377
|
+
velocity: axis === 'x' ? event.velocityX : event.velocityY,
|
|
378
|
+
}
|
|
379
|
+
target[axis] = solveDragRelease(event[axis], curve).to
|
|
380
|
+
return {
|
|
381
|
+
property: axis,
|
|
382
|
+
keyframes: [null, null],
|
|
383
|
+
transition: { curve, delay: 0, repeat: 0, reverse: false },
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
const animation = new ViewAnimation(this.view, plans)
|
|
387
|
+
this.release = animation
|
|
388
|
+
void animation.finished.then((finished) => {
|
|
389
|
+
if (generation !== this.generation || this.destroyed) return
|
|
390
|
+
this.release = undefined
|
|
391
|
+
if (finished) this.config.callbacks.onSettle?.({ ...target })
|
|
392
|
+
})
|
|
393
|
+
this.emit(() => this.config.callbacks.onRelease?.({ ...event, target, animation }))
|
|
394
|
+
}
|
|
395
|
+
}
|
package/src/driver/ios.ts
CHANGED
|
@@ -5,11 +5,13 @@ import {
|
|
|
5
5
|
presentedIOSDecoration,
|
|
6
6
|
syncIOSDecoration,
|
|
7
7
|
} from './ios-decoration.js'
|
|
8
|
-
import type
|
|
8
|
+
import { Application, type View } from '@nativescript/core'
|
|
9
|
+
import { stopView } from '../engine.js'
|
|
9
10
|
import type { NumericProperty } from '../properties.js'
|
|
10
11
|
import { isColorProperty } from '../color.js'
|
|
11
12
|
import { playIOSColor, presentedIOSColor } from './ios-color.js'
|
|
12
13
|
import { after, type Driver } from './index.js'
|
|
14
|
+
import { hasInteractionSurface } from '../interaction-surface.js'
|
|
13
15
|
|
|
14
16
|
interface Handle {
|
|
15
17
|
stop(): void
|
|
@@ -65,6 +67,40 @@ export function createIOSDriver(): Driver {
|
|
|
65
67
|
const layers = layersOf(view)
|
|
66
68
|
const key = `jolt:${track.property}`
|
|
67
69
|
const rotation = track.property.startsWith('rotate')
|
|
70
|
+
if (hasInteractionSurface(view)) {
|
|
71
|
+
if (typeof JoltInteractiveAnimation === 'undefined')
|
|
72
|
+
throw new Error('Rebuild the iOS app to enable Jolt interaction surfaces.')
|
|
73
|
+
if (track.property === 'rotateX' || track.property === 'rotateY')
|
|
74
|
+
ensurePerspective(view, layers)
|
|
75
|
+
const suspend = () => stopView(view, [track.property])
|
|
76
|
+
const completed = (finished: boolean) => {
|
|
77
|
+
Application.off(Application.suspendEvent, suspend)
|
|
78
|
+
onEnd(finished)
|
|
79
|
+
}
|
|
80
|
+
const native =
|
|
81
|
+
JoltInteractiveAnimation.startViewLayersKeyPathValuesTimesDurationDelayRepeatReverseFinalValueStartedAtCompletion(
|
|
82
|
+
view.nativeViewProtected,
|
|
83
|
+
NSArray.arrayWithArray(layers),
|
|
84
|
+
keyPaths[track.property as NumericProperty],
|
|
85
|
+
NSArray.arrayWithArray(
|
|
86
|
+
track.values.map((value) => (rotation ? radians(value) : value)),
|
|
87
|
+
),
|
|
88
|
+
NSArray.arrayWithArray(track.times as number[]),
|
|
89
|
+
track.duration,
|
|
90
|
+
track.delay,
|
|
91
|
+
track.repeat,
|
|
92
|
+
track.reverse,
|
|
93
|
+
rotation ? radians(track.to) : track.to,
|
|
94
|
+
startedAt,
|
|
95
|
+
completed,
|
|
96
|
+
)
|
|
97
|
+
Application.on(Application.suspendEvent, suspend)
|
|
98
|
+
return {
|
|
99
|
+
elapsed: () => native.elapsed,
|
|
100
|
+
control: (time, paused) => native.controlPaused(time, paused),
|
|
101
|
+
stop: () => native.cancel(),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
68
104
|
const infinite = track.repeat === Infinity
|
|
69
105
|
if (track.property === 'rotateX' || track.property === 'rotateY') {
|
|
70
106
|
ensurePerspective(view, layers)
|
|
@@ -143,14 +179,27 @@ export function createIOSDriver(): Driver {
|
|
|
143
179
|
},
|
|
144
180
|
elapsed: (handle) => (handle as Handle).elapsed(),
|
|
145
181
|
|
|
146
|
-
sync
|
|
182
|
+
sync(view, excluding) {
|
|
183
|
+
syncIOSDecoration(view, excluding)
|
|
184
|
+
if (view.nativeViewProtected && typeof JoltInteractiveAnimation !== 'undefined')
|
|
185
|
+
JoltInteractiveAnimation.syncViewExcluding(
|
|
186
|
+
view.nativeViewProtected,
|
|
187
|
+
excluding ? (keyPaths[excluding as NumericProperty] ?? null) : null,
|
|
188
|
+
)
|
|
189
|
+
},
|
|
147
190
|
|
|
148
|
-
presented
|
|
149
|
-
isDecorationProperty(property)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
191
|
+
presented(view, property) {
|
|
192
|
+
if (isDecorationProperty(property)) return presentedIOSDecoration(view, property)
|
|
193
|
+
if (isColorProperty(property)) return presentedIOSColor(view, property)
|
|
194
|
+
if (!view.nativeViewProtected || typeof JoltInteractiveAnimation === 'undefined')
|
|
195
|
+
return undefined
|
|
196
|
+
const value = JoltInteractiveAnimation.valueForViewKeyPath(
|
|
197
|
+
view.nativeViewProtected,
|
|
198
|
+
keyPaths[property],
|
|
199
|
+
)
|
|
200
|
+
if (value === null || value === undefined) return undefined
|
|
201
|
+
return property.startsWith('rotate') ? (value * 180) / Math.PI : Number(value)
|
|
202
|
+
},
|
|
154
203
|
|
|
155
204
|
after,
|
|
156
205
|
|
package/src/engine.ts
CHANGED
|
@@ -436,6 +436,17 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
/** Current native presentation when available, otherwise the active curve or model. */
|
|
440
|
+
export function currentValue(view: View, property: Property): number {
|
|
441
|
+
const entry = running.get(view)?.get(property)
|
|
442
|
+
if (!entry) return readValue(view, property)
|
|
443
|
+
const driver = getDriver()
|
|
444
|
+
return (
|
|
445
|
+
driver.presented(view, property) ??
|
|
446
|
+
entry.track.valueAt(elapsedOf(entry, driver.now()))
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
439
450
|
/** Stops whatever is animating these properties of the view, or all of them. */
|
|
440
451
|
export function stopView(view: View, properties?: readonly Property[]) {
|
|
441
452
|
const tokens = revisions.get(view)
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,10 @@ export { animate, decay, stop } from './animate.js'
|
|
|
2
2
|
export { sequence } from './sequence.js'
|
|
3
3
|
export type { Step, SequenceOptions, SequencePosition } from './sequence.js'
|
|
4
4
|
export { press } from './press.js'
|
|
5
|
+
export { interactionSurface } from './interaction-surface.js'
|
|
6
|
+
export type { InteractionSurface } from './interaction-surface.js'
|
|
7
|
+
export { drag } from './drag.js'
|
|
8
|
+
export type { Drag, DragOptions, DragPoint, DragEvent, DragRelease } from './drag.js'
|
|
5
9
|
export { scrollLinked } from './scroll-linked.js'
|
|
6
10
|
export type { ScrollLinkedOptions } from './scroll-linked.js'
|
|
7
11
|
export { animateItems } from './items.js'
|
|
@@ -50,3 +54,13 @@ export type { PlaybackAnimation } from './playback.js'
|
|
|
50
54
|
export type { Animation } from './engine.js'
|
|
51
55
|
export type { ColorValue, ColorKeyframes } from './color.js'
|
|
52
56
|
export type * from './types.js'
|
|
57
|
+
export { motionScope } from './motion-scope.js'
|
|
58
|
+
export type { MotionScope, MotionResource } from './motion-scope.js'
|
|
59
|
+
export { motionStates } from './motion-states.js'
|
|
60
|
+
export type {
|
|
61
|
+
MotionStates,
|
|
62
|
+
MotionStatesOptions,
|
|
63
|
+
MotionState,
|
|
64
|
+
MotionValues,
|
|
65
|
+
StateTransition,
|
|
66
|
+
} from './motion-states.js'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { View, ViewBase } from '@nativescript/core'
|
|
2
|
+
import { toView } from './animate.js'
|
|
3
|
+
import type { Target } from './types.js'
|
|
4
|
+
|
|
5
|
+
const surfaces = new WeakMap<ViewBase, Set<object>>()
|
|
6
|
+
export interface InteractionSurface {
|
|
7
|
+
readonly disposed: boolean
|
|
8
|
+
/** Existing animations retain their driver until they end. Disables future opt-in. */
|
|
9
|
+
dispose(): void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Opt a view/subtree into native geometry updates before starting its animations. */
|
|
13
|
+
export function interactionSurface(target: Target): InteractionSurface {
|
|
14
|
+
const view = toView(target)
|
|
15
|
+
let tokens = surfaces.get(view)
|
|
16
|
+
if (!tokens) surfaces.set(view, (tokens = new Set()))
|
|
17
|
+
const token = {}
|
|
18
|
+
tokens.add(token)
|
|
19
|
+
let disposed = false
|
|
20
|
+
return {
|
|
21
|
+
get disposed() {
|
|
22
|
+
return disposed
|
|
23
|
+
},
|
|
24
|
+
dispose() {
|
|
25
|
+
if (disposed) return
|
|
26
|
+
disposed = true
|
|
27
|
+
tokens.delete(token)
|
|
28
|
+
if (!tokens.size) surfaces.delete(view)
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Evaluated at animation start; no per-frame JS traversal. */
|
|
34
|
+
export function hasInteractionSurface(view: View): boolean {
|
|
35
|
+
let current: ViewBase | undefined = view
|
|
36
|
+
while (current) {
|
|
37
|
+
if (surfaces.get(current)?.size) return true
|
|
38
|
+
current = current.parent
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
}
|
package/src/ios-plugin.d.ts
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
declare class JoltInteractiveAnimation extends NSObject {
|
|
2
|
+
static startViewLayersKeyPathValuesTimesDurationDelayRepeatReverseFinalValueStartedAtCompletion(
|
|
3
|
+
view: UIView,
|
|
4
|
+
layers: NSArray<CALayer>,
|
|
5
|
+
keyPath: string,
|
|
6
|
+
values: NSArray<number>,
|
|
7
|
+
times: NSArray<number>,
|
|
8
|
+
duration: number,
|
|
9
|
+
delay: number,
|
|
10
|
+
repeat: number,
|
|
11
|
+
reverse: boolean,
|
|
12
|
+
finalValue: number,
|
|
13
|
+
startedAt: number,
|
|
14
|
+
completion: (completed: boolean) => void,
|
|
15
|
+
): JoltInteractiveAnimation
|
|
16
|
+
static syncViewExcluding(view: UIView, keyPath: string | null): void
|
|
17
|
+
static valueForViewKeyPath(view: UIView, keyPath: string): number | null
|
|
18
|
+
static activeCount(): number
|
|
19
|
+
readonly elapsed: number
|
|
20
|
+
controlPaused(elapsed: number, paused: boolean): void
|
|
21
|
+
cancel(): void
|
|
22
|
+
}
|
|
1
23
|
declare class JoltImageEffect extends NSObject {
|
|
2
24
|
stopPreservingImage(): void
|
|
3
25
|
static createWithViewDestinationModeOriginXOriginYAmplitudeRadiusDurationSeedListener(
|