@rafaelaugusto/jolt 0.1.0-beta.0 → 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 +441 -25
- package/dist/animate.d.ts +21 -5
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +205 -25
- 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 +164 -20
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +76 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +303 -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 +45 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -2
- 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/presets.d.ts +16 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +24 -6
- package/dist/presets.js.map +1 -1
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +25 -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 +13 -7
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +214 -19
- 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 +50 -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 +33 -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 +278 -38
- 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 +194 -25
- package/src/engine.ts +329 -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 +61 -4
- 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/presets.ts +31 -7
- package/src/press.ts +32 -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 +212 -19
- 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 +51 -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
|
|
|
@@ -20,6 +30,8 @@ export interface Plan {
|
|
|
20
30
|
/** `null` stands for the value the property has when the animation starts. */
|
|
21
31
|
keyframes: readonly (number | null)[]
|
|
22
32
|
transition: ResolvedTransition
|
|
33
|
+
relative?: boolean
|
|
34
|
+
path?: PreparedPath
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
interface Entry {
|
|
@@ -28,11 +40,32 @@ interface Entry {
|
|
|
28
40
|
readonly owner: ViewAnimation
|
|
29
41
|
handle: unknown
|
|
30
42
|
done: boolean
|
|
43
|
+
finished: boolean
|
|
44
|
+
generation: number
|
|
45
|
+
token: symbol
|
|
46
|
+
completedValue?: number
|
|
31
47
|
}
|
|
32
48
|
|
|
49
|
+
const revisions = new WeakMap<View, Map<Property, symbol>>()
|
|
50
|
+
|
|
33
51
|
const running = new WeakMap<View, Map<Property, Entry>>()
|
|
34
52
|
const arrivals = new WeakMap<Animation, Promise<unknown>>()
|
|
35
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
|
+
|
|
36
69
|
/**
|
|
37
70
|
* Resolves once the animation looks done, which for a spring is well before
|
|
38
71
|
* `finished`. Sequences move on to the next step then.
|
|
@@ -45,7 +78,7 @@ export function setArrival(animation: Animation, arrival: Promise<unknown>) {
|
|
|
45
78
|
arrivals.set(animation, arrival)
|
|
46
79
|
}
|
|
47
80
|
|
|
48
|
-
export class ViewAnimation implements
|
|
81
|
+
export class ViewAnimation implements PlaybackAnimation {
|
|
49
82
|
readonly finished: Promise<boolean>
|
|
50
83
|
private resolve!: (finished: boolean) => void
|
|
51
84
|
private readonly entries: Entry[] = []
|
|
@@ -55,13 +88,203 @@ export class ViewAnimation implements Animation {
|
|
|
55
88
|
private cancelWait: (() => void) | undefined
|
|
56
89
|
private arrive!: () => void
|
|
57
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()
|
|
58
262
|
|
|
59
263
|
constructor(
|
|
60
264
|
readonly view: View,
|
|
61
265
|
plans: readonly Plan[],
|
|
266
|
+
private readonly haptic?: HapticOptions,
|
|
267
|
+
private held = false,
|
|
62
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
|
+
)
|
|
63
275
|
this.finished = new Promise((resolve) => (this.resolve = resolve))
|
|
64
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
|
+
})
|
|
65
288
|
if (view.isLoaded && view.nativeViewProtected) this.start(plans)
|
|
66
289
|
else this.waitForLoad(plans)
|
|
67
290
|
}
|
|
@@ -76,6 +299,7 @@ export class ViewAnimation implements Animation {
|
|
|
76
299
|
stop() {
|
|
77
300
|
if (this.settled) return
|
|
78
301
|
this.cancelWait?.()
|
|
302
|
+
this.haptics?.cancel()
|
|
79
303
|
const now = getDriver().now()
|
|
80
304
|
for (const entry of this.entries) {
|
|
81
305
|
if (!entry.done) release(entry, now, true)
|
|
@@ -85,38 +309,62 @@ export class ViewAnimation implements Animation {
|
|
|
85
309
|
}
|
|
86
310
|
|
|
87
311
|
ended(finished: boolean) {
|
|
88
|
-
if (!finished)
|
|
89
|
-
|
|
312
|
+
if (!finished) {
|
|
313
|
+
this.complete = false
|
|
314
|
+
this.haptics?.cancel()
|
|
315
|
+
}
|
|
316
|
+
if (--this.pending === 0) this.becameIdle()
|
|
90
317
|
}
|
|
91
318
|
|
|
92
319
|
private start(plans: readonly Plan[]) {
|
|
320
|
+
this.started = true
|
|
93
321
|
const driver = getDriver()
|
|
94
322
|
const now = driver.now()
|
|
95
323
|
const entries = entriesOf(this.view)
|
|
96
324
|
|
|
97
|
-
const tracks = plans.map(({ property, keyframes, transition }) => {
|
|
325
|
+
const tracks = plans.map(({ property, keyframes, transition, relative, path }) => {
|
|
98
326
|
const previous = entries.get(property)
|
|
99
327
|
let current: number
|
|
100
328
|
let velocity = 0
|
|
101
329
|
if (previous) {
|
|
102
|
-
const elapsed = now
|
|
330
|
+
const elapsed = elapsedOf(previous, now)
|
|
103
331
|
current = driver.presented(this.view, property) ?? previous.track.valueAt(elapsed)
|
|
104
|
-
velocity = previous.track.velocityAt(elapsed)
|
|
332
|
+
velocity = previous.owner.paused ? 0 : previous.track.velocityAt(elapsed)
|
|
105
333
|
release(previous, now, false)
|
|
106
334
|
} else {
|
|
107
|
-
current =
|
|
335
|
+
current =
|
|
336
|
+
(isColorProperty(property)
|
|
337
|
+
? driver.presented(this.view, property)
|
|
338
|
+
: undefined) ?? readValue(this.view, property)
|
|
108
339
|
}
|
|
340
|
+
if (path)
|
|
341
|
+
return path.track(
|
|
342
|
+
property as PathProperty,
|
|
343
|
+
relative ? current : 0,
|
|
344
|
+
transition.delay,
|
|
345
|
+
)
|
|
109
346
|
return createTrack(
|
|
110
347
|
property,
|
|
111
|
-
keyframes.map((value) =>
|
|
348
|
+
keyframes.map((value) => {
|
|
349
|
+
if (value === null) return current
|
|
350
|
+
if (!relative || isColorProperty(property)) return value
|
|
351
|
+
return property === 'opacity' || property.startsWith('scale')
|
|
352
|
+
? current * value
|
|
353
|
+
: current + value
|
|
354
|
+
}),
|
|
112
355
|
transition,
|
|
113
356
|
velocity,
|
|
114
357
|
)
|
|
115
358
|
})
|
|
116
359
|
|
|
117
360
|
for (const track of tracks) writeValue(this.view, track.property, track.to)
|
|
361
|
+
driver.sync?.(this.view)
|
|
118
362
|
|
|
119
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)
|
|
120
368
|
if (track.end === 0) continue
|
|
121
369
|
const entry: Entry = {
|
|
122
370
|
track,
|
|
@@ -124,30 +372,41 @@ export class ViewAnimation implements Animation {
|
|
|
124
372
|
owner: this,
|
|
125
373
|
handle: undefined,
|
|
126
374
|
done: false,
|
|
375
|
+
finished: false,
|
|
376
|
+
generation: 0,
|
|
377
|
+
token,
|
|
127
378
|
}
|
|
128
379
|
entries.set(track.property, entry)
|
|
129
380
|
this.entries.push(entry)
|
|
130
381
|
this.pending++
|
|
131
382
|
}
|
|
132
383
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (this.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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()
|
|
147
405
|
}
|
|
148
406
|
|
|
149
407
|
private waitForLoad(plans: readonly Plan[]) {
|
|
150
|
-
for (const { property, keyframes } of plans) {
|
|
408
|
+
for (const { property, keyframes, relative } of plans) {
|
|
409
|
+
if (relative && !isColorProperty(property)) continue
|
|
151
410
|
const first = keyframes[0]
|
|
152
411
|
if (first !== null && first !== undefined) writeValue(this.view, property, first)
|
|
153
412
|
}
|
|
@@ -166,13 +425,51 @@ export class ViewAnimation implements Animation {
|
|
|
166
425
|
if (this.settled) return
|
|
167
426
|
this.settled = true
|
|
168
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
|
|
169
433
|
this.arrive()
|
|
170
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
|
|
171
465
|
}
|
|
172
466
|
}
|
|
173
467
|
|
|
174
468
|
/** Stops whatever is animating these properties of the view, or all of them. */
|
|
175
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))
|
|
176
473
|
const entries = running.get(view)
|
|
177
474
|
if (!entries) return
|
|
178
475
|
const now = getDriver().now()
|
|
@@ -200,9 +497,14 @@ function release(entry: Entry, now: number, commit: boolean) {
|
|
|
200
497
|
forget(entry)
|
|
201
498
|
if (commit) {
|
|
202
499
|
const value =
|
|
203
|
-
driver.presented(view, property) ?? entry.track.valueAt(now
|
|
500
|
+
driver.presented(view, property) ?? entry.track.valueAt(elapsedOf(entry, now))
|
|
204
501
|
writeValue(view, property, value)
|
|
502
|
+
driver.sync?.(view, property)
|
|
205
503
|
}
|
|
206
504
|
driver.stop(view, entry.handle)
|
|
207
505
|
entry.owner.ended(false)
|
|
208
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
|
+
}
|