@rafaelaugusto/jolt 0.1.0-beta.11 → 0.1.0-beta.13
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 +42 -8
- package/dist/animate.d.ts +15 -3
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +52 -34
- package/dist/animate.js.map +1 -1
- package/dist/drag.d.ts +17 -3
- package/dist/drag.d.ts.map +1 -1
- package/dist/drag.js +34 -11
- package/dist/drag.js.map +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +2 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/frame.d.ts +3 -0
- package/dist/driver/frame.d.ts.map +1 -0
- package/dist/driver/frame.js +7 -0
- package/dist/driver/frame.js.map +1 -0
- package/dist/driver/index.d.ts +2 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +2 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/easing.d.ts +1 -1
- package/dist/easing.d.ts.map +1 -1
- package/dist/easing.js +11 -0
- package/dist/easing.js.map +1 -1
- package/dist/engine.d.ts +20 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +166 -38
- package/dist/engine.js.map +1 -1
- package/dist/gradient.d.ts.map +1 -1
- package/dist/gradient.js +10 -4
- package/dist/gradient.js.map +1 -1
- package/dist/image-effect.d.ts +1 -0
- package/dist/image-effect.d.ts.map +1 -1
- package/dist/image-effect.js +10 -2
- package/dist/image-effect.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +63 -10
- package/dist/keyboard.js.map +1 -1
- package/dist/mixer.d.ts +49 -0
- package/dist/mixer.d.ts.map +1 -0
- package/dist/mixer.js +238 -0
- package/dist/mixer.js.map +1 -0
- package/dist/motion-scope.d.ts +6 -2
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +12 -4
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-states.d.ts +10 -5
- package/dist/motion-states.d.ts.map +1 -1
- package/dist/motion-states.js +55 -22
- package/dist/motion-states.js.map +1 -1
- package/dist/motion-value.d.ts +152 -0
- package/dist/motion-value.d.ts.map +1 -0
- package/dist/motion-value.js +635 -0
- package/dist/motion-value.js.map +1 -0
- package/dist/native/index.d.ts +6 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +3 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native-properties.d.ts +28 -0
- package/dist/native-properties.d.ts.map +1 -0
- package/dist/native-properties.js +154 -0
- package/dist/native-properties.js.map +1 -0
- package/dist/native-property-driver.d.ts +19 -0
- package/dist/native-property-driver.d.ts.map +1 -0
- package/dist/native-property-driver.js +69 -0
- package/dist/native-property-driver.js.map +1 -0
- package/dist/native-property.d.ts +35 -0
- package/dist/native-property.d.ts.map +1 -0
- package/dist/native-property.js +277 -0
- package/dist/native-property.js.map +1 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +2 -2
- package/dist/path.js.map +1 -1
- package/dist/playback-group.d.ts.map +1 -1
- package/dist/playback-group.js +17 -1
- package/dist/playback-group.js.map +1 -1
- package/dist/playback.d.ts +15 -0
- package/dist/playback.d.ts.map +1 -1
- package/dist/playback.js +23 -0
- package/dist/playback.js.map +1 -1
- package/dist/sequence.d.ts +24 -0
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +181 -19
- package/dist/sequence.js.map +1 -1
- package/dist/timed-animation.d.ts +38 -0
- package/dist/timed-animation.d.ts.map +1 -0
- package/dist/timed-animation.js +177 -0
- package/dist/timed-animation.js.map +1 -0
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +31 -12
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +15 -4
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +46 -7
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/value-observers.d.ts +19 -0
- package/dist/value-observers.d.ts.map +1 -0
- package/dist/value-observers.js +111 -0
- package/dist/value-observers.js.map +1 -0
- package/dist/value-track.d.ts +13 -0
- package/dist/value-track.d.ts.map +1 -0
- package/dist/value-track.js +59 -0
- package/dist/value-track.js.map +1 -0
- package/dist/vue/index.d.ts +28 -2
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +78 -2
- package/dist/vue/index.js.map +1 -1
- package/package.json +6 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrame.java +53 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrameFollower.java +57 -0
- package/src/android-plugin.d.ts +64 -0
- package/src/animate.ts +106 -17
- package/src/drag.ts +67 -21
- package/src/driver/android.ts +2 -0
- package/src/driver/frame.ts +10 -0
- package/src/driver/index.ts +2 -0
- package/src/driver/ios.ts +2 -0
- package/src/easing.ts +14 -0
- package/src/engine.ts +181 -55
- package/src/gradient.ts +10 -4
- package/src/image-effect.ts +10 -2
- package/src/index.ts +23 -1
- package/src/ios-plugin.d.ts +33 -0
- package/src/keyboard.ts +83 -18
- package/src/mixer.ts +264 -0
- package/src/motion-scope.ts +17 -5
- package/src/motion-states.ts +79 -29
- package/src/motion-value.ts +795 -0
- package/src/native/index.ts +16 -0
- package/src/native-properties.ts +188 -0
- package/src/native-property-driver.ts +123 -0
- package/src/native-property.ts +353 -0
- package/src/path.ts +2 -1
- package/src/playback-group.ts +17 -1
- package/src/playback.ts +41 -0
- package/src/sequence.ts +209 -20
- package/src/timed-animation.ts +192 -0
- package/src/track.ts +37 -12
- package/src/transition.ts +69 -10
- package/src/types.ts +5 -0
- package/src/value-observers.ts +133 -0
- package/src/value-track.ts +71 -0
- package/src/vue/index.ts +98 -1
package/src/playback-group.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type ViewAnimation,
|
|
7
7
|
} from './engine.js'
|
|
8
8
|
import { getDriver } from './driver/index.js'
|
|
9
|
-
import { createClock, type PlaybackClock } from './playback.js'
|
|
9
|
+
import { createClock, registerPlayback, type PlaybackClock } from './playback.js'
|
|
10
10
|
import { validateProgress, type PlaybackAnimation } from './playback.js'
|
|
11
11
|
|
|
12
12
|
/** Staggered views share a finite time range. Shorter children remain seekable
|
|
@@ -17,6 +17,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
|
|
|
17
17
|
let finalProgress: number | undefined
|
|
18
18
|
let clock: PlaybackClock | undefined
|
|
19
19
|
let pendingProgress: number | undefined
|
|
20
|
+
let changed = () => {}
|
|
20
21
|
const duration = () => Math.max(0, ...children.map((child) => child.duration))
|
|
21
22
|
const ensureClock = () => {
|
|
22
23
|
if (clock || settled || children.some((child) => !child.ready)) return
|
|
@@ -44,6 +45,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
|
|
|
44
45
|
clock?.seek(total * value)
|
|
45
46
|
for (const child of children) child.seekTime(total * value)
|
|
46
47
|
finishIfIdle()
|
|
48
|
+
changed()
|
|
47
49
|
}
|
|
48
50
|
for (const child of children) {
|
|
49
51
|
child.onIdle(finishIfIdle)
|
|
@@ -52,6 +54,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
|
|
|
52
54
|
if (pendingProgress !== undefined && children.every((child) => child.ready))
|
|
53
55
|
seek(pendingProgress)
|
|
54
56
|
finishIfIdle()
|
|
57
|
+
changed()
|
|
55
58
|
})
|
|
56
59
|
}
|
|
57
60
|
const finished = Promise.all(children.map((child) => child.finished)).then(
|
|
@@ -63,6 +66,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
|
|
|
63
66
|
: 0
|
|
64
67
|
settled = true
|
|
65
68
|
clock?.cancel()
|
|
69
|
+
changed()
|
|
66
70
|
return results.every(Boolean)
|
|
67
71
|
},
|
|
68
72
|
)
|
|
@@ -91,6 +95,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
|
|
|
91
95
|
paused = true
|
|
92
96
|
clock?.pause()
|
|
93
97
|
children.forEach((child) => child.pause())
|
|
98
|
+
changed()
|
|
94
99
|
}
|
|
95
100
|
},
|
|
96
101
|
play() {
|
|
@@ -99,6 +104,7 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
|
|
|
99
104
|
clock?.play()
|
|
100
105
|
children.forEach((child) => child.play())
|
|
101
106
|
finishIfIdle()
|
|
107
|
+
changed()
|
|
102
108
|
}
|
|
103
109
|
},
|
|
104
110
|
stop() {
|
|
@@ -106,9 +112,19 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
|
|
|
106
112
|
settled = true
|
|
107
113
|
clock?.cancel()
|
|
108
114
|
children.forEach((child) => child.stop())
|
|
115
|
+
changed()
|
|
109
116
|
},
|
|
110
117
|
then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
|
|
111
118
|
}
|
|
119
|
+
changed = registerPlayback(animation, () => ({
|
|
120
|
+
position: pendingProgress,
|
|
121
|
+
time: finalProgress === undefined ? (clock?.time() ?? 0) : finalProgress * duration(),
|
|
122
|
+
duration: duration(),
|
|
123
|
+
paused,
|
|
124
|
+
ready: children.every((child) => child.ready),
|
|
125
|
+
settled,
|
|
126
|
+
completed: finalProgress === 1,
|
|
127
|
+
}))
|
|
112
128
|
setArrival(animation, Promise.all(children.map(arrivalOf)))
|
|
113
129
|
const listeners = new Set<() => void>()
|
|
114
130
|
const timings = children.map((child) => arrivalTimingOf(child)!)
|
package/src/playback.ts
CHANGED
|
@@ -23,6 +23,47 @@ export interface PlaybackClock {
|
|
|
23
23
|
cancel(): void
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Boundary notifications, never frame callbacks. Consumers sample this same clock. */
|
|
27
|
+
export interface PlaybackState {
|
|
28
|
+
readonly position?: number
|
|
29
|
+
readonly time: number
|
|
30
|
+
readonly duration: number
|
|
31
|
+
readonly paused: boolean
|
|
32
|
+
readonly ready: boolean
|
|
33
|
+
readonly settled: boolean
|
|
34
|
+
readonly completed: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sources = new WeakMap<
|
|
38
|
+
Animation,
|
|
39
|
+
{
|
|
40
|
+
read(): PlaybackState
|
|
41
|
+
listeners: Set<() => void>
|
|
42
|
+
}
|
|
43
|
+
>()
|
|
44
|
+
|
|
45
|
+
export function registerPlayback(animation: Animation, read: () => PlaybackState) {
|
|
46
|
+
const source = { read, listeners: new Set<() => void>() }
|
|
47
|
+
sources.set(animation, source)
|
|
48
|
+
return () => {
|
|
49
|
+
for (const listener of [...source.listeners]) listener()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function playbackSource(animation: Animation) {
|
|
54
|
+
const source = sources.get(animation)
|
|
55
|
+
if (!source) throw new TypeError('This animation does not expose a playback clock.')
|
|
56
|
+
return {
|
|
57
|
+
read: source.read,
|
|
58
|
+
subscribe(listener: () => void) {
|
|
59
|
+
source.listeners.add(listener)
|
|
60
|
+
return () => {
|
|
61
|
+
source.listeners.delete(listener)
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
26
67
|
/** Read only on user commands; there is no frame loop in JavaScript. */
|
|
27
68
|
export function createClock(now: () => number, initial = 0): PlaybackClock {
|
|
28
69
|
let offset = initial
|
package/src/sequence.ts
CHANGED
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
stopView,
|
|
11
11
|
type Animation,
|
|
12
12
|
} from './engine.js'
|
|
13
|
-
import { createClock } from './playback.js'
|
|
13
|
+
import { createClock, registerPlayback, playbackSource } from './playback.js'
|
|
14
|
+
import {
|
|
15
|
+
isMotionValue,
|
|
16
|
+
type MotionValue,
|
|
17
|
+
type ValueAnimationOptions,
|
|
18
|
+
} from './motion-value.js'
|
|
14
19
|
import type { PresetName } from './presets.js'
|
|
15
20
|
import { writeValue, type Property } from './properties.js'
|
|
16
21
|
import type { SpringName } from './transition.js'
|
|
@@ -28,6 +33,34 @@ export type Step =
|
|
|
28
33
|
]
|
|
29
34
|
| (() => Animation)
|
|
30
35
|
|
|
36
|
+
/** A participant that can genuinely freeze and continue, rather than restart. */
|
|
37
|
+
export interface PlayableAnimation extends Animation {
|
|
38
|
+
pause(): void
|
|
39
|
+
play(): void
|
|
40
|
+
readonly paused: boolean
|
|
41
|
+
readonly duration?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type TimelineStep =
|
|
45
|
+
| Exclude<Step, () => Animation>
|
|
46
|
+
| readonly [
|
|
47
|
+
MotionValue,
|
|
48
|
+
number | readonly (number | null)[],
|
|
49
|
+
((ValueAnimationOptions & { at?: SequencePosition }) | SpringName)?,
|
|
50
|
+
]
|
|
51
|
+
| { readonly at?: SequencePosition; readonly run: () => PlayableAnimation }
|
|
52
|
+
| (() => PlayableAnimation)
|
|
53
|
+
|
|
54
|
+
export interface TimelineAnimation extends PlayableAnimation {
|
|
55
|
+
/** Unknown until every step has activated and every participant reports a duration. */
|
|
56
|
+
readonly duration: number | undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Overlapping steps with a shared pause/resume control, including pending launches. */
|
|
60
|
+
export function timeline(steps: readonly TimelineStep[]): TimelineAnimation {
|
|
61
|
+
return compose(steps, true) as TimelineAnimation
|
|
62
|
+
}
|
|
63
|
+
|
|
31
64
|
/**
|
|
32
65
|
* By default a step starts once the previous one looks done. `at` can overlap
|
|
33
66
|
* that arrival, share the previous start, or select an absolute timeline time.
|
|
@@ -44,26 +77,54 @@ export type Step =
|
|
|
44
77
|
* ```
|
|
45
78
|
*/
|
|
46
79
|
export function sequence(steps: readonly Step[]): Animation {
|
|
80
|
+
return compose(steps, false)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function compose(
|
|
84
|
+
steps: readonly TimelineStep[] | readonly Step[],
|
|
85
|
+
controlled: boolean,
|
|
86
|
+
): Animation {
|
|
47
87
|
const prepared = steps.map((step) => {
|
|
48
88
|
if (typeof step === 'function') return { step, position: undefined }
|
|
49
|
-
|
|
89
|
+
if (!Array.isArray(step)) {
|
|
90
|
+
const descriptor = step as { at?: SequencePosition; run: () => PlayableAnimation }
|
|
91
|
+
if (typeof descriptor.run !== 'function')
|
|
92
|
+
throw new TypeError('A timeline step needs a run() factory.')
|
|
93
|
+
return { step: descriptor.run, position: parsePosition(descriptor.at) }
|
|
94
|
+
}
|
|
95
|
+
const [target, animation, options] = step as Exclude<
|
|
96
|
+
TimelineStep,
|
|
97
|
+
Function | { run: () => PlayableAnimation }
|
|
98
|
+
>
|
|
50
99
|
if (typeof options !== 'object' || options === null)
|
|
51
100
|
return { step, position: undefined }
|
|
52
101
|
const { at, ...transition } = options
|
|
53
102
|
const position = parsePosition(at)
|
|
54
103
|
return { step: [target, animation, transition] as Step, position }
|
|
55
104
|
})
|
|
56
|
-
applyStartingValues(
|
|
105
|
+
applyStartingValues(
|
|
106
|
+
prepared
|
|
107
|
+
.map(({ step }) => step as Step)
|
|
108
|
+
.filter((step) => typeof step === 'function' || !isMotionValue(step[0])),
|
|
109
|
+
)
|
|
57
110
|
|
|
58
111
|
const driver = getDriver()
|
|
59
112
|
const slowdown = 1 / timeScale()
|
|
60
113
|
// Absolute positions share the platform clock, including Android duration scale.
|
|
61
|
-
const clock =
|
|
62
|
-
|
|
63
|
-
|
|
114
|
+
const clock =
|
|
115
|
+
controlled || prepared.some(({ position }) => typeof position === 'number')
|
|
116
|
+
? (driver.clock?.(Infinity) ?? createClock(() => driver.now()))
|
|
117
|
+
: undefined
|
|
64
118
|
const started: Animation[] = []
|
|
65
119
|
const cancellations = new Set<() => void>()
|
|
66
120
|
let stopped = false
|
|
121
|
+
let paused = false
|
|
122
|
+
let settled = false
|
|
123
|
+
let completed = false
|
|
124
|
+
let changed = () => {}
|
|
125
|
+
const resumeChildren = new Set<PlayableAnimation>()
|
|
126
|
+
const waits = new Set<{ at: number; fire(): void; cancel(): void; schedule(): void }>()
|
|
127
|
+
const time = () => clock?.time() ?? 0
|
|
67
128
|
let resolveScheduled!: () => void
|
|
68
129
|
let rejectScheduled!: (reason: unknown) => void
|
|
69
130
|
const scheduled = new Promise<void>((resolve, reject) => {
|
|
@@ -77,28 +138,55 @@ export function sequence(steps: readonly Step[]): Animation {
|
|
|
77
138
|
position,
|
|
78
139
|
onStart: new Set<() => void>(),
|
|
79
140
|
animation: undefined as Animation | undefined,
|
|
141
|
+
start: undefined as number | undefined,
|
|
80
142
|
}
|
|
81
143
|
})
|
|
82
144
|
const cancelSchedule = () => {
|
|
83
145
|
for (const cancel of cancellations) cancel()
|
|
84
146
|
cancellations.clear()
|
|
85
|
-
|
|
147
|
+
for (const wait of waits) wait.cancel()
|
|
148
|
+
waits.clear()
|
|
149
|
+
if (!controlled || stopped || settled) clock?.cancel()
|
|
86
150
|
}
|
|
87
151
|
const fail = (error: unknown) => {
|
|
88
152
|
if (stopped) return
|
|
89
153
|
stopped = true
|
|
154
|
+
settled = true
|
|
90
155
|
cancelSchedule()
|
|
91
156
|
states.forEach((state) => state.onStart.clear())
|
|
92
157
|
started.forEach((child) => child.stop())
|
|
158
|
+
changed()
|
|
93
159
|
rejectScheduled(error)
|
|
94
160
|
}
|
|
95
161
|
const launch = (index: number) => {
|
|
96
162
|
const state = states[index]!
|
|
97
163
|
if (stopped || state.animation) return
|
|
164
|
+
if (paused) {
|
|
165
|
+
wait(0, () => launch(index))
|
|
166
|
+
return
|
|
167
|
+
}
|
|
98
168
|
try {
|
|
99
169
|
const step = state.step
|
|
100
|
-
const child =
|
|
170
|
+
const child =
|
|
171
|
+
typeof step === 'function'
|
|
172
|
+
? step()
|
|
173
|
+
: (animate as (...args: any[]) => Animation)(...(step as any[]))
|
|
174
|
+
if (controlled && !isPlayable(child)) {
|
|
175
|
+
child?.stop?.()
|
|
176
|
+
throw new TypeError(
|
|
177
|
+
'A timeline participant must expose pause(), play() and paused; use sequence() for non-playable effects.',
|
|
178
|
+
)
|
|
179
|
+
}
|
|
101
180
|
state.animation = child
|
|
181
|
+
state.start = time()
|
|
182
|
+
if (controlled) {
|
|
183
|
+
try {
|
|
184
|
+
const playback = playbackSource(child).read()
|
|
185
|
+
if (playback.ready) state.start = Math.max(0, time() - playback.time)
|
|
186
|
+
} catch {
|
|
187
|
+
/* A custom playable participant can omit a clock. */
|
|
188
|
+
}
|
|
189
|
+
}
|
|
102
190
|
started.push(child)
|
|
103
191
|
// Observe setup failures immediately, even while other steps are still
|
|
104
192
|
// scheduled. Nested sequences can reject before this one reaches arrival.
|
|
@@ -110,25 +198,51 @@ export function sequence(steps: readonly Step[]): Animation {
|
|
|
110
198
|
cancelSchedule()
|
|
111
199
|
resolveScheduled()
|
|
112
200
|
}
|
|
201
|
+
changed()
|
|
113
202
|
} catch (error) {
|
|
114
203
|
fail(error)
|
|
115
204
|
}
|
|
116
205
|
}
|
|
117
206
|
const wait = (ms: number, callback: () => void) => {
|
|
118
207
|
if (stopped) return
|
|
119
|
-
if (
|
|
120
|
-
|
|
208
|
+
if (!controlled) {
|
|
209
|
+
if (ms <= 0) {
|
|
210
|
+
callback()
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
let cancel = () => {}
|
|
214
|
+
let fired = false
|
|
215
|
+
const off = driver.after(ms, () => {
|
|
216
|
+
fired = true
|
|
217
|
+
cancellations.delete(cancel)
|
|
218
|
+
if (!stopped) callback()
|
|
219
|
+
})
|
|
220
|
+
cancel = off
|
|
221
|
+
if (!fired) cancellations.add(cancel)
|
|
121
222
|
return
|
|
122
223
|
}
|
|
123
|
-
let
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
224
|
+
let off = () => {}
|
|
225
|
+
const waiting = {
|
|
226
|
+
at: time() + Math.max(0, ms),
|
|
227
|
+
fire() {
|
|
228
|
+
off()
|
|
229
|
+
waits.delete(waiting)
|
|
230
|
+
if (!stopped) callback()
|
|
231
|
+
},
|
|
232
|
+
cancel() {
|
|
233
|
+
off()
|
|
234
|
+
off = () => {}
|
|
235
|
+
},
|
|
236
|
+
schedule() {
|
|
237
|
+
waiting.cancel()
|
|
238
|
+
if (paused || stopped) return
|
|
239
|
+
const remaining = waiting.at - time()
|
|
240
|
+
if (remaining <= 0) waiting.fire()
|
|
241
|
+
else off = driver.after(remaining, waiting.fire)
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
waits.add(waiting)
|
|
245
|
+
waiting.schedule()
|
|
132
246
|
}
|
|
133
247
|
|
|
134
248
|
for (let index = 0; index < states.length; index++) {
|
|
@@ -214,7 +328,11 @@ export function sequence(steps: readonly Step[]): Animation {
|
|
|
214
328
|
const arrived = scheduled.then(() => Promise.all(started.map(arrivalOf)))
|
|
215
329
|
const finished = arrived.then(async () => {
|
|
216
330
|
const results = await Promise.all(started.map((animation) => animation.finished))
|
|
217
|
-
|
|
331
|
+
completed = !stopped && results.length === steps.length && results.every(Boolean)
|
|
332
|
+
settled = true
|
|
333
|
+
clock?.cancel()
|
|
334
|
+
changed()
|
|
335
|
+
return completed
|
|
218
336
|
})
|
|
219
337
|
|
|
220
338
|
const animation: Animation = {
|
|
@@ -222,13 +340,69 @@ export function sequence(steps: readonly Step[]): Animation {
|
|
|
222
340
|
stop() {
|
|
223
341
|
if (stopped) return
|
|
224
342
|
stopped = true
|
|
343
|
+
settled = true
|
|
225
344
|
cancelSchedule()
|
|
226
345
|
states.forEach((state) => state.onStart.clear())
|
|
227
346
|
resolveScheduled()
|
|
228
347
|
started.forEach((animation) => animation.stop())
|
|
348
|
+
resumeChildren.clear()
|
|
349
|
+
changed()
|
|
229
350
|
},
|
|
230
351
|
then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
|
|
231
352
|
}
|
|
353
|
+
if (controlled) {
|
|
354
|
+
const duration = () => {
|
|
355
|
+
if (pending) return undefined
|
|
356
|
+
let total = 0
|
|
357
|
+
for (const state of states) {
|
|
358
|
+
const child = state.animation as PlayableAnimation | undefined
|
|
359
|
+
if (!child || child.duration === undefined) return undefined
|
|
360
|
+
try {
|
|
361
|
+
if (!playbackSource(child).read().ready) return undefined
|
|
362
|
+
} catch {
|
|
363
|
+
/* custom playable */
|
|
364
|
+
}
|
|
365
|
+
total = Math.max(total, (state.start ?? 0) + child.duration)
|
|
366
|
+
}
|
|
367
|
+
return total
|
|
368
|
+
}
|
|
369
|
+
Object.defineProperties(animation, {
|
|
370
|
+
paused: { get: () => paused },
|
|
371
|
+
duration: { get: duration },
|
|
372
|
+
})
|
|
373
|
+
Object.assign(animation, {
|
|
374
|
+
pause() {
|
|
375
|
+
if (paused || settled) return
|
|
376
|
+
paused = true
|
|
377
|
+
clock?.pause()
|
|
378
|
+
for (const wait of waits) wait.cancel()
|
|
379
|
+
for (const child of started as PlayableAnimation[]) {
|
|
380
|
+
if (!child.paused) {
|
|
381
|
+
resumeChildren.add(child)
|
|
382
|
+
child.pause()
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
changed()
|
|
386
|
+
},
|
|
387
|
+
play() {
|
|
388
|
+
if (!paused || settled) return
|
|
389
|
+
paused = false
|
|
390
|
+
clock?.play()
|
|
391
|
+
for (const child of resumeChildren) child.play()
|
|
392
|
+
resumeChildren.clear()
|
|
393
|
+
for (const wait of [...waits]) wait.schedule()
|
|
394
|
+
changed()
|
|
395
|
+
},
|
|
396
|
+
})
|
|
397
|
+
changed = registerPlayback(animation, () => ({
|
|
398
|
+
time: time(),
|
|
399
|
+
duration: duration() ?? Infinity,
|
|
400
|
+
paused,
|
|
401
|
+
ready: duration() !== undefined,
|
|
402
|
+
settled,
|
|
403
|
+
completed,
|
|
404
|
+
}))
|
|
405
|
+
}
|
|
232
406
|
setArrival(animation, arrived)
|
|
233
407
|
return animation
|
|
234
408
|
}
|
|
@@ -276,3 +450,18 @@ function applyStartingValues(steps: readonly Step[]) {
|
|
|
276
450
|
}
|
|
277
451
|
}
|
|
278
452
|
}
|
|
453
|
+
|
|
454
|
+
function isPlayable(animation: Animation): animation is PlayableAnimation {
|
|
455
|
+
const handle = animation as PlayableAnimation | undefined
|
|
456
|
+
return (
|
|
457
|
+
!!handle &&
|
|
458
|
+
typeof handle.pause === 'function' &&
|
|
459
|
+
typeof handle.play === 'function' &&
|
|
460
|
+
typeof handle.paused === 'boolean' &&
|
|
461
|
+
typeof handle.stop === 'function' &&
|
|
462
|
+
typeof handle.finished?.then === 'function' &&
|
|
463
|
+
typeof handle.finished?.catch === 'function' &&
|
|
464
|
+
(handle.duration === undefined ||
|
|
465
|
+
(typeof handle.duration === 'number' && handle.duration >= 0))
|
|
466
|
+
)
|
|
467
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { getDriver } from './driver/index.js'
|
|
2
|
+
import { Trace } from '@nativescript/core'
|
|
3
|
+
import { setArrival, setArrivalTiming } from './engine.js'
|
|
4
|
+
import {
|
|
5
|
+
createClock,
|
|
6
|
+
registerPlayback,
|
|
7
|
+
validateProgress,
|
|
8
|
+
type PlaybackAnimation,
|
|
9
|
+
type PlaybackCommand,
|
|
10
|
+
} from './playback.js'
|
|
11
|
+
|
|
12
|
+
/** A sampled resource's control clock. Native sinks play frames; JS runs only at boundaries. */
|
|
13
|
+
export class TimedAnimation implements PlaybackAnimation {
|
|
14
|
+
readonly finished: Promise<boolean>
|
|
15
|
+
private resolve!: (value: boolean) => void
|
|
16
|
+
private arrive!: () => void
|
|
17
|
+
private readonly clock
|
|
18
|
+
private readonly notifyPlayback: () => void
|
|
19
|
+
private readonly listeners = new Set<() => void>()
|
|
20
|
+
private cancelEnd?: () => void
|
|
21
|
+
private cancelArrival?: () => void
|
|
22
|
+
private _paused = false
|
|
23
|
+
private settled = false
|
|
24
|
+
private completed = false
|
|
25
|
+
private arrived = false
|
|
26
|
+
private active = false
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
readonly duration: number,
|
|
30
|
+
private readonly arrival: number,
|
|
31
|
+
private readonly update: (
|
|
32
|
+
time: number,
|
|
33
|
+
paused: boolean,
|
|
34
|
+
command: PlaybackCommand,
|
|
35
|
+
) => void,
|
|
36
|
+
private readonly finish: (completed: boolean) => void,
|
|
37
|
+
) {
|
|
38
|
+
const driver = getDriver()
|
|
39
|
+
this.clock = driver.clock?.(duration) ?? createClock(() => driver.now())
|
|
40
|
+
this.finished = new Promise((resolve) => {
|
|
41
|
+
this.resolve = resolve
|
|
42
|
+
})
|
|
43
|
+
setArrival(
|
|
44
|
+
this,
|
|
45
|
+
new Promise<void>((resolve) => {
|
|
46
|
+
this.arrive = resolve
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
this.notifyPlayback = registerPlayback(this, () => ({
|
|
50
|
+
time: this.time,
|
|
51
|
+
duration,
|
|
52
|
+
paused: this._paused,
|
|
53
|
+
ready: this.active,
|
|
54
|
+
settled: this.settled,
|
|
55
|
+
completed: this.completed,
|
|
56
|
+
}))
|
|
57
|
+
setArrivalTiming(this, {
|
|
58
|
+
read: () => ({
|
|
59
|
+
remaining: Math.max(0, arrival - this.time),
|
|
60
|
+
paused: this.paused,
|
|
61
|
+
arrived: this.arrived,
|
|
62
|
+
}),
|
|
63
|
+
subscribe: (callback) => {
|
|
64
|
+
this.listeners.add(callback)
|
|
65
|
+
return () => {
|
|
66
|
+
this.listeners.delete(callback)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get time() {
|
|
73
|
+
return Math.max(0, Math.min(this.duration, this.clock.time()))
|
|
74
|
+
}
|
|
75
|
+
get paused() {
|
|
76
|
+
return this._paused
|
|
77
|
+
}
|
|
78
|
+
get done() {
|
|
79
|
+
return this.settled
|
|
80
|
+
}
|
|
81
|
+
get progress() {
|
|
82
|
+
if (!Number.isFinite(this.duration))
|
|
83
|
+
throw new RangeError('Progress requires a finite animation.')
|
|
84
|
+
return this.completed ? 1 : this.duration ? this.time / this.duration : 0
|
|
85
|
+
}
|
|
86
|
+
set progress(value: number) {
|
|
87
|
+
validateProgress(value, this.duration)
|
|
88
|
+
this.seekTime(value * this.duration)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
start() {
|
|
92
|
+
if (this.active || this.settled) return
|
|
93
|
+
this.active = true
|
|
94
|
+
this.schedule()
|
|
95
|
+
this.notify()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
pause() {
|
|
99
|
+
if (this._paused || this.settled) return
|
|
100
|
+
this.clock.pause()
|
|
101
|
+
this._paused = true
|
|
102
|
+
this.cancelTimers()
|
|
103
|
+
this.update(this.time, true, 'pause')
|
|
104
|
+
this.notify()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
play() {
|
|
108
|
+
if (!this._paused || this.settled) return
|
|
109
|
+
this._paused = false
|
|
110
|
+
this.clock.play()
|
|
111
|
+
this.update(this.time, false, 'play')
|
|
112
|
+
this.schedule()
|
|
113
|
+
this.notify()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
seekTime(time: number) {
|
|
117
|
+
if (!Number.isFinite(time) || time < 0)
|
|
118
|
+
throw new RangeError('Seek time must be finite and nonnegative.')
|
|
119
|
+
if (this.settled) return
|
|
120
|
+
this.clock.seek(Math.min(this.duration, time))
|
|
121
|
+
this.update(this.time, this._paused, 'seek')
|
|
122
|
+
this.schedule()
|
|
123
|
+
this.notify()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
stop() {
|
|
127
|
+
this.settle(false)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
then<A = boolean, B = never>(
|
|
131
|
+
onfulfilled?: ((value: boolean) => A | PromiseLike<A>) | null,
|
|
132
|
+
onrejected?: ((reason: unknown) => B | PromiseLike<B>) | null,
|
|
133
|
+
): Promise<A | B> {
|
|
134
|
+
return this.finished.then(onfulfilled, onrejected)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private notify() {
|
|
138
|
+
this.notifyPlayback()
|
|
139
|
+
for (const callback of [...this.listeners]) callback()
|
|
140
|
+
}
|
|
141
|
+
private cancelTimers() {
|
|
142
|
+
this.cancelEnd?.()
|
|
143
|
+
this.cancelArrival?.()
|
|
144
|
+
this.cancelEnd = this.cancelArrival = undefined
|
|
145
|
+
}
|
|
146
|
+
private schedule() {
|
|
147
|
+
this.cancelTimers()
|
|
148
|
+
if (!this.active || this._paused || this.settled) return
|
|
149
|
+
const driver = getDriver()
|
|
150
|
+
if (Number.isFinite(this.duration)) {
|
|
151
|
+
const remaining = this.duration - this.time
|
|
152
|
+
if (remaining <= 0) {
|
|
153
|
+
this.settle(true)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
this.cancelEnd = driver.after(remaining, () => this.settle(true))
|
|
157
|
+
}
|
|
158
|
+
if (!this.arrived && Number.isFinite(this.arrival)) {
|
|
159
|
+
const remaining = this.arrival - this.time
|
|
160
|
+
const arrive = () => {
|
|
161
|
+
this.arrived = true
|
|
162
|
+
this.arrive()
|
|
163
|
+
this.notify()
|
|
164
|
+
}
|
|
165
|
+
if (remaining <= 0) arrive()
|
|
166
|
+
else this.cancelArrival = driver.after(remaining, arrive)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
private settle(completed: boolean) {
|
|
170
|
+
if (this.settled) return
|
|
171
|
+
this.settled = true
|
|
172
|
+
this.completed = completed
|
|
173
|
+
this.cancelTimers()
|
|
174
|
+
if (completed) this.clock.seek(this.duration)
|
|
175
|
+
this.clock.cancel()
|
|
176
|
+
try {
|
|
177
|
+
this.finish(completed)
|
|
178
|
+
} catch (error) {
|
|
179
|
+
this.completed = false
|
|
180
|
+
Trace.write(
|
|
181
|
+
`Jolt sampled playback failed: ${String(error)}`,
|
|
182
|
+
Trace.categories.Error,
|
|
183
|
+
Trace.messageType.error,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
this.arrived = true
|
|
187
|
+
this.arrive()
|
|
188
|
+
this.notify()
|
|
189
|
+
this.resolve(this.completed)
|
|
190
|
+
this.listeners.clear()
|
|
191
|
+
}
|
|
192
|
+
}
|