@rafaelaugusto/jolt 0.1.0-beta.6 → 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 +21 -4
- 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/image-effect-native.d.ts +26 -0
- package/dist/image-effect-native.d.ts.map +1 -0
- package/dist/image-effect-native.js +73 -0
- package/dist/image-effect-native.js.map +1 -0
- package/dist/image-effect.d.ts +50 -0
- package/dist/image-effect.d.ts.map +1 -0
- package/dist/image-effect.js +290 -0
- package/dist/image-effect.js.map +1 -0
- package/dist/image-owner.d.ts +3 -0
- package/dist/image-owner.d.ts.map +1 -0
- package/dist/image-owner.js +11 -0
- package/dist/image-owner.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -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/liquid.d.ts.map +1 -1
- package/dist/liquid.js +4 -0
- package/dist/liquid.js.map +1 -1
- 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/dist/zoom-native.d.ts +30 -0
- package/dist/zoom-native.d.ts.map +1 -0
- package/dist/zoom-native.js +56 -0
- package/dist/zoom-native.js.map +1 -0
- package/dist/zoomable.d.ts +48 -0
- package/dist/zoomable.d.ts.map +1 -0
- package/dist/zoomable.js +249 -0
- package/dist/zoomable.js.map +1 -0
- package/package.json +2 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
- package/platforms/android/proguard-rules.pro +4 -0
- package/platforms/ios/build.xcconfig +1 -0
- package/platforms/ios/src/JoltImageEffect.h +25 -0
- package/platforms/ios/src/JoltImageEffect.mm +242 -0
- package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
- package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
- package/platforms/ios/src/JoltPhotoZoom.h +23 -0
- package/platforms/ios/src/JoltPhotoZoom.m +229 -0
- package/platforms/ios/src/module.modulemap +6 -0
- package/src/android-plugin.d.ts +78 -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/image-effect-native.ts +129 -0
- package/src/image-effect.ts +336 -0
- package/src/image-owner.ts +12 -0
- package/src/index.ts +23 -0
- package/src/interaction-surface.ts +41 -0
- package/src/ios-plugin.d.ts +81 -0
- package/src/liquid.ts +4 -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/zoom-native.ts +124 -0
- package/src/zoomable.ts +330 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { solveDecay } from './decay.js'
|
|
2
|
+
import { solveSpring, springPhysics } from './spring.js'
|
|
3
|
+
|
|
4
|
+
export interface DragCurve {
|
|
5
|
+
type: 'drag'
|
|
6
|
+
velocity: number
|
|
7
|
+
friction: number
|
|
8
|
+
bounds: readonly [number, number]
|
|
9
|
+
snap?: readonly number[]
|
|
10
|
+
rubberband: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const clampDrag = (value: number, [min, max]: readonly [number, number]) =>
|
|
14
|
+
Math.max(min, Math.min(max, value))
|
|
15
|
+
|
|
16
|
+
/** Bounded resistance; coordinates and the resistance length are DIPs. */
|
|
17
|
+
export function resistDrag(
|
|
18
|
+
value: number,
|
|
19
|
+
bounds: readonly [number, number],
|
|
20
|
+
length: number,
|
|
21
|
+
) {
|
|
22
|
+
const edge = clampDrag(value, bounds)
|
|
23
|
+
const distance = value - edge
|
|
24
|
+
return edge + (distance / (1 + Math.abs(distance) / length)) * 0.55
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Recover the finger-space origin when grabbing an already rubberbanded pose. */
|
|
28
|
+
export function resumeDrag(
|
|
29
|
+
value: number,
|
|
30
|
+
bounds: readonly [number, number],
|
|
31
|
+
length: number,
|
|
32
|
+
) {
|
|
33
|
+
const edge = clampDrag(value, bounds)
|
|
34
|
+
const distance = value - edge
|
|
35
|
+
// A native edge spring or external owner can exceed the usual resistance range.
|
|
36
|
+
// Expand it for this gesture so every finite visible pose has a stable inverse.
|
|
37
|
+
length = Math.max(length, Math.abs(distance) * 4)
|
|
38
|
+
return { origin: edge + distance / (0.55 - Math.abs(distance) / length), length }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function dragDestination(from: number, curve: DragCurve) {
|
|
42
|
+
const projected = solveDecay(from, curve.velocity, curve.friction, 0.25).to
|
|
43
|
+
if (curve.snap)
|
|
44
|
+
return curve.snap.reduce((best, point) =>
|
|
45
|
+
Math.abs(point - projected) < Math.abs(best - projected) ? point : best,
|
|
46
|
+
)
|
|
47
|
+
return clampDrag(projected, curve.bounds)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One precomputed motion. Native keyframes play both the coast and edge recovery. */
|
|
51
|
+
export function solveDragRelease(from: number, curve: DragCurve) {
|
|
52
|
+
const to = dragDestination(from, curve)
|
|
53
|
+
const coast = solveDecay(from, curve.velocity, curve.friction, 0.25)
|
|
54
|
+
const edge = clampDrag(from, curve.bounds)
|
|
55
|
+
const springTo = (start: number, target: number, velocity: number) => {
|
|
56
|
+
const motion = solveSpring(
|
|
57
|
+
springPhysics({ duration: 500, bounce: 0 }),
|
|
58
|
+
start,
|
|
59
|
+
target,
|
|
60
|
+
velocity,
|
|
61
|
+
0.25,
|
|
62
|
+
)
|
|
63
|
+
const position = (time: number) => {
|
|
64
|
+
const value = time >= motion.duration ? target : motion.position(time)
|
|
65
|
+
return curve.rubberband ? value : clampDrag(value, curve.bounds)
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
...motion,
|
|
69
|
+
position,
|
|
70
|
+
velocity: (time: number) => {
|
|
71
|
+
const raw = motion.position(time)
|
|
72
|
+
return time >= motion.duration ||
|
|
73
|
+
(!curve.rubberband && raw !== clampDrag(raw, curve.bounds))
|
|
74
|
+
? 0
|
|
75
|
+
: motion.velocity(time)
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (curve.snap || from !== edge)
|
|
80
|
+
return {
|
|
81
|
+
...springTo(from, curve.snap ? to : edge, curve.velocity),
|
|
82
|
+
to: curve.snap ? to : edge,
|
|
83
|
+
}
|
|
84
|
+
if (to === coast.to) return coast
|
|
85
|
+
|
|
86
|
+
// Reach the boundary with the decay's actual velocity, then absorb that velocity.
|
|
87
|
+
const rate = -Math.log(curve.friction)
|
|
88
|
+
const collision = Math.max(
|
|
89
|
+
0,
|
|
90
|
+
-Math.log1p((-(to - from) * rate * 1000) / curve.velocity) / rate,
|
|
91
|
+
)
|
|
92
|
+
const recovery = springTo(to, to, coast.velocity(collision))
|
|
93
|
+
const duration = collision + (curve.rubberband ? recovery.duration : 0)
|
|
94
|
+
return {
|
|
95
|
+
to,
|
|
96
|
+
duration,
|
|
97
|
+
position: (time: number) =>
|
|
98
|
+
time >= duration
|
|
99
|
+
? to
|
|
100
|
+
: time < collision
|
|
101
|
+
? coast.position(time)
|
|
102
|
+
: recovery.position(time - collision),
|
|
103
|
+
velocity: (time: number) =>
|
|
104
|
+
time >= duration
|
|
105
|
+
? 0
|
|
106
|
+
: time < collision
|
|
107
|
+
? coast.velocity(time)
|
|
108
|
+
: recovery.velocity(time - collision),
|
|
109
|
+
}
|
|
110
|
+
}
|
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)
|