@rafaelaugusto/jolt 0.1.0-beta.4 → 0.1.0-beta.6
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 +134 -4
- package/dist/animate.d.ts +7 -4
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +58 -13
- package/dist/animate.js.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +34 -0
- package/dist/config.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/driver/android-color.d.ts +3 -0
- package/dist/driver/android-color.d.ts.map +1 -1
- package/dist/driver/android-color.js +20 -18
- package/dist/driver/android-color.js.map +1 -1
- 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.map +1 -1
- package/dist/driver/android.js +30 -36
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +9 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.d.ts +2 -0
- package/dist/driver/ios-color.d.ts.map +1 -1
- package/dist/driver/ios-color.js +19 -8
- package/dist/driver/ios-color.js.map +1 -1
- 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-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 +76 -40
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +64 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +243 -19
- package/dist/engine.js.map +1 -1
- package/dist/haptics.d.ts +4 -1
- package/dist/haptics.d.ts.map +1 -1
- package/dist/haptics.js +27 -3
- package/dist/haptics.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- 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 +17 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +116 -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/liquid.d.ts.map +1 -1
- package/dist/liquid.js +2 -1
- package/dist/liquid.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +2 -1
- package/dist/loader.js.map +1 -1
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +4 -3
- package/dist/particles.js.map +1 -1
- 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 +3 -3
- package/dist/press.js.map +1 -1
- package/dist/properties.d.ts +2 -1
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +14 -1
- package/dist/properties.js.map +1 -1
- package/dist/reveal.d.ts.map +1 -1
- package/dist/reveal.js +3 -2
- package/dist/reveal.js.map +1 -1
- 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.map +1 -1
- package/dist/shimmer.js +2 -1
- package/dist/shimmer.js.map +1 -1
- 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.map +1 -1
- package/dist/symbol-effect.js +4 -3
- package/dist/symbol-effect.js.map +1 -1
- package/dist/text-layout.d.ts +8 -0
- package/dist/text-layout.d.ts.map +1 -1
- package/dist/text-layout.js +8 -2
- package/dist/text-layout.js.map +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +60 -64
- package/dist/text.js.map +1 -1
- package/dist/track.d.ts +2 -0
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +72 -16
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +4 -1
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +89 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +134 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +15 -3
- package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +2 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
- package/platforms/android/proguard-rules.pro +7 -0
- package/src/android-plugin.d.ts +64 -0
- package/src/animate.ts +96 -33
- package/src/config.ts +46 -0
- package/src/decoration.ts +180 -0
- package/src/driver/android-color.ts +22 -20
- package/src/driver/android-decoration.ts +182 -0
- package/src/driver/android-playback.ts +157 -0
- package/src/driver/android.ts +57 -43
- package/src/driver/index.ts +15 -0
- package/src/driver/ios-color.ts +19 -10
- package/src/driver/ios-decoration.ts +332 -0
- package/src/driver/ios-playback.ts +51 -0
- package/src/driver/ios-shadow.ts +50 -0
- package/src/driver/ios.ts +87 -46
- package/src/engine.ts +267 -22
- package/src/haptics.ts +25 -2
- package/src/index.ts +12 -1
- package/src/items.ts +140 -0
- package/src/keyboard.ts +132 -0
- package/src/label.ts +76 -0
- package/src/liquid.ts +2 -1
- package/src/loader.ts +2 -1
- package/src/particles.ts +4 -4
- package/src/path.ts +124 -0
- package/src/playback-group.ts +143 -0
- package/src/playback.ts +62 -0
- package/src/press.ts +3 -5
- package/src/properties.ts +18 -2
- package/src/reveal.ts +3 -3
- 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 +2 -1
- package/src/svg-path.ts +355 -0
- package/src/symbol-effect.ts +4 -3
- package/src/text-layout.ts +14 -2
- package/src/text.ts +62 -81
- package/src/track.ts +98 -25
- package/src/transition.ts +4 -1
- package/src/types.ts +23 -1
- package/src/vue/index.ts +189 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { View } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
type BackgroundView = { _redrawNativeBackground?: (...args: unknown[]) => void }
|
|
4
|
+
const repairs = new WeakSet<View>()
|
|
5
|
+
|
|
6
|
+
/** Core treats alpha 0 as 1. Repair its model layer after background redraws,
|
|
7
|
+
* not on animation frames. The wrapper belongs only to this view and is
|
|
8
|
+
* removed when the shadow color changes. */
|
|
9
|
+
export function preserveZeroShadow(view: View) {
|
|
10
|
+
const target = view as unknown as BackgroundView
|
|
11
|
+
const hasZero = () =>
|
|
12
|
+
Array.isArray(view.boxShadow) &&
|
|
13
|
+
view.boxShadow.some((shadow) => shadow.color.argb >>> 24 === 0)
|
|
14
|
+
if (!hasZero() || repairs.has(view)) return
|
|
15
|
+
const descriptor = Object.getOwnPropertyDescriptor(view, '_redrawNativeBackground')
|
|
16
|
+
const original = target._redrawNativeBackground
|
|
17
|
+
const apply = () => {
|
|
18
|
+
if (!hasZero()) return dispose()
|
|
19
|
+
if (!view.isLoaded) return
|
|
20
|
+
const layers = view.nativeViewProtected?.outerShadowContainerLayer?.sublayers as
|
|
21
|
+
NSArray<CALayer> | undefined
|
|
22
|
+
if (!layers || !Array.isArray(view.boxShadow)) return
|
|
23
|
+
CATransaction.begin()
|
|
24
|
+
CATransaction.setDisableActions(true)
|
|
25
|
+
for (let i = 0; i < Math.min(layers.count, view.boxShadow.length); i++)
|
|
26
|
+
if (view.boxShadow[i]!.color.argb >>> 24 === 0)
|
|
27
|
+
layers.objectAtIndex(i).shadowOpacity = 0
|
|
28
|
+
CATransaction.commit()
|
|
29
|
+
}
|
|
30
|
+
const redraw = function (this: BackgroundView, ...args: unknown[]) {
|
|
31
|
+
original!.apply(this, args)
|
|
32
|
+
apply()
|
|
33
|
+
}
|
|
34
|
+
const dispose = () => {
|
|
35
|
+
view.style.off('boxShadowChange', apply)
|
|
36
|
+
view.off('loaded', apply)
|
|
37
|
+
view.off('layoutChanged', apply)
|
|
38
|
+
if (original && target._redrawNativeBackground === redraw) {
|
|
39
|
+
if (descriptor) Object.defineProperty(view, '_redrawNativeBackground', descriptor)
|
|
40
|
+
else delete target._redrawNativeBackground
|
|
41
|
+
}
|
|
42
|
+
repairs.delete(view)
|
|
43
|
+
}
|
|
44
|
+
repairs.add(view)
|
|
45
|
+
if (original) target._redrawNativeBackground = redraw
|
|
46
|
+
view.style.on('boxShadowChange', apply)
|
|
47
|
+
view.on('loaded', apply)
|
|
48
|
+
view.on('layoutChanged', apply)
|
|
49
|
+
apply()
|
|
50
|
+
}
|
package/src/driver/ios.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { iosPlayback } from './ios-playback.js'
|
|
2
|
+
import { isDecorationProperty } from '../decoration.js'
|
|
3
|
+
import {
|
|
4
|
+
playIOSDecoration,
|
|
5
|
+
presentedIOSDecoration,
|
|
6
|
+
syncIOSDecoration,
|
|
7
|
+
} from './ios-decoration.js'
|
|
1
8
|
import type { View } from '@nativescript/core'
|
|
2
9
|
import type { NumericProperty } from '../properties.js'
|
|
3
10
|
import { isColorProperty } from '../color.js'
|
|
@@ -5,10 +12,9 @@ import { playIOSColor, presentedIOSColor } from './ios-color.js'
|
|
|
5
12
|
import { after, type Driver } from './index.js'
|
|
6
13
|
|
|
7
14
|
interface Handle {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
color?: ReturnType<typeof playIOSColor>
|
|
15
|
+
stop(): void
|
|
16
|
+
elapsed(): number
|
|
17
|
+
control(elapsed: number, paused: boolean): void
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
const keyPaths: Record<NumericProperty, string> = {
|
|
@@ -52,8 +58,10 @@ export function createIOSDriver(): Driver {
|
|
|
52
58
|
now: () => CACurrentMediaTime() * 1000,
|
|
53
59
|
|
|
54
60
|
play(view, track, startedAt, onEnd): Handle {
|
|
61
|
+
if (isDecorationProperty(track.property))
|
|
62
|
+
return playIOSDecoration(view, track, startedAt, onEnd)
|
|
55
63
|
if (isColorProperty(track.property))
|
|
56
|
-
return
|
|
64
|
+
return playIOSColor(view, track, startedAt, onEnd)
|
|
57
65
|
const layers = layersOf(view)
|
|
58
66
|
const key = `jolt:${track.property}`
|
|
59
67
|
const rotation = track.property.startsWith('rotate')
|
|
@@ -62,57 +70,90 @@ export function createIOSDriver(): Driver {
|
|
|
62
70
|
ensurePerspective(view, layers)
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
Math.min(80, maximum),
|
|
73
|
-
maximum,
|
|
74
|
-
maximum,
|
|
73
|
+
const timing = iosPlayback(track, startedAt)
|
|
74
|
+
let stopped = false,
|
|
75
|
+
generation = 0
|
|
76
|
+
const attach = () => {
|
|
77
|
+
const instance = ++generation
|
|
78
|
+
const animation = CAKeyframeAnimation.animationWithKeyPath(
|
|
79
|
+
keyPaths[track.property as NumericProperty],
|
|
75
80
|
)
|
|
81
|
+
// iOS 15+. The app must opt in through CADisableMinimumFrameDurationOnPhone.
|
|
82
|
+
if (animation.respondsToSelector('setPreferredFrameRateRange:')) {
|
|
83
|
+
const nativeView = view.nativeViewProtected as UIView
|
|
84
|
+
const screen = nativeView.window?.screen ?? UIScreen.mainScreen
|
|
85
|
+
const maximum = Math.min(120, screen.maximumFramesPerSecond)
|
|
86
|
+
animation.preferredFrameRateRange = CAFrameRateRangeMake(
|
|
87
|
+
Math.min(80, maximum),
|
|
88
|
+
maximum,
|
|
89
|
+
maximum,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
animation.values = NSArray.arrayWithArray(
|
|
93
|
+
track.values.map((value) => (rotation ? radians(value) : value)),
|
|
94
|
+
)
|
|
95
|
+
animation.keyTimes = NSArray.arrayWithArray(track.times as number[])
|
|
96
|
+
animation.duration = Math.max(track.duration, 1) / 1000
|
|
97
|
+
// Keep infinite loops when iOS removes animations on backgrounding.
|
|
98
|
+
// They are removed explicitly when stopped or replaced.
|
|
99
|
+
if (track.repeat > 0) {
|
|
100
|
+
animation.autoreverses = track.reverse
|
|
101
|
+
animation.repeatCount = track.reverse
|
|
102
|
+
? (track.repeat + 1) / 2
|
|
103
|
+
: track.repeat + 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
CATransaction.begin()
|
|
107
|
+
if (!timing.paused && !infinite)
|
|
108
|
+
CATransaction.setCompletionBlock(() => {
|
|
109
|
+
if (stopped || instance !== generation) return
|
|
110
|
+
stopped = true
|
|
111
|
+
onEnd()
|
|
112
|
+
})
|
|
113
|
+
for (const layer of layers) {
|
|
114
|
+
timing.configure(animation, layer)
|
|
115
|
+
layer.addAnimationForKey(animation, key)
|
|
116
|
+
}
|
|
117
|
+
CATransaction.commit()
|
|
76
118
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
119
|
+
attach()
|
|
120
|
+
return {
|
|
121
|
+
elapsed: timing.elapsed,
|
|
122
|
+
control(time, paused) {
|
|
123
|
+
if (!stopped) {
|
|
124
|
+
timing.control(time, paused)
|
|
125
|
+
attach()
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
stop() {
|
|
129
|
+
if (stopped) return
|
|
130
|
+
stopped = true
|
|
131
|
+
generation++
|
|
132
|
+
for (const layer of layers) layer.removeAnimationForKey(key)
|
|
133
|
+
onEnd(false)
|
|
134
|
+
},
|
|
90
135
|
}
|
|
91
|
-
|
|
92
|
-
CATransaction.begin()
|
|
93
|
-
// iOS completes the transaction on backgrounding even if the loop is retained.
|
|
94
|
-
if (!infinite) CATransaction.setCompletionBlock(onEnd)
|
|
95
|
-
for (const layer of layers) layer.addAnimationForKey(animation, key)
|
|
96
|
-
CATransaction.commit()
|
|
97
|
-
|
|
98
|
-
return { layers, key, onStop: infinite ? onEnd : undefined }
|
|
99
136
|
},
|
|
100
137
|
|
|
101
138
|
stop(_view, handle) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
playing.onStop = undefined
|
|
109
|
-
for (const layer of layers) layer.removeAnimationForKey(key)
|
|
110
|
-
onStop?.()
|
|
139
|
+
;(handle as Handle).stop()
|
|
140
|
+
},
|
|
141
|
+
control(_view, handle, elapsed, paused) {
|
|
142
|
+
;(handle as Handle).control(elapsed, paused)
|
|
111
143
|
},
|
|
144
|
+
elapsed: (handle) => (handle as Handle).elapsed(),
|
|
145
|
+
|
|
146
|
+
sync: syncIOSDecoration,
|
|
112
147
|
|
|
113
148
|
presented: (view, property) =>
|
|
114
|
-
|
|
149
|
+
isDecorationProperty(property)
|
|
150
|
+
? presentedIOSDecoration(view, property)
|
|
151
|
+
: isColorProperty(property)
|
|
152
|
+
? presentedIOSColor(view, property)
|
|
153
|
+
: undefined,
|
|
115
154
|
|
|
116
155
|
after,
|
|
156
|
+
|
|
157
|
+
reducedMotion: () => UIAccessibilityIsReduceMotionEnabled(),
|
|
117
158
|
}
|
|
118
159
|
}
|
package/src/engine.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
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'
|
|
3
10
|
import { playHaptics, type HapticOptions } from './haptics.js'
|
|
@@ -5,12 +12,13 @@ import { readValue, writeValue, type Property } from './properties.js'
|
|
|
5
12
|
import { createTrack, type Track } from './track.js'
|
|
6
13
|
import type { ResolvedTransition } from './transition.js'
|
|
7
14
|
import { isColorProperty } from './color.js'
|
|
15
|
+
import type { PreparedPath, PathProperty } from './path.js'
|
|
8
16
|
|
|
9
17
|
export interface Animation extends PromiseLike<boolean> {
|
|
10
18
|
/**
|
|
11
19
|
* Resolves with `true` once the animation played to the end, or with `false`
|
|
12
20
|
* if it was stopped, superseded or skipped.
|
|
13
|
-
*
|
|
21
|
+
* Playback cancellation never rejects; sequences can reject setup/callback errors.
|
|
14
22
|
*/
|
|
15
23
|
readonly finished: Promise<boolean>
|
|
16
24
|
/** Stops playback. Each effect documents whether it preserves or restores its visual state. */
|
|
@@ -23,6 +31,7 @@ export interface Plan {
|
|
|
23
31
|
keyframes: readonly (number | null)[]
|
|
24
32
|
transition: ResolvedTransition
|
|
25
33
|
relative?: boolean
|
|
34
|
+
path?: PreparedPath
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
interface Entry {
|
|
@@ -31,11 +40,32 @@ interface Entry {
|
|
|
31
40
|
readonly owner: ViewAnimation
|
|
32
41
|
handle: unknown
|
|
33
42
|
done: boolean
|
|
43
|
+
finished: boolean
|
|
44
|
+
generation: number
|
|
45
|
+
token: symbol
|
|
46
|
+
completedValue?: number
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
const revisions = new WeakMap<View, Map<Property, symbol>>()
|
|
50
|
+
|
|
36
51
|
const running = new WeakMap<View, Map<Property, Entry>>()
|
|
37
52
|
const arrivals = new WeakMap<Animation, Promise<unknown>>()
|
|
38
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
|
+
|
|
39
69
|
/**
|
|
40
70
|
* Resolves once the animation looks done, which for a spring is well before
|
|
41
71
|
* `finished`. Sequences move on to the next step then.
|
|
@@ -48,7 +78,7 @@ export function setArrival(animation: Animation, arrival: Promise<unknown>) {
|
|
|
48
78
|
arrivals.set(animation, arrival)
|
|
49
79
|
}
|
|
50
80
|
|
|
51
|
-
export class ViewAnimation implements
|
|
81
|
+
export class ViewAnimation implements PlaybackAnimation {
|
|
52
82
|
readonly finished: Promise<boolean>
|
|
53
83
|
private resolve!: (finished: boolean) => void
|
|
54
84
|
private readonly entries: Entry[] = []
|
|
@@ -59,14 +89,202 @@ export class ViewAnimation implements Animation {
|
|
|
59
89
|
private arrive!: () => void
|
|
60
90
|
private cancelArrival: (() => void) | undefined
|
|
61
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()
|
|
62
262
|
|
|
63
263
|
constructor(
|
|
64
264
|
readonly view: View,
|
|
65
265
|
plans: readonly Plan[],
|
|
66
266
|
private readonly haptic?: HapticOptions,
|
|
267
|
+
private held = false,
|
|
67
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
|
+
)
|
|
68
275
|
this.finished = new Promise((resolve) => (this.resolve = resolve))
|
|
69
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
|
+
})
|
|
70
288
|
if (view.isLoaded && view.nativeViewProtected) this.start(plans)
|
|
71
289
|
else this.waitForLoad(plans)
|
|
72
290
|
}
|
|
@@ -95,22 +313,23 @@ export class ViewAnimation implements Animation {
|
|
|
95
313
|
this.complete = false
|
|
96
314
|
this.haptics?.cancel()
|
|
97
315
|
}
|
|
98
|
-
if (--this.pending === 0) this.
|
|
316
|
+
if (--this.pending === 0) this.becameIdle()
|
|
99
317
|
}
|
|
100
318
|
|
|
101
319
|
private start(plans: readonly Plan[]) {
|
|
320
|
+
this.started = true
|
|
102
321
|
const driver = getDriver()
|
|
103
322
|
const now = driver.now()
|
|
104
323
|
const entries = entriesOf(this.view)
|
|
105
324
|
|
|
106
|
-
const tracks = plans.map(({ property, keyframes, transition, relative }) => {
|
|
325
|
+
const tracks = plans.map(({ property, keyframes, transition, relative, path }) => {
|
|
107
326
|
const previous = entries.get(property)
|
|
108
327
|
let current: number
|
|
109
328
|
let velocity = 0
|
|
110
329
|
if (previous) {
|
|
111
330
|
const elapsed = elapsedOf(previous, now)
|
|
112
331
|
current = driver.presented(this.view, property) ?? previous.track.valueAt(elapsed)
|
|
113
|
-
velocity = previous.track.velocityAt(elapsed)
|
|
332
|
+
velocity = previous.owner.paused ? 0 : previous.track.velocityAt(elapsed)
|
|
114
333
|
release(previous, now, false)
|
|
115
334
|
} else {
|
|
116
335
|
current =
|
|
@@ -118,6 +337,12 @@ export class ViewAnimation implements Animation {
|
|
|
118
337
|
? driver.presented(this.view, property)
|
|
119
338
|
: undefined) ?? readValue(this.view, property)
|
|
120
339
|
}
|
|
340
|
+
if (path)
|
|
341
|
+
return path.track(
|
|
342
|
+
property as PathProperty,
|
|
343
|
+
relative ? current : 0,
|
|
344
|
+
transition.delay,
|
|
345
|
+
)
|
|
121
346
|
return createTrack(
|
|
122
347
|
property,
|
|
123
348
|
keyframes.map((value) => {
|
|
@@ -133,8 +358,13 @@ export class ViewAnimation implements Animation {
|
|
|
133
358
|
})
|
|
134
359
|
|
|
135
360
|
for (const track of tracks) writeValue(this.view, track.property, track.to)
|
|
361
|
+
driver.sync?.(this.view)
|
|
136
362
|
|
|
137
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)
|
|
138
368
|
if (track.end === 0) continue
|
|
139
369
|
const entry: Entry = {
|
|
140
370
|
track,
|
|
@@ -142,28 +372,36 @@ export class ViewAnimation implements Animation {
|
|
|
142
372
|
owner: this,
|
|
143
373
|
handle: undefined,
|
|
144
374
|
done: false,
|
|
375
|
+
finished: false,
|
|
376
|
+
generation: 0,
|
|
377
|
+
token,
|
|
145
378
|
}
|
|
146
379
|
entries.set(track.property, entry)
|
|
147
380
|
this.entries.push(entry)
|
|
148
381
|
this.pending++
|
|
149
382
|
}
|
|
150
383
|
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
if (this.
|
|
164
|
-
|
|
165
|
-
this.
|
|
166
|
-
|
|
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()
|
|
167
405
|
}
|
|
168
406
|
|
|
169
407
|
private waitForLoad(plans: readonly Plan[]) {
|
|
@@ -187,16 +425,22 @@ export class ViewAnimation implements Animation {
|
|
|
187
425
|
if (this.settled) return
|
|
188
426
|
this.settled = true
|
|
189
427
|
this.cancelArrival?.()
|
|
428
|
+
this.clock?.cancel()
|
|
429
|
+
this.view.off('unloaded', this.unloaded)
|
|
190
430
|
if (this.complete) this.haptics?.finish()
|
|
191
431
|
else this.haptics?.cancel()
|
|
192
432
|
this.haptics = undefined
|
|
193
433
|
this.arrive()
|
|
194
434
|
this.resolve(this.complete)
|
|
435
|
+
this.timingChanged()
|
|
195
436
|
}
|
|
196
437
|
}
|
|
197
438
|
|
|
198
439
|
/** Stops whatever is animating these properties of the view, or all of them. */
|
|
199
440
|
export function stopView(view: View, properties?: readonly Property[]) {
|
|
441
|
+
const tokens = revisions.get(view)
|
|
442
|
+
for (const property of properties ?? tokens?.keys() ?? [])
|
|
443
|
+
tokens?.set(property, Symbol(property))
|
|
200
444
|
const entries = running.get(view)
|
|
201
445
|
if (!entries) return
|
|
202
446
|
const now = getDriver().now()
|
|
@@ -226,11 +470,12 @@ function release(entry: Entry, now: number, commit: boolean) {
|
|
|
226
470
|
const value =
|
|
227
471
|
driver.presented(view, property) ?? entry.track.valueAt(elapsedOf(entry, now))
|
|
228
472
|
writeValue(view, property, value)
|
|
473
|
+
driver.sync?.(view, property)
|
|
229
474
|
}
|
|
230
475
|
driver.stop(view, entry.handle)
|
|
231
476
|
entry.owner.ended(false)
|
|
232
477
|
}
|
|
233
478
|
|
|
234
479
|
function elapsedOf(entry: Entry, now: number) {
|
|
235
|
-
return
|
|
480
|
+
return entry.owner.elapsed(entry, now)
|
|
236
481
|
}
|