@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/engine.ts
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createClock,
|
|
3
|
+
validateProgress,
|
|
4
|
+
type PlaybackAnimation,
|
|
5
|
+
type PlaybackClock,
|
|
6
|
+
type PlaybackCommand,
|
|
7
|
+
} from './playback.js'
|
|
1
8
|
import type { View } from '@nativescript/core'
|
|
2
9
|
import { getDriver } from './driver/index.js'
|
|
10
|
+
import { playHaptics, type HapticOptions } from './haptics.js'
|
|
3
11
|
import { readValue, writeValue, type Property } from './properties.js'
|
|
4
12
|
import { createTrack, type Track } from './track.js'
|
|
5
13
|
import type { ResolvedTransition } from './transition.js'
|
|
14
|
+
import { isColorProperty } from './color.js'
|
|
15
|
+
import type { PreparedPath, PathProperty } from './path.js'
|
|
6
16
|
|
|
7
17
|
export interface Animation extends PromiseLike<boolean> {
|
|
8
18
|
/**
|
|
9
19
|
* Resolves with `true` once the animation played to the end, or with `false`
|
|
10
|
-
* if it was stopped or
|
|
11
|
-
*
|
|
20
|
+
* if it was stopped, superseded or skipped.
|
|
21
|
+
* Playback cancellation never rejects; sequences can reject setup/callback errors.
|
|
12
22
|
*/
|
|
13
23
|
readonly finished: Promise<boolean>
|
|
14
|
-
/** Stops
|
|
24
|
+
/** Stops playback. Each effect documents whether it preserves or restores its visual state. */
|
|
15
25
|
stop(): void
|
|
16
26
|
}
|
|
17
27
|
|
|
@@ -21,6 +31,7 @@ export interface Plan {
|
|
|
21
31
|
keyframes: readonly (number | null)[]
|
|
22
32
|
transition: ResolvedTransition
|
|
23
33
|
relative?: boolean
|
|
34
|
+
path?: PreparedPath
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
interface Entry {
|
|
@@ -29,11 +40,32 @@ interface Entry {
|
|
|
29
40
|
readonly owner: ViewAnimation
|
|
30
41
|
handle: unknown
|
|
31
42
|
done: boolean
|
|
43
|
+
finished: boolean
|
|
44
|
+
generation: number
|
|
45
|
+
token: symbol
|
|
46
|
+
completedValue?: number
|
|
32
47
|
}
|
|
33
48
|
|
|
49
|
+
const revisions = new WeakMap<View, Map<Property, symbol>>()
|
|
50
|
+
|
|
34
51
|
const running = new WeakMap<View, Map<Property, Entry>>()
|
|
35
52
|
const arrivals = new WeakMap<Animation, Promise<unknown>>()
|
|
36
53
|
|
|
54
|
+
/** Internal forecast for starting a timeline step before another one arrives. */
|
|
55
|
+
export interface ArrivalTiming {
|
|
56
|
+
read(): { remaining: number | undefined; paused: boolean; arrived: boolean }
|
|
57
|
+
subscribe(callback: () => void): () => void
|
|
58
|
+
}
|
|
59
|
+
const arrivalTimings = new WeakMap<Animation, ArrivalTiming>()
|
|
60
|
+
|
|
61
|
+
export function arrivalTimingOf(animation: Animation) {
|
|
62
|
+
return arrivalTimings.get(animation)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function setArrivalTiming(animation: Animation, timing: ArrivalTiming) {
|
|
66
|
+
arrivalTimings.set(animation, timing)
|
|
67
|
+
}
|
|
68
|
+
|
|
37
69
|
/**
|
|
38
70
|
* Resolves once the animation looks done, which for a spring is well before
|
|
39
71
|
* `finished`. Sequences move on to the next step then.
|
|
@@ -46,7 +78,7 @@ export function setArrival(animation: Animation, arrival: Promise<unknown>) {
|
|
|
46
78
|
arrivals.set(animation, arrival)
|
|
47
79
|
}
|
|
48
80
|
|
|
49
|
-
export class ViewAnimation implements
|
|
81
|
+
export class ViewAnimation implements PlaybackAnimation {
|
|
50
82
|
readonly finished: Promise<boolean>
|
|
51
83
|
private resolve!: (finished: boolean) => void
|
|
52
84
|
private readonly entries: Entry[] = []
|
|
@@ -56,13 +88,203 @@ export class ViewAnimation implements Animation {
|
|
|
56
88
|
private cancelWait: (() => void) | undefined
|
|
57
89
|
private arrive!: () => void
|
|
58
90
|
private cancelArrival: (() => void) | undefined
|
|
91
|
+
private haptics: ReturnType<typeof playHaptics>
|
|
92
|
+
private clock: PlaybackClock | undefined
|
|
93
|
+
private _paused = false
|
|
94
|
+
private controlled = false
|
|
95
|
+
private pendingProgress: number | undefined
|
|
96
|
+
private total = 0
|
|
97
|
+
private started = false
|
|
98
|
+
private arrival = 0
|
|
99
|
+
private arrived = false
|
|
100
|
+
private idleListener: (() => void) | undefined
|
|
101
|
+
private readyListener: (() => void) | undefined
|
|
102
|
+
private readonly endless: boolean
|
|
103
|
+
private readonly timingListeners = new Set<() => void>()
|
|
104
|
+
private timingChanged() {
|
|
105
|
+
for (const callback of [...this.timingListeners]) callback()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get paused() {
|
|
109
|
+
return this._paused
|
|
110
|
+
}
|
|
111
|
+
get duration() {
|
|
112
|
+
return this.started ? this.total : this.endless ? Infinity : 0
|
|
113
|
+
}
|
|
114
|
+
get currentTime() {
|
|
115
|
+
return this.time()
|
|
116
|
+
}
|
|
117
|
+
get ready() {
|
|
118
|
+
return this.started || this.settled
|
|
119
|
+
}
|
|
120
|
+
get idle() {
|
|
121
|
+
return this.settled || (this.started && this.pending === 0)
|
|
122
|
+
}
|
|
123
|
+
get progress() {
|
|
124
|
+
if (!Number.isFinite(this.duration))
|
|
125
|
+
throw new RangeError('Progress requires a finite animation.')
|
|
126
|
+
if (this.settled && this.complete) return 1
|
|
127
|
+
return (
|
|
128
|
+
this.pendingProgress ??
|
|
129
|
+
(this.duration ? Math.min(1, this.time() / this.duration) : this.settled ? 1 : 0)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
set progress(value: number) {
|
|
133
|
+
validateProgress(value, this.duration)
|
|
134
|
+
if (this.settled) return
|
|
135
|
+
if (!this.started) {
|
|
136
|
+
this.pendingProgress = value
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
this.seekTime(this.duration * value)
|
|
140
|
+
}
|
|
141
|
+
onIdle(callback: () => void) {
|
|
142
|
+
this.idleListener = callback
|
|
143
|
+
}
|
|
144
|
+
onReady(callback: () => void) {
|
|
145
|
+
this.readyListener = callback
|
|
146
|
+
}
|
|
147
|
+
releaseGroup() {
|
|
148
|
+
this.held = false
|
|
149
|
+
if (this.idle && !this._paused) this.settle()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
pause() {
|
|
153
|
+
if (this.settled || this._paused) return
|
|
154
|
+
this.controlled = true
|
|
155
|
+
this._paused = true
|
|
156
|
+
this.clock?.pause()
|
|
157
|
+
this.cancelArrival?.()
|
|
158
|
+
this.haptics?.pause()
|
|
159
|
+
if (this.started) this.controlTracks(this.time(), 'pause')
|
|
160
|
+
this.timingChanged()
|
|
161
|
+
}
|
|
162
|
+
play() {
|
|
163
|
+
if (this.settled || !this._paused) return
|
|
164
|
+
this.controlled = true
|
|
165
|
+
this._paused = false
|
|
166
|
+
this.clock?.play()
|
|
167
|
+
if (this.started) {
|
|
168
|
+
this.controlTracks(this.time(), 'play')
|
|
169
|
+
this.scheduleArrival()
|
|
170
|
+
this.haptics?.play()
|
|
171
|
+
if (this.pending === 0) this.becameIdle()
|
|
172
|
+
}
|
|
173
|
+
this.timingChanged()
|
|
174
|
+
}
|
|
175
|
+
seekTime(time: number) {
|
|
176
|
+
if (this.settled) return
|
|
177
|
+
this.controlled = true
|
|
178
|
+
const position = Math.max(0, Math.min(this.total, time))
|
|
179
|
+
this.clock?.seek(position)
|
|
180
|
+
if (!this._paused) this.clock?.play()
|
|
181
|
+
for (const entry of this.entries) {
|
|
182
|
+
if (
|
|
183
|
+
entry.done &&
|
|
184
|
+
entry.finished &&
|
|
185
|
+
revisions.get(this.view)?.get(entry.track.property) === entry.token &&
|
|
186
|
+
readValue(this.view, entry.track.property) === entry.completedValue
|
|
187
|
+
) {
|
|
188
|
+
entry.done = false
|
|
189
|
+
entry.finished = false
|
|
190
|
+
this.pending++
|
|
191
|
+
entriesOf(this.view).set(entry.track.property, entry)
|
|
192
|
+
this.playEntry(entry, position)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
this.controlTracks(position)
|
|
196
|
+
this.scheduleArrival()
|
|
197
|
+
this.haptics?.seek()
|
|
198
|
+
if (this.pending === 0) this.becameIdle()
|
|
199
|
+
this.timingChanged()
|
|
200
|
+
}
|
|
201
|
+
elapsed(entry: Entry, now: number) {
|
|
202
|
+
return this.controlled
|
|
203
|
+
? this.time()
|
|
204
|
+
: (getDriver().elapsed?.(entry.handle) ?? now - entry.startedAt)
|
|
205
|
+
}
|
|
206
|
+
private time() {
|
|
207
|
+
return this.clock?.time() ?? 0
|
|
208
|
+
}
|
|
209
|
+
private controlTracks(position: number, command: PlaybackCommand = 'seek') {
|
|
210
|
+
const driver = getDriver()
|
|
211
|
+
for (const entry of this.entries)
|
|
212
|
+
if (!entry.done)
|
|
213
|
+
driver.control?.(this.view, entry.handle, position, this._paused, command)
|
|
214
|
+
}
|
|
215
|
+
private scheduleArrival() {
|
|
216
|
+
this.cancelArrival?.()
|
|
217
|
+
this.cancelArrival = undefined
|
|
218
|
+
if (
|
|
219
|
+
this.pending === 0 ||
|
|
220
|
+
this.arrived ||
|
|
221
|
+
this._paused ||
|
|
222
|
+
!Number.isFinite(this.arrival) ||
|
|
223
|
+
this.total <= this.arrival
|
|
224
|
+
)
|
|
225
|
+
return
|
|
226
|
+
const remaining = this.arrival - this.time()
|
|
227
|
+
if (remaining <= 0) {
|
|
228
|
+
this.arrived = true
|
|
229
|
+
this.arrive()
|
|
230
|
+
this.timingChanged()
|
|
231
|
+
} else
|
|
232
|
+
this.cancelArrival = getDriver().after(remaining, () => {
|
|
233
|
+
this.arrived = true
|
|
234
|
+
this.arrive()
|
|
235
|
+
this.timingChanged()
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
private becameIdle() {
|
|
239
|
+
this.clock?.pause()
|
|
240
|
+
this.idleListener?.()
|
|
241
|
+
if (!this.complete || (!this.held && !this._paused)) this.settle()
|
|
242
|
+
}
|
|
243
|
+
private playEntry(entry: Entry, elapsed = 0) {
|
|
244
|
+
const initial = entry.generation === 0
|
|
245
|
+
const instance = ++entry.generation
|
|
246
|
+
const driver = getDriver()
|
|
247
|
+
entry.handle = driver.play(
|
|
248
|
+
this.view,
|
|
249
|
+
entry.track,
|
|
250
|
+
initial ? entry.startedAt : driver.now() - elapsed,
|
|
251
|
+
(finished = true) => {
|
|
252
|
+
if (entry.done || instance !== entry.generation) return
|
|
253
|
+
entry.done = true
|
|
254
|
+
entry.finished = finished
|
|
255
|
+
entry.completedValue = readValue(this.view, entry.track.property)
|
|
256
|
+
forget(entry)
|
|
257
|
+
this.ended(finished)
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
private unloaded = () => this.stop()
|
|
59
262
|
|
|
60
263
|
constructor(
|
|
61
264
|
readonly view: View,
|
|
62
265
|
plans: readonly Plan[],
|
|
266
|
+
private readonly haptic?: HapticOptions,
|
|
267
|
+
private held = false,
|
|
63
268
|
) {
|
|
269
|
+
this.endless = plans.some(
|
|
270
|
+
(plan) =>
|
|
271
|
+
!Number.isFinite(plan.transition.repeat) ||
|
|
272
|
+
(plan.transition.curve.type === 'tween' &&
|
|
273
|
+
!Number.isFinite(plan.transition.curve.duration)),
|
|
274
|
+
)
|
|
64
275
|
this.finished = new Promise((resolve) => (this.resolve = resolve))
|
|
65
276
|
setArrival(this, new Promise<void>((resolve) => (this.arrive = resolve)))
|
|
277
|
+
setArrivalTiming(this, {
|
|
278
|
+
read: () => ({
|
|
279
|
+
remaining: this.started ? Math.max(0, this.arrival - this.time()) : undefined,
|
|
280
|
+
paused: this._paused,
|
|
281
|
+
arrived: this.arrived || this.settled,
|
|
282
|
+
}),
|
|
283
|
+
subscribe: (callback) => {
|
|
284
|
+
this.timingListeners.add(callback)
|
|
285
|
+
return () => this.timingListeners.delete(callback)
|
|
286
|
+
},
|
|
287
|
+
})
|
|
66
288
|
if (view.isLoaded && view.nativeViewProtected) this.start(plans)
|
|
67
289
|
else this.waitForLoad(plans)
|
|
68
290
|
}
|
|
@@ -77,6 +299,7 @@ export class ViewAnimation implements Animation {
|
|
|
77
299
|
stop() {
|
|
78
300
|
if (this.settled) return
|
|
79
301
|
this.cancelWait?.()
|
|
302
|
+
this.haptics?.cancel()
|
|
80
303
|
const now = getDriver().now()
|
|
81
304
|
for (const entry of this.entries) {
|
|
82
305
|
if (!entry.done) release(entry, now, true)
|
|
@@ -86,32 +309,45 @@ export class ViewAnimation implements Animation {
|
|
|
86
309
|
}
|
|
87
310
|
|
|
88
311
|
ended(finished: boolean) {
|
|
89
|
-
if (!finished)
|
|
90
|
-
|
|
312
|
+
if (!finished) {
|
|
313
|
+
this.complete = false
|
|
314
|
+
this.haptics?.cancel()
|
|
315
|
+
}
|
|
316
|
+
if (--this.pending === 0) this.becameIdle()
|
|
91
317
|
}
|
|
92
318
|
|
|
93
319
|
private start(plans: readonly Plan[]) {
|
|
320
|
+
this.started = true
|
|
94
321
|
const driver = getDriver()
|
|
95
322
|
const now = driver.now()
|
|
96
323
|
const entries = entriesOf(this.view)
|
|
97
324
|
|
|
98
|
-
const tracks = plans.map(({ property, keyframes, transition, relative }) => {
|
|
325
|
+
const tracks = plans.map(({ property, keyframes, transition, relative, path }) => {
|
|
99
326
|
const previous = entries.get(property)
|
|
100
327
|
let current: number
|
|
101
328
|
let velocity = 0
|
|
102
329
|
if (previous) {
|
|
103
|
-
const elapsed = now
|
|
330
|
+
const elapsed = elapsedOf(previous, now)
|
|
104
331
|
current = driver.presented(this.view, property) ?? previous.track.valueAt(elapsed)
|
|
105
|
-
velocity = previous.track.velocityAt(elapsed)
|
|
332
|
+
velocity = previous.owner.paused ? 0 : previous.track.velocityAt(elapsed)
|
|
106
333
|
release(previous, now, false)
|
|
107
334
|
} else {
|
|
108
|
-
current =
|
|
335
|
+
current =
|
|
336
|
+
(isColorProperty(property)
|
|
337
|
+
? driver.presented(this.view, property)
|
|
338
|
+
: undefined) ?? readValue(this.view, property)
|
|
109
339
|
}
|
|
340
|
+
if (path)
|
|
341
|
+
return path.track(
|
|
342
|
+
property as PathProperty,
|
|
343
|
+
relative ? current : 0,
|
|
344
|
+
transition.delay,
|
|
345
|
+
)
|
|
110
346
|
return createTrack(
|
|
111
347
|
property,
|
|
112
348
|
keyframes.map((value) => {
|
|
113
349
|
if (value === null) return current
|
|
114
|
-
if (!relative) return value
|
|
350
|
+
if (!relative || isColorProperty(property)) return value
|
|
115
351
|
return property === 'opacity' || property.startsWith('scale')
|
|
116
352
|
? current * value
|
|
117
353
|
: current + value
|
|
@@ -122,8 +358,13 @@ export class ViewAnimation implements Animation {
|
|
|
122
358
|
})
|
|
123
359
|
|
|
124
360
|
for (const track of tracks) writeValue(this.view, track.property, track.to)
|
|
361
|
+
driver.sync?.(this.view)
|
|
125
362
|
|
|
126
363
|
for (const track of tracks) {
|
|
364
|
+
let tokens = revisions.get(this.view)
|
|
365
|
+
if (!tokens) revisions.set(this.view, (tokens = new Map()))
|
|
366
|
+
const token = Symbol(track.property)
|
|
367
|
+
tokens.set(track.property, token)
|
|
127
368
|
if (track.end === 0) continue
|
|
128
369
|
const entry: Entry = {
|
|
129
370
|
track,
|
|
@@ -131,31 +372,41 @@ export class ViewAnimation implements Animation {
|
|
|
131
372
|
owner: this,
|
|
132
373
|
handle: undefined,
|
|
133
374
|
done: false,
|
|
375
|
+
finished: false,
|
|
376
|
+
generation: 0,
|
|
377
|
+
token,
|
|
134
378
|
}
|
|
135
379
|
entries.set(track.property, entry)
|
|
136
380
|
this.entries.push(entry)
|
|
137
381
|
this.pending++
|
|
138
382
|
}
|
|
139
383
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (this.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
384
|
+
this.total = Math.max(0, ...tracks.map((track) => track.end))
|
|
385
|
+
this.arrival = Math.max(0, ...tracks.map((track) => track.arrival))
|
|
386
|
+
this.clock =
|
|
387
|
+
(this.pending ? driver.clock?.(this.total) : undefined) ??
|
|
388
|
+
createClock(() => driver.now())
|
|
389
|
+
if (this._paused) this.clock.pause()
|
|
390
|
+
this.view.on('unloaded', this.unloaded)
|
|
391
|
+
this.haptics = playHaptics(this.view, tracks, this.haptic, driver, now, () =>
|
|
392
|
+
this.time(),
|
|
393
|
+
)
|
|
394
|
+
if (this._paused) this.haptics?.pause()
|
|
395
|
+
for (const entry of this.entries) this.playEntry(entry)
|
|
396
|
+
if (this.pendingProgress !== undefined) {
|
|
397
|
+
const progress = this.pendingProgress
|
|
398
|
+
this.pendingProgress = undefined
|
|
399
|
+
this.seekTime(progress * this.total)
|
|
400
|
+
} else if (this._paused) this.controlTracks(0, 'pause')
|
|
401
|
+
this.scheduleArrival()
|
|
402
|
+
this.readyListener?.()
|
|
403
|
+
this.timingChanged()
|
|
404
|
+
if (this.pending === 0) this.becameIdle()
|
|
154
405
|
}
|
|
155
406
|
|
|
156
407
|
private waitForLoad(plans: readonly Plan[]) {
|
|
157
408
|
for (const { property, keyframes, relative } of plans) {
|
|
158
|
-
if (relative) continue
|
|
409
|
+
if (relative && !isColorProperty(property)) continue
|
|
159
410
|
const first = keyframes[0]
|
|
160
411
|
if (first !== null && first !== undefined) writeValue(this.view, property, first)
|
|
161
412
|
}
|
|
@@ -174,13 +425,51 @@ export class ViewAnimation implements Animation {
|
|
|
174
425
|
if (this.settled) return
|
|
175
426
|
this.settled = true
|
|
176
427
|
this.cancelArrival?.()
|
|
428
|
+
this.clock?.cancel()
|
|
429
|
+
this.view.off('unloaded', this.unloaded)
|
|
430
|
+
if (this.complete) this.haptics?.finish()
|
|
431
|
+
else this.haptics?.cancel()
|
|
432
|
+
this.haptics = undefined
|
|
177
433
|
this.arrive()
|
|
178
434
|
this.resolve(this.complete)
|
|
435
|
+
this.timingChanged()
|
|
436
|
+
}
|
|
437
|
+
}
|
|
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
|
+
|
|
450
|
+
/** Internal property ownership marker; layout cleanup must preserve newer animations. */
|
|
451
|
+
export function propertyRevision(view: View, property: Property) {
|
|
452
|
+
return revisions.get(view)?.get(property)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Sample an existing owner at layout time, without stealing an animation started meanwhile. */
|
|
456
|
+
export function captureValue(view: View, property: Property): () => number | undefined {
|
|
457
|
+
const token = propertyRevision(view, property)
|
|
458
|
+
const entry = running.get(view)?.get(property)
|
|
459
|
+
const initial = currentValue(view, property)
|
|
460
|
+
return () => {
|
|
461
|
+
if (propertyRevision(view, property) !== token) return undefined
|
|
462
|
+
if (entry && running.get(view)?.get(property) === entry)
|
|
463
|
+
return currentValue(view, property)
|
|
464
|
+
return entry?.finished ? entry.track.to : initial
|
|
179
465
|
}
|
|
180
466
|
}
|
|
181
467
|
|
|
182
468
|
/** Stops whatever is animating these properties of the view, or all of them. */
|
|
183
469
|
export function stopView(view: View, properties?: readonly Property[]) {
|
|
470
|
+
const tokens = revisions.get(view)
|
|
471
|
+
for (const property of properties ?? tokens?.keys() ?? [])
|
|
472
|
+
tokens?.set(property, Symbol(property))
|
|
184
473
|
const entries = running.get(view)
|
|
185
474
|
if (!entries) return
|
|
186
475
|
const now = getDriver().now()
|
|
@@ -208,9 +497,14 @@ function release(entry: Entry, now: number, commit: boolean) {
|
|
|
208
497
|
forget(entry)
|
|
209
498
|
if (commit) {
|
|
210
499
|
const value =
|
|
211
|
-
driver.presented(view, property) ?? entry.track.valueAt(now
|
|
500
|
+
driver.presented(view, property) ?? entry.track.valueAt(elapsedOf(entry, now))
|
|
212
501
|
writeValue(view, property, value)
|
|
502
|
+
driver.sync?.(view, property)
|
|
213
503
|
}
|
|
214
504
|
driver.stop(view, entry.handle)
|
|
215
505
|
entry.owner.ended(false)
|
|
216
506
|
}
|
|
507
|
+
|
|
508
|
+
function elapsedOf(entry: Entry, now: number) {
|
|
509
|
+
return entry.owner.elapsed(entry, now)
|
|
510
|
+
}
|
package/src/haptics.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { isIOS, type View } from '@nativescript/core'
|
|
2
|
+
import type { Driver } from './driver/index.js'
|
|
3
|
+
import type { Track } from './track.js'
|
|
4
|
+
|
|
5
|
+
export type HapticStyle = 'light' | 'medium' | 'heavy'
|
|
6
|
+
|
|
7
|
+
export interface HapticOptions {
|
|
8
|
+
style: HapticStyle
|
|
9
|
+
/** Points from 0 to 1 over the finite animation, excluding its initial delay. Defaults to arrival. */
|
|
10
|
+
times?: readonly number[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Haptic = HapticStyle | HapticOptions | false
|
|
14
|
+
|
|
15
|
+
/** Validate before any running animation is interrupted. */
|
|
16
|
+
export function resolveHaptic(haptic: Haptic | undefined): HapticOptions | undefined {
|
|
17
|
+
if (haptic === undefined || haptic === false) return undefined
|
|
18
|
+
const options = typeof haptic === 'string' ? { style: haptic } : haptic
|
|
19
|
+
if (!options || !['light', 'medium', 'heavy'].includes(options.style))
|
|
20
|
+
throw new TypeError('Haptic style must be light, medium or heavy.')
|
|
21
|
+
const times = options.times
|
|
22
|
+
if (
|
|
23
|
+
times !== undefined &&
|
|
24
|
+
(!Array.isArray(times) ||
|
|
25
|
+
times.length === 0 ||
|
|
26
|
+
Array.from(times).some(
|
|
27
|
+
(time, i) =>
|
|
28
|
+
!Number.isFinite(time) ||
|
|
29
|
+
time <= 0 ||
|
|
30
|
+
time > 1 ||
|
|
31
|
+
(i > 0 && time <= times[i - 1]!),
|
|
32
|
+
))
|
|
33
|
+
)
|
|
34
|
+
throw new RangeError(
|
|
35
|
+
'Haptic times must be increasing points greater than 0 and at most 1.',
|
|
36
|
+
)
|
|
37
|
+
return { style: options.style, times: times && [...times] }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const PREPARE_AHEAD = 100
|
|
41
|
+
const MAX_LATENESS = 100
|
|
42
|
+
const COMPLETION_TOLERANCE = 20
|
|
43
|
+
|
|
44
|
+
/** Discrete feedback only: animation playback remains entirely native. */
|
|
45
|
+
export function playHaptics(
|
|
46
|
+
view: View,
|
|
47
|
+
tracks: readonly Track[],
|
|
48
|
+
options: HapticOptions | undefined,
|
|
49
|
+
driver: Driver,
|
|
50
|
+
startedAt: number,
|
|
51
|
+
elapsedTime = () => driver.now() - startedAt,
|
|
52
|
+
) {
|
|
53
|
+
if (!isIOS || !options || typeof UIImpactFeedbackGenerator === 'undefined') return
|
|
54
|
+
const moving = tracks.filter(
|
|
55
|
+
(track) =>
|
|
56
|
+
track.duration > 0 && track.values.some((value) => value !== track.values[0]),
|
|
57
|
+
)
|
|
58
|
+
if (!moving.length) return
|
|
59
|
+
const end = Math.max(...tracks.map((track) => track.end))
|
|
60
|
+
if (!Number.isFinite(end)) return
|
|
61
|
+
const delay = Math.min(...moving.map((track) => track.delay))
|
|
62
|
+
const offsets = options.times
|
|
63
|
+
? options.times.map((time) => delay + (end - delay) * time)
|
|
64
|
+
: [Math.max(...tracks.map((track) => track.arrival))]
|
|
65
|
+
const styles = {
|
|
66
|
+
light: 0,
|
|
67
|
+
medium: 1,
|
|
68
|
+
heavy: 2,
|
|
69
|
+
} satisfies Record<HapticStyle, UIImpactFeedbackStyle>
|
|
70
|
+
let generator: UIImpactFeedbackGenerator | undefined =
|
|
71
|
+
UIImpactFeedbackGenerator.alloc().initWithStyle(styles[options.style])
|
|
72
|
+
const native = view.nativeViewProtected as UIView
|
|
73
|
+
let paused = false
|
|
74
|
+
let next = 0
|
|
75
|
+
let cancelPrepare: (() => void) | undefined
|
|
76
|
+
let cancelImpact: (() => void) | undefined
|
|
77
|
+
|
|
78
|
+
const active = () =>
|
|
79
|
+
view.isLoaded &&
|
|
80
|
+
view.nativeViewProtected === native &&
|
|
81
|
+
!!native.window &&
|
|
82
|
+
UIApplication.sharedApplication.applicationState === 0 // UIApplicationState.Active
|
|
83
|
+
|
|
84
|
+
const cancelTimers = () => {
|
|
85
|
+
cancelPrepare?.()
|
|
86
|
+
cancelImpact?.()
|
|
87
|
+
cancelPrepare = cancelImpact = undefined
|
|
88
|
+
}
|
|
89
|
+
const cancel = () => {
|
|
90
|
+
cancelPrepare?.()
|
|
91
|
+
cancelImpact?.()
|
|
92
|
+
cancelPrepare = cancelImpact = undefined
|
|
93
|
+
generator = undefined
|
|
94
|
+
view.off('unloaded', cancel)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fire = (tolerance = 0) => {
|
|
98
|
+
const elapsed = elapsedTime()
|
|
99
|
+
// A busy UI thread can delay timers. Drop stale impacts instead of replaying
|
|
100
|
+
// several old taps at once when it becomes responsive again.
|
|
101
|
+
while (next < offsets.length && offsets[next]! <= elapsed + tolerance) {
|
|
102
|
+
const offset = offsets[next++]!
|
|
103
|
+
if (active() && elapsed - offset <= MAX_LATENESS) generator?.impactOccurred()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const schedule = () => {
|
|
108
|
+
if (paused) return
|
|
109
|
+
if (!generator || next === offsets.length) return cancel()
|
|
110
|
+
const remaining = offsets[next]! - elapsedTime()
|
|
111
|
+
if (active()) generator.prepare()
|
|
112
|
+
if (remaining > PREPARE_AHEAD) {
|
|
113
|
+
cancelPrepare = driver.after(remaining - PREPARE_AHEAD, () => {
|
|
114
|
+
if (active()) generator?.prepare()
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
cancelImpact = driver.after(Math.max(0, remaining), () => {
|
|
118
|
+
fire()
|
|
119
|
+
schedule()
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
view.on('unloaded', cancel)
|
|
124
|
+
schedule()
|
|
125
|
+
return {
|
|
126
|
+
cancel,
|
|
127
|
+
pause() {
|
|
128
|
+
paused = true
|
|
129
|
+
cancelTimers()
|
|
130
|
+
},
|
|
131
|
+
play() {
|
|
132
|
+
if (paused) {
|
|
133
|
+
paused = false
|
|
134
|
+
schedule()
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
seek() {
|
|
138
|
+
cancelTimers()
|
|
139
|
+
while (next < offsets.length && offsets[next]! <= elapsedTime()) next++
|
|
140
|
+
schedule()
|
|
141
|
+
},
|
|
142
|
+
finish() {
|
|
143
|
+
// Native completion may beat the timer at the final keyframe by a frame.
|
|
144
|
+
fire(COMPLETION_TOLERANCE)
|
|
145
|
+
cancel()
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
}
|