@rafaelaugusto/jolt 0.1.0-beta.1 → 0.1.0-beta.10
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 +367 -18
- package/dist/animate.d.ts +19 -3
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +186 -14
- package/dist/animate.js.map +1 -1
- package/dist/color.d.ts +11 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +73 -0
- package/dist/color.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +73 -0
- package/dist/config.js.map +1 -0
- package/dist/decay.d.ts +11 -0
- package/dist/decay.d.ts.map +1 -0
- package/dist/decay.js +22 -0
- package/dist/decay.js.map +1 -0
- package/dist/decoration.d.ts +18 -0
- package/dist/decoration.d.ts.map +1 -0
- package/dist/decoration.js +155 -0
- package/dist/decoration.js.map +1 -0
- 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/android-color.d.ts +20 -0
- package/dist/driver/android-color.d.ts.map +1 -0
- package/dist/driver/android-color.js +136 -0
- package/dist/driver/android-color.js.map +1 -0
- package/dist/driver/android-decoration.d.ts +22 -0
- package/dist/driver/android-decoration.d.ts.map +1 -0
- package/dist/driver/android-decoration.js +139 -0
- package/dist/driver/android-decoration.js.map +1 -0
- package/dist/driver/android-playback.d.ts +13 -0
- package/dist/driver/android-playback.d.ts.map +1 -0
- package/dist/driver/android-playback.js +141 -0
- package/dist/driver/android-playback.js.map +1 -0
- package/dist/driver/android.d.ts +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +165 -13
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +25 -2
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.d.ts +16 -0
- package/dist/driver/ios-color.d.ts.map +1 -0
- package/dist/driver/ios-color.js +392 -0
- package/dist/driver/ios-color.js.map +1 -0
- package/dist/driver/ios-decoration.d.ts +17 -0
- package/dist/driver/ios-decoration.d.ts.map +1 -0
- package/dist/driver/ios-decoration.js +271 -0
- package/dist/driver/ios-decoration.js.map +1 -0
- package/dist/driver/ios-layout.d.ts +9 -0
- package/dist/driver/ios-layout.d.ts.map +1 -0
- package/dist/driver/ios-layout.js +52 -0
- package/dist/driver/ios-layout.js.map +1 -0
- package/dist/driver/ios-playback.d.ts +10 -0
- package/dist/driver/ios-playback.d.ts.map +1 -0
- package/dist/driver/ios-playback.js +47 -0
- package/dist/driver/ios-playback.js.map +1 -0
- package/dist/driver/ios-shadow.d.ts +6 -0
- package/dist/driver/ios-shadow.d.ts.map +1 -0
- package/dist/driver/ios-shadow.js +52 -0
- package/dist/driver/ios-shadow.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +163 -28
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +75 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +293 -23
- package/dist/engine.js.map +1 -1
- package/dist/haptics.d.ts +21 -0
- package/dist/haptics.d.ts.map +1 -0
- package/dist/haptics.js +120 -0
- package/dist/haptics.js.map +1 -0
- 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 +43 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- 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/items.d.ts +20 -0
- package/dist/items.d.ts.map +1 -0
- package/dist/items.js +109 -0
- package/dist/items.js.map +1 -0
- package/dist/keyboard.d.ts +41 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +256 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/label.d.ts +7 -0
- package/dist/label.d.ts.map +1 -0
- package/dist/label.js +60 -0
- package/dist/label.js.map +1 -0
- package/dist/layout.d.ts +13 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +348 -0
- package/dist/layout.js.map +1 -0
- package/dist/liquid-grid.d.ts +3 -0
- package/dist/liquid-grid.d.ts.map +1 -0
- package/dist/liquid-grid.js +24 -0
- package/dist/liquid-grid.js.map +1 -0
- package/dist/liquid.d.ts +18 -0
- package/dist/liquid.d.ts.map +1 -0
- package/dist/liquid.js +234 -0
- package/dist/liquid.js.map +1 -0
- package/dist/loader.d.ts +19 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +173 -0
- package/dist/loader.js.map +1 -0
- package/dist/motion-scope.d.ts +39 -0
- package/dist/motion-scope.d.ts.map +1 -0
- package/dist/motion-scope.js +238 -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/particles.d.ts +39 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +265 -0
- package/dist/particles.js.map +1 -0
- package/dist/path.d.ts +12 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +90 -0
- package/dist/path.js.map +1 -0
- package/dist/playback-group.d.ts +6 -0
- package/dist/playback-group.d.ts.map +1 -0
- package/dist/playback-group.js +141 -0
- package/dist/playback-group.js.map +1 -0
- package/dist/playback.d.ts +25 -0
- package/dist/playback.d.ts.map +1 -0
- package/dist/playback.js +35 -0
- package/dist/playback.js.map +1 -0
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +23 -7
- package/dist/press.js.map +1 -1
- package/dist/properties.d.ts +6 -3
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +26 -2
- package/dist/properties.js.map +1 -1
- package/dist/reveal.d.ts +25 -0
- package/dist/reveal.d.ts.map +1 -0
- package/dist/reveal.js +294 -0
- package/dist/reveal.js.map +1 -0
- package/dist/roll-diff.d.ts +22 -0
- package/dist/roll-diff.d.ts.map +1 -0
- package/dist/roll-diff.js +36 -0
- package/dist/roll-diff.js.map +1 -0
- package/dist/roll.d.ts +29 -0
- package/dist/roll.d.ts.map +1 -0
- package/dist/roll.js +442 -0
- package/dist/roll.js.map +1 -0
- package/dist/scroll-linked.d.ts +10 -0
- package/dist/scroll-linked.d.ts.map +1 -0
- package/dist/scroll-linked.js +58 -0
- package/dist/scroll-linked.js.map +1 -0
- package/dist/sequence.d.ts +10 -5
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +208 -14
- package/dist/sequence.js.map +1 -1
- package/dist/shimmer.d.ts +12 -0
- package/dist/shimmer.d.ts.map +1 -0
- package/dist/shimmer.js +161 -0
- package/dist/shimmer.js.map +1 -0
- package/dist/svg-path.d.ts +14 -0
- package/dist/svg-path.d.ts.map +1 -0
- package/dist/svg-path.js +281 -0
- package/dist/svg-path.js.map +1 -0
- package/dist/symbol-effect.d.ts +23 -0
- package/dist/symbol-effect.d.ts.map +1 -0
- package/dist/symbol-effect.js +187 -0
- package/dist/symbol-effect.js.map +1 -0
- package/dist/text-layout.d.ts +30 -0
- package/dist/text-layout.d.ts.map +1 -0
- package/dist/text-layout.js +185 -0
- package/dist/text-layout.js.map +1 -0
- package/dist/text.d.ts +17 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +263 -0
- package/dist/text.js.map +1 -0
- package/dist/track.d.ts +3 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +266 -13
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +17 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +41 -6
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +48 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +95 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +163 -0
- package/dist/vue/index.js.map +1 -0
- 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 +29 -5
- package/platforms/android/include.gradle +7 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
- package/platforms/android/proguard-rules.pro +19 -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 +155 -0
- package/src/animate.ts +249 -36
- package/src/color.ts +101 -0
- package/src/config.ts +84 -0
- package/src/decay.ts +45 -0
- package/src/decoration.ts +180 -0
- package/src/drag-input.ts +74 -0
- package/src/drag-physics.ts +110 -0
- package/src/drag.ts +395 -0
- package/src/driver/android-color.ts +170 -0
- package/src/driver/android-decoration.ts +182 -0
- package/src/driver/android-playback.ts +157 -0
- package/src/driver/android.ts +208 -20
- package/src/driver/index.ts +37 -2
- package/src/driver/ios-color.ts +441 -0
- package/src/driver/ios-decoration.ts +332 -0
- package/src/driver/ios-layout.ts +63 -0
- package/src/driver/ios-playback.ts +51 -0
- package/src/driver/ios-shadow.ts +50 -0
- package/src/driver/ios.ts +193 -33
- package/src/engine.ts +321 -27
- package/src/haptics.ts +148 -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 +59 -2
- package/src/interaction-surface.ts +41 -0
- package/src/ios-plugin.d.ts +81 -0
- package/src/items.ts +140 -0
- package/src/keyboard.ts +291 -0
- package/src/label.ts +76 -0
- package/src/layout.ts +408 -0
- package/src/liquid-grid.ts +28 -0
- package/src/liquid.ts +287 -0
- package/src/loader.ts +202 -0
- package/src/motion-scope.ts +279 -0
- package/src/motion-states.ts +215 -0
- package/src/particles.ts +323 -0
- package/src/path.ts +124 -0
- package/src/playback-group.ts +143 -0
- package/src/playback.ts +62 -0
- package/src/press.ts +26 -10
- package/src/properties.ts +32 -6
- package/src/reveal.ts +374 -0
- package/src/roll-diff.ts +57 -0
- package/src/roll.ts +515 -0
- package/src/scroll-linked.ts +73 -0
- package/src/sequence.ts +204 -14
- package/src/shimmer.ts +180 -0
- package/src/svg-path.ts +355 -0
- package/src/symbol-effect.ts +226 -0
- package/src/text-layout.ts +228 -0
- package/src/text.ts +279 -0
- package/src/track.ts +347 -12
- package/src/transition.ts +63 -6
- package/src/types.ts +49 -1
- package/src/vue/index.ts +231 -0
- package/src/zoom-native.ts +124 -0
- package/src/zoomable.ts +330 -0
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
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { type View } from '@nativescript/core'
|
|
2
|
+
import { readColor, type ColorProperty } from '../color.js'
|
|
3
|
+
import { androidPlayback, type AndroidPlayback } from './android-playback.js'
|
|
4
|
+
import type { PlaybackCommand } from '../playback.js'
|
|
5
|
+
import type { Track } from '../track.js'
|
|
6
|
+
|
|
7
|
+
interface NativeTarget {
|
|
8
|
+
getColor(): number
|
|
9
|
+
setColor(value: number): void
|
|
10
|
+
isSupported(): boolean
|
|
11
|
+
}
|
|
12
|
+
interface ColorHandle {
|
|
13
|
+
control(elapsed: number, paused: boolean, command?: PlaybackCommand): void
|
|
14
|
+
cancel(): void
|
|
15
|
+
elapsed(): number
|
|
16
|
+
target: NativeTarget
|
|
17
|
+
}
|
|
18
|
+
const playing = new WeakMap<View, Map<ColorProperty, ColorHandle>>()
|
|
19
|
+
|
|
20
|
+
export function syncAndroidColor(view: View) {
|
|
21
|
+
for (const handle of playing.get(view)?.values() ?? [])
|
|
22
|
+
handle.target.setColor(handle.target.getColor())
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function presentedAndroidColor(view: View, property: ColorProperty): number {
|
|
26
|
+
const active = playing.get(view)?.get(property)
|
|
27
|
+
if (active) return active.target.getColor() >>> 0
|
|
28
|
+
if (property === 'color' && view.nativeViewProtected instanceof android.widget.TextView)
|
|
29
|
+
return view.nativeViewProtected.getCurrentTextColor() >>> 0
|
|
30
|
+
return readColor(view, property)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function playAndroidColor(
|
|
34
|
+
view: View,
|
|
35
|
+
track: Track,
|
|
36
|
+
onEnd: (finished?: boolean) => void,
|
|
37
|
+
): ColorHandle {
|
|
38
|
+
const property = track.property as ColorProperty
|
|
39
|
+
const changes =
|
|
40
|
+
property === 'borderColor'
|
|
41
|
+
? [
|
|
42
|
+
'borderTopColorChange',
|
|
43
|
+
'borderRightColorChange',
|
|
44
|
+
'borderBottomColorChange',
|
|
45
|
+
'borderLeftColorChange',
|
|
46
|
+
]
|
|
47
|
+
: [`${property}Change`]
|
|
48
|
+
const bridge = globalThis as unknown as {
|
|
49
|
+
org: {
|
|
50
|
+
rafaelaugusto: {
|
|
51
|
+
jolt: {
|
|
52
|
+
ColorTarget: new (
|
|
53
|
+
view: android.view.View,
|
|
54
|
+
property: number,
|
|
55
|
+
color: number,
|
|
56
|
+
) => NativeTarget
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let target: NativeTarget
|
|
62
|
+
try {
|
|
63
|
+
target = new bridge.org.rafaelaugusto.jolt.ColorTarget(
|
|
64
|
+
view.nativeViewProtected,
|
|
65
|
+
property === 'backgroundColor' ? 0 : property === 'borderColor' ? 1 : 2,
|
|
66
|
+
track.values[0]! | 0,
|
|
67
|
+
)
|
|
68
|
+
} catch {
|
|
69
|
+
// A JS-only update cannot install the native plugin. Keep the final model.
|
|
70
|
+
onEnd(false)
|
|
71
|
+
return {
|
|
72
|
+
cancel() {},
|
|
73
|
+
control() {},
|
|
74
|
+
elapsed: () => 0,
|
|
75
|
+
target: {
|
|
76
|
+
getColor: () => readColor(view, property),
|
|
77
|
+
setColor() {},
|
|
78
|
+
isSupported: () => false,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const frames = Array.create(android.animation.Keyframe, track.values.length)
|
|
83
|
+
track.values.forEach((value, index) => {
|
|
84
|
+
frames[index] = android.animation.Keyframe.ofInt(track.times[index]!, value | 0)
|
|
85
|
+
})
|
|
86
|
+
const holder = android.animation.PropertyValuesHolder.ofKeyframe('color', frames)
|
|
87
|
+
holder.setEvaluator(new android.animation.ArgbEvaluator())
|
|
88
|
+
const holders = Array.create(android.animation.PropertyValuesHolder, 1)
|
|
89
|
+
holders[0] = holder
|
|
90
|
+
const animator = android.animation.ObjectAnimator.ofPropertyValuesHolder(
|
|
91
|
+
target as unknown as java.lang.Object,
|
|
92
|
+
holders,
|
|
93
|
+
)
|
|
94
|
+
animator.setDuration(Math.max(track.duration, 1))
|
|
95
|
+
animator.setStartDelay(track.delay)
|
|
96
|
+
animator.setInterpolator(new android.view.animation.LinearInterpolator())
|
|
97
|
+
if (track.repeat > 0) {
|
|
98
|
+
animator.setRepeatCount(
|
|
99
|
+
Number.isFinite(track.repeat)
|
|
100
|
+
? track.repeat
|
|
101
|
+
: android.animation.ValueAnimator.INFINITE,
|
|
102
|
+
)
|
|
103
|
+
animator.setRepeatMode(
|
|
104
|
+
track.reverse
|
|
105
|
+
? android.animation.ValueAnimator.REVERSE
|
|
106
|
+
: android.animation.ValueAnimator.RESTART,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
let stopped = false,
|
|
110
|
+
queued = false
|
|
111
|
+
const cleanup = () => {
|
|
112
|
+
if (stopped) return
|
|
113
|
+
stopped = true
|
|
114
|
+
view.off('unloaded', cancel)
|
|
115
|
+
if (property === 'color') {
|
|
116
|
+
view.off('textChange', invalidate)
|
|
117
|
+
view.off('formattedTextChange', invalidate)
|
|
118
|
+
}
|
|
119
|
+
for (const name of changes) view.style.off(name, invalidate)
|
|
120
|
+
if (playing.get(view)?.get(property) === handle) playing.get(view)?.delete(property)
|
|
121
|
+
}
|
|
122
|
+
let player: AndroidPlayback | undefined
|
|
123
|
+
const cancel = () => {
|
|
124
|
+
if (stopped) return
|
|
125
|
+
cleanup()
|
|
126
|
+
player?.cancel()
|
|
127
|
+
onEnd(false)
|
|
128
|
+
}
|
|
129
|
+
const invalidate = () => {
|
|
130
|
+
if (stopped || queued) return
|
|
131
|
+
queued = true
|
|
132
|
+
void Promise.resolve().then(() => {
|
|
133
|
+
queued = false
|
|
134
|
+
if (!stopped) cancel()
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
const handle: ColorHandle = {
|
|
138
|
+
target,
|
|
139
|
+
elapsed: () => player?.elapsed() ?? 0,
|
|
140
|
+
control(time, paused, command) {
|
|
141
|
+
player?.control(time, paused, command)
|
|
142
|
+
},
|
|
143
|
+
cancel() {
|
|
144
|
+
cleanup()
|
|
145
|
+
player?.cancel()
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
let entries = playing.get(view)
|
|
149
|
+
if (!entries) playing.set(view, (entries = new Map()))
|
|
150
|
+
entries.set(property, handle)
|
|
151
|
+
view.on('unloaded', cancel)
|
|
152
|
+
if (property === 'color') {
|
|
153
|
+
view.on('textChange', invalidate)
|
|
154
|
+
view.on('formattedTextChange', invalidate)
|
|
155
|
+
}
|
|
156
|
+
for (const name of changes) view.style.on(name, invalidate)
|
|
157
|
+
if (target.isSupported()) {
|
|
158
|
+
target.setColor(track.values[0]! | 0)
|
|
159
|
+
player = androidPlayback(animator, track, () => {
|
|
160
|
+
if (!stopped) {
|
|
161
|
+
cleanup()
|
|
162
|
+
onEnd()
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
} else {
|
|
166
|
+
cleanup()
|
|
167
|
+
onEnd(false)
|
|
168
|
+
}
|
|
169
|
+
return handle
|
|
170
|
+
}
|