@rafaelaugusto/jolt 0.1.0-beta.12 → 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 +4 -1
- package/dist/animate.d.ts +6 -4
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +3 -1
- package/dist/animate.js.map +1 -1
- package/dist/drag.js +3 -3
- 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/engine.d.ts +2 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +23 -2
- package/dist/engine.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 +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/motion-scope.d.ts +2 -1
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +4 -1
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-value.d.ts +45 -8
- package/dist/motion-value.d.ts.map +1 -1
- package/dist/motion-value.js +289 -91
- package/dist/motion-value.js.map +1 -1
- 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/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/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/package.json +6 -2
- package/src/android-plugin.d.ts +37 -0
- package/src/animate.ts +13 -7
- package/src/drag.ts +3 -3
- 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/engine.ts +23 -2
- package/src/image-effect.ts +10 -2
- package/src/index.ts +5 -0
- package/src/ios-plugin.d.ts +33 -0
- package/src/motion-scope.ts +5 -1
- package/src/motion-value.ts +331 -99
- 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/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/value-observers.ts +133 -0
- package/src/value-track.ts +71 -0
package/src/motion-value.ts
CHANGED
|
@@ -20,14 +20,7 @@ import {
|
|
|
20
20
|
validateDecorationTarget,
|
|
21
21
|
} from './decoration.js'
|
|
22
22
|
import { getDriver } from './driver/index.js'
|
|
23
|
-
import {
|
|
24
|
-
setArrival,
|
|
25
|
-
setBase,
|
|
26
|
-
stopBase,
|
|
27
|
-
ViewAnimation,
|
|
28
|
-
type Animation,
|
|
29
|
-
type Plan,
|
|
30
|
-
} from './engine.js'
|
|
23
|
+
import { setBase, stopBase, ViewAnimation, type Animation, type Plan } from './engine.js'
|
|
31
24
|
import { linear } from './easing.js'
|
|
32
25
|
import { isProperty, type Property } from './properties.js'
|
|
33
26
|
import { createTrack, instantTrack, type Track } from './track.js'
|
|
@@ -39,19 +32,32 @@ import {
|
|
|
39
32
|
type Transition,
|
|
40
33
|
} from './transition.js'
|
|
41
34
|
import type { KeyframeProperty, Target } from './types.js'
|
|
35
|
+
import { playbackSource, type PlaybackAnimation, type PlaybackClock } from './playback.js'
|
|
36
|
+
import { TimedAnimation } from './timed-animation.js'
|
|
37
|
+
import { valueTrack } from './value-track.js'
|
|
38
|
+
import {
|
|
39
|
+
ValueObservers,
|
|
40
|
+
type SubscribeOptions,
|
|
41
|
+
type ValueListener,
|
|
42
|
+
} from './value-observers.js'
|
|
42
43
|
|
|
43
44
|
export interface MapOptions {
|
|
44
45
|
/** Hold the first and last outputs outside the input range. Defaults to `true`. */
|
|
45
46
|
clamp?: boolean
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** Named motion value layers add scalar offsets before derived mappings. */
|
|
50
|
+
export type ValueAnimationOptions = Transition & { layer?: string }
|
|
51
|
+
|
|
48
52
|
declare const motionValueBrand: unique symbol
|
|
49
53
|
|
|
50
54
|
/** A number derived from a motion value, ready to bind to a view. */
|
|
51
55
|
export interface DerivedValue {
|
|
52
56
|
/** The value now, while it animates too. */
|
|
53
57
|
get(): number
|
|
54
|
-
/**
|
|
58
|
+
/** Observe changes in JavaScript on the native frame clock. Unsubscribe when finished. */
|
|
59
|
+
subscribe(listener: ValueListener<number>, options?: SubscribeOptions): () => void
|
|
60
|
+
/** Maps through ascending or descending input ranges, like React Native's `interpolate`. */
|
|
55
61
|
to(
|
|
56
62
|
input: readonly number[],
|
|
57
63
|
output: readonly number[],
|
|
@@ -63,6 +69,8 @@ export interface DerivedValue {
|
|
|
63
69
|
/** A color derived from a motion value, for `backgroundColor`, `borderColor` or `color`. */
|
|
64
70
|
export interface DerivedColor {
|
|
65
71
|
get(): Color
|
|
72
|
+
/** Observe mapped color changes in JavaScript on the native frame clock. */
|
|
73
|
+
subscribe(listener: ValueListener<Color>, options?: SubscribeOptions): () => void
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/** A number that animates natively and drives every view property bound to it. */
|
|
@@ -88,12 +96,13 @@ interface Run {
|
|
|
88
96
|
readonly track: Track
|
|
89
97
|
readonly base: number
|
|
90
98
|
readonly span: number
|
|
91
|
-
readonly animation:
|
|
99
|
+
readonly animation: TimedAnimation
|
|
92
100
|
readonly startedAt: number
|
|
93
|
-
readonly
|
|
94
|
-
readonly cancel: () => void
|
|
101
|
+
readonly clock?: Pick<PlaybackClock, 'time'>
|
|
95
102
|
}
|
|
96
103
|
|
|
104
|
+
type Presentation = Pick<Run, 'track' | 'base' | 'span' | 'startedAt' | 'clock'>
|
|
105
|
+
|
|
97
106
|
const bound = new WeakMap<View, Map<Property, Binding>>()
|
|
98
107
|
|
|
99
108
|
class Derived {
|
|
@@ -112,6 +121,10 @@ class Derived {
|
|
|
112
121
|
return this.color ? new Color(value) : value
|
|
113
122
|
}
|
|
114
123
|
|
|
124
|
+
subscribe(listener: ValueListener<number | Color>, options?: SubscribeOptions) {
|
|
125
|
+
return this.root.observers.subscribe(this.read, this.color, listener, options)
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
to(
|
|
116
129
|
input: readonly number[],
|
|
117
130
|
output: readonly (number | ColorValue)[],
|
|
@@ -126,46 +139,71 @@ class Derived {
|
|
|
126
139
|
|
|
127
140
|
class Root extends Derived {
|
|
128
141
|
readonly bindings = new Set<Binding>()
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
readonly listeners = new Set<() => void>()
|
|
143
|
+
readonly observers = new ValueObservers((time) => this.raw(time))
|
|
144
|
+
private readonly rests = new Map<string, number>()
|
|
145
|
+
private readonly runs = new Map<string, Run>()
|
|
146
|
+
private presentation: Presentation | undefined
|
|
147
|
+
private cancelPresentation: (() => void) | undefined
|
|
131
148
|
|
|
132
149
|
constructor(initial: number) {
|
|
133
150
|
super(undefined, (value) => value, false)
|
|
134
151
|
if (!Number.isFinite(initial))
|
|
135
152
|
throw new RangeError(`A motion value starts from a finite number, got ${initial}.`)
|
|
136
|
-
this.
|
|
153
|
+
this.rests.set('', initial)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
raw(time = getDriver().now()): number {
|
|
157
|
+
let result = 0
|
|
158
|
+
for (const key of new Set([...this.rests.keys(), ...this.runs.keys()]))
|
|
159
|
+
result += this.part(key, time)
|
|
160
|
+
return result
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Drag input owns only the base; bounds and snap points stay in base units. */
|
|
164
|
+
base(): number {
|
|
165
|
+
return this.part('')
|
|
137
166
|
}
|
|
138
167
|
|
|
139
|
-
|
|
140
|
-
const run = this.
|
|
141
|
-
|
|
142
|
-
|
|
168
|
+
private part(key: string, time = getDriver().now()): number {
|
|
169
|
+
const run = this.runs.get(key)
|
|
170
|
+
return run
|
|
171
|
+
? run.base + run.span * run.track.valueAt(this.runTime(run, time))
|
|
172
|
+
: (this.rests.get(key) ?? 0)
|
|
143
173
|
}
|
|
144
174
|
|
|
145
|
-
private
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
175
|
+
private runTime(run: Presentation, time = getDriver().now()) {
|
|
176
|
+
return run.clock?.time() ?? time - run.startedAt
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private velocity(key: string): number {
|
|
180
|
+
const run = this.runs.get(key)
|
|
181
|
+
if (!run || run.animation.paused) return 0
|
|
182
|
+
return run.span * run.track.velocityAt(this.runTime(run))
|
|
149
183
|
}
|
|
150
184
|
|
|
151
185
|
animate(
|
|
152
186
|
target: number | readonly (number | null)[],
|
|
153
|
-
transition:
|
|
154
|
-
):
|
|
155
|
-
const
|
|
187
|
+
transition: ValueAnimationOptions | SpringName = {},
|
|
188
|
+
): PlaybackAnimation {
|
|
189
|
+
const options: ValueAnimationOptions =
|
|
190
|
+
typeof transition === 'string' ? springOf(transition) : transition
|
|
191
|
+
const key = options.layer === undefined ? '' : layerName(options.layer)
|
|
192
|
+
const from = this.part(key)
|
|
156
193
|
const frames = (typeof target === 'number' ? [null, target] : [...target]).map(
|
|
157
194
|
(frame) => (frame === null ? from : frame),
|
|
158
195
|
)
|
|
159
196
|
if (frames.length < 2) frames.unshift(from)
|
|
160
197
|
if (frames.some((frame) => typeof frame !== 'number' || !Number.isFinite(frame)))
|
|
161
198
|
throw new RangeError(`Invalid motion value keyframes: ${JSON.stringify(target)}.`)
|
|
162
|
-
const options = typeof transition === 'string' ? springOf(transition) : transition
|
|
163
199
|
const resolved = resolveTransition(options, 'x', frames.length)
|
|
164
200
|
const base = Math.min(...frames)
|
|
165
201
|
const span = Math.max(...frames) - base
|
|
166
|
-
const velocity = this.velocity()
|
|
167
|
-
|
|
168
|
-
|
|
202
|
+
const velocity = this.velocity(key)
|
|
203
|
+
if (span === 0) {
|
|
204
|
+
this.halt(key)
|
|
205
|
+
return this.jump(frames.at(-1)!, key)
|
|
206
|
+
}
|
|
169
207
|
|
|
170
208
|
// Sampled in the value's own range so springs settle relative to the distance.
|
|
171
209
|
const track = createTrack(
|
|
@@ -174,99 +212,158 @@ class Root extends Derived {
|
|
|
174
212
|
resolved,
|
|
175
213
|
velocity / span,
|
|
176
214
|
)
|
|
177
|
-
|
|
215
|
+
this.halt(key)
|
|
216
|
+
return track.end === 0
|
|
217
|
+
? this.jump(base + span * track.to, key)
|
|
218
|
+
: this.start(track, base, span, key)
|
|
178
219
|
}
|
|
179
220
|
|
|
180
221
|
/** Plays a track sampled in the value's own units, such as a drag release. */
|
|
181
|
-
play(track: Track):
|
|
182
|
-
this.halt()
|
|
183
|
-
return track.end === 0 ? this.jump(track.to) : this.start(track, 0, 1)
|
|
222
|
+
play(track: Track): PlaybackAnimation {
|
|
223
|
+
this.halt('')
|
|
224
|
+
return track.end === 0 ? this.jump(track.to, '') : this.start(track, 0, 1, '')
|
|
184
225
|
}
|
|
185
226
|
|
|
186
|
-
private jump(value: number):
|
|
187
|
-
|
|
188
|
-
this.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
227
|
+
private jump(value: number, key: string): PlaybackAnimation {
|
|
228
|
+
this.rests.set(key, value)
|
|
229
|
+
this.sync()
|
|
230
|
+
const animation = new TimedAnimation(
|
|
231
|
+
0,
|
|
232
|
+
0,
|
|
233
|
+
() => {},
|
|
234
|
+
() => {},
|
|
235
|
+
)
|
|
236
|
+
animation.start()
|
|
192
237
|
return animation
|
|
193
238
|
}
|
|
194
239
|
|
|
195
|
-
private
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (this.run?.animation === animation) this.stop()
|
|
202
|
-
},
|
|
203
|
-
then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
|
|
204
|
-
}
|
|
205
|
-
return { animation, settle }
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
private start(track: Track, base: number, span: number): Animation {
|
|
209
|
-
const { animation, settle } = this.handle()
|
|
210
|
-
let arrive: () => void = () => {}
|
|
211
|
-
let run: Run | undefined
|
|
240
|
+
private start(
|
|
241
|
+
track: Track,
|
|
242
|
+
base: number,
|
|
243
|
+
span: number,
|
|
244
|
+
key: string,
|
|
245
|
+
): PlaybackAnimation {
|
|
212
246
|
const driver = getDriver()
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
]
|
|
228
|
-
this.run = run = {
|
|
247
|
+
const animation = new TimedAnimation(
|
|
248
|
+
track.end,
|
|
249
|
+
track.arrival,
|
|
250
|
+
() => this.sync(),
|
|
251
|
+
(completed) => {
|
|
252
|
+
if (this.runs.get(key)?.animation !== animation) return
|
|
253
|
+
const held = base + span * track.valueAt(animation.time)
|
|
254
|
+
this.runs.delete(key)
|
|
255
|
+
if (!completed && key !== '') this.rests.delete(key)
|
|
256
|
+
else this.rests.set(key, completed ? base + span * track.to : held)
|
|
257
|
+
this.sync()
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
const run: Run = {
|
|
229
261
|
track,
|
|
230
262
|
base,
|
|
231
263
|
span,
|
|
232
264
|
animation,
|
|
233
265
|
startedAt: driver.now(),
|
|
234
|
-
|
|
235
|
-
arrive()
|
|
236
|
-
settle(completed)
|
|
237
|
-
},
|
|
238
|
-
cancel: () => timers.forEach((cancel) => cancel?.()),
|
|
266
|
+
clock: { time: () => animation.time },
|
|
239
267
|
}
|
|
240
|
-
|
|
268
|
+
this.runs.set(key, run)
|
|
269
|
+
this.sync()
|
|
270
|
+
animation.start()
|
|
241
271
|
return animation
|
|
242
272
|
}
|
|
243
273
|
|
|
244
274
|
set(value: number) {
|
|
245
275
|
if (!Number.isFinite(value))
|
|
246
276
|
throw new RangeError(`A motion value takes a finite number, got ${value}.`)
|
|
247
|
-
this.halt()
|
|
248
|
-
this.
|
|
249
|
-
|
|
277
|
+
for (const key of [...this.runs.keys()]) this.halt(key)
|
|
278
|
+
this.rests.clear()
|
|
279
|
+
this.rests.set('', value)
|
|
280
|
+
this.sync()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
setBase(value: number) {
|
|
284
|
+
if (!Number.isFinite(value))
|
|
285
|
+
throw new RangeError('A motion value takes a finite number.')
|
|
286
|
+
this.halt('')
|
|
287
|
+
this.rests.set('', value)
|
|
288
|
+
this.sync()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
stopBase() {
|
|
292
|
+
this.stopChannel('')
|
|
250
293
|
}
|
|
251
294
|
|
|
252
|
-
stop() {
|
|
253
|
-
if (
|
|
295
|
+
stop(layer?: string) {
|
|
296
|
+
if (layer !== undefined) return this.stopChannel(layerName(layer))
|
|
297
|
+
if (!this.runs.size && this.rests.size === 1) return
|
|
254
298
|
const value = this.raw()
|
|
255
|
-
this.halt()
|
|
256
|
-
this.
|
|
299
|
+
for (const key of [...this.runs.keys()]) this.halt(key)
|
|
300
|
+
this.rests.clear()
|
|
301
|
+
this.rests.set('', value)
|
|
302
|
+
this.sync()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private stopChannel(key: string) {
|
|
306
|
+
if (!this.runs.has(key) && (key === '' || !this.rests.has(key))) return
|
|
307
|
+
const held = this.part(key)
|
|
308
|
+
this.halt(key)
|
|
309
|
+
if (key === '') this.rests.set(key, held)
|
|
310
|
+
else this.rests.delete(key)
|
|
311
|
+
this.sync()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private sync() {
|
|
315
|
+
this.cancelPresentation?.()
|
|
316
|
+
this.cancelPresentation = undefined
|
|
317
|
+
const now = getDriver().now()
|
|
318
|
+
let rest = 0
|
|
319
|
+
for (const [key, value] of this.rests) if (!this.runs.has(key)) rest += value
|
|
320
|
+
const runs = [...this.runs.values()].filter((run) => !run.animation.paused)
|
|
321
|
+
for (const [key, run] of this.runs)
|
|
322
|
+
if (run.animation.paused) rest += this.part(key, now)
|
|
323
|
+
if (!runs.length) this.presentation = undefined
|
|
324
|
+
else if (runs.length === 1) {
|
|
325
|
+
const run = runs[0]!
|
|
326
|
+
this.presentation = { ...run, base: run.base + rest }
|
|
327
|
+
} else {
|
|
328
|
+
const { track, windowed } = valueTrack(
|
|
329
|
+
runs.map((run) => ({
|
|
330
|
+
track: run.track,
|
|
331
|
+
base: run.base,
|
|
332
|
+
span: run.span,
|
|
333
|
+
elapsed: this.runTime(run, now),
|
|
334
|
+
})),
|
|
335
|
+
rest,
|
|
336
|
+
)
|
|
337
|
+
const driver = getDriver()
|
|
338
|
+
const clock = driver.clock?.(track.end)
|
|
339
|
+
this.presentation = { track, base: 0, span: 1, startedAt: now, clock }
|
|
340
|
+
const cancelWindow = windowed
|
|
341
|
+
? driver.after(track.end, () => this.sync())
|
|
342
|
+
: undefined
|
|
343
|
+
this.cancelPresentation = () => {
|
|
344
|
+
cancelWindow?.()
|
|
345
|
+
clock?.cancel()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
this.changed()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
protected changed() {
|
|
257
352
|
for (const binding of this.bindings) binding.sync()
|
|
353
|
+
for (const listener of [...this.listeners]) listener()
|
|
354
|
+
this.observers.changed(this.running)
|
|
258
355
|
}
|
|
259
356
|
|
|
260
357
|
/** The plan that plays this value's run, mapped, on one bound property. */
|
|
261
358
|
plan(binding: Binding): Plan {
|
|
262
|
-
const run = this.
|
|
359
|
+
const run = this.presentation
|
|
263
360
|
const { property, read } = binding
|
|
264
361
|
const write = (value: number) =>
|
|
265
362
|
isDecorationProperty(property)
|
|
266
363
|
? clampDecoration(property, read(value))
|
|
267
364
|
: read(value)
|
|
268
365
|
if (!run) {
|
|
269
|
-
const value = write(this.
|
|
366
|
+
const value = write(this.raw())
|
|
270
367
|
return {
|
|
271
368
|
property,
|
|
272
369
|
keyframes: [],
|
|
@@ -313,22 +410,31 @@ class Root extends Derived {
|
|
|
313
410
|
}
|
|
314
411
|
|
|
315
412
|
elapsed(): number {
|
|
316
|
-
return this.
|
|
413
|
+
return this.presentation ? this.runTime(this.presentation) : 0
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
get paused(): boolean {
|
|
417
|
+
return this.runs.size > 0 && !this.running
|
|
317
418
|
}
|
|
318
419
|
|
|
319
420
|
get running(): boolean {
|
|
320
|
-
return this.run
|
|
421
|
+
return [...this.runs.values()].some((run) => !run.animation.paused)
|
|
321
422
|
}
|
|
322
423
|
|
|
323
|
-
private halt() {
|
|
324
|
-
const run = this.
|
|
424
|
+
private halt(key: string) {
|
|
425
|
+
const run = this.runs.get(key)
|
|
325
426
|
if (!run) return
|
|
326
|
-
this.
|
|
327
|
-
run.
|
|
328
|
-
run.settle(false)
|
|
427
|
+
this.runs.delete(key)
|
|
428
|
+
run.animation.stop()
|
|
329
429
|
}
|
|
330
430
|
}
|
|
331
431
|
|
|
432
|
+
function layerName(name: string): string {
|
|
433
|
+
if (typeof name !== 'string' || !name.length)
|
|
434
|
+
throw new TypeError('A layer is named by a nonempty string.')
|
|
435
|
+
return name
|
|
436
|
+
}
|
|
437
|
+
|
|
332
438
|
const still: ResolvedTransition = {
|
|
333
439
|
curve: { type: 'tween', duration: 0, ease: [linear], times: undefined },
|
|
334
440
|
delay: 0,
|
|
@@ -424,9 +530,13 @@ function attach(view: View, property: Property, source: Derived) {
|
|
|
424
530
|
getDriver().sync?.(view)
|
|
425
531
|
return
|
|
426
532
|
}
|
|
427
|
-
const elapsed = root.elapsed()
|
|
428
533
|
const animation = new ViewAnimation(view, [root.plan(binding)])
|
|
429
|
-
|
|
534
|
+
// Native keyframe setup can take time, especially for a composite track on Android.
|
|
535
|
+
// Catch up after construction so each binding joins the source's current phase.
|
|
536
|
+
const elapsed = root.elapsed()
|
|
537
|
+
if (root.paused) animation.pause()
|
|
538
|
+
if ((elapsed > 1 || root.paused) && animation.duration > 0)
|
|
539
|
+
animation.seekTime(elapsed)
|
|
430
540
|
binding.animation = animation
|
|
431
541
|
},
|
|
432
542
|
}
|
|
@@ -444,6 +554,118 @@ function attach(view: View, property: Property, source: Derived) {
|
|
|
444
554
|
}
|
|
445
555
|
}
|
|
446
556
|
|
|
557
|
+
/** A read-only value on an existing finite animation's clock. No second animation or timer. */
|
|
558
|
+
export function playbackProgress(animation: Animation): DerivedValue {
|
|
559
|
+
const source = playbackSource(animation)
|
|
560
|
+
if (!Number.isFinite(source.read().duration))
|
|
561
|
+
throw new RangeError('Progress requires a finite animation.')
|
|
562
|
+
const root = new PlaybackValue(source)
|
|
563
|
+
return new Derived(root, (value) => value, false) as unknown as DerivedValue
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
class PlaybackValue extends Root {
|
|
567
|
+
constructor(private readonly playback: ReturnType<typeof playbackSource>) {
|
|
568
|
+
super(0)
|
|
569
|
+
const source = playback
|
|
570
|
+
const off = source.subscribe(() => {
|
|
571
|
+
this.changed()
|
|
572
|
+
if (source.read().settled) off()
|
|
573
|
+
})
|
|
574
|
+
if (source.read().settled) off()
|
|
575
|
+
this.observers.changed(this.running)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
override raw() {
|
|
579
|
+
const state = this.playback.read()
|
|
580
|
+
return (
|
|
581
|
+
state.position ??
|
|
582
|
+
(state.completed
|
|
583
|
+
? 1
|
|
584
|
+
: state.duration > 0
|
|
585
|
+
? Math.max(0, Math.min(1, state.time / state.duration))
|
|
586
|
+
: 0)
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
override get running() {
|
|
591
|
+
const state = this.playback.read()
|
|
592
|
+
return state.ready && !state.settled && !state.paused && state.duration > 0
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
override get paused() {
|
|
596
|
+
return this.playback.read().paused
|
|
597
|
+
}
|
|
598
|
+
override elapsed() {
|
|
599
|
+
return this.playback.read().time
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
override plan(binding: Binding): Plan {
|
|
603
|
+
const state = this.playback.read()
|
|
604
|
+
if (!state.ready || state.settled || state.duration === 0)
|
|
605
|
+
return {
|
|
606
|
+
property: binding.property,
|
|
607
|
+
keyframes: [],
|
|
608
|
+
transition: still,
|
|
609
|
+
track: () => instantTrack(binding.property, binding.read(this.raw())),
|
|
610
|
+
}
|
|
611
|
+
// Bypass timeScale: the source duration is already in platform playback milliseconds.
|
|
612
|
+
const duration = state.duration
|
|
613
|
+
const at = (ms: number) => binding.read(Math.max(0, Math.min(1, ms / duration)))
|
|
614
|
+
const track: Track = {
|
|
615
|
+
property: binding.property,
|
|
616
|
+
delay: 0,
|
|
617
|
+
duration,
|
|
618
|
+
repeat: 0,
|
|
619
|
+
reverse: false,
|
|
620
|
+
times: [0, 1],
|
|
621
|
+
values: [at(0), at(duration)],
|
|
622
|
+
to: at(duration),
|
|
623
|
+
end: duration,
|
|
624
|
+
arrival: duration,
|
|
625
|
+
valueAt: at,
|
|
626
|
+
velocityAt: (ms) => (at(ms + 0.5) - at(ms - 0.5)) * 1000,
|
|
627
|
+
}
|
|
628
|
+
// Piecewise/color derivations need intermediate samples, not only the two endpoints.
|
|
629
|
+
const steps = Math.max(1, Math.min(6000, Math.ceil((duration * 60) / 1000)))
|
|
630
|
+
const times = Array.from({ length: steps + 1 }, (_, i) => i / steps)
|
|
631
|
+
return {
|
|
632
|
+
property: binding.property,
|
|
633
|
+
keyframes: [],
|
|
634
|
+
transition: still,
|
|
635
|
+
track: () =>
|
|
636
|
+
reducedMotion() &&
|
|
637
|
+
binding.property !== 'opacity' &&
|
|
638
|
+
!isColorProperty(binding.property)
|
|
639
|
+
? instantTrack(binding.property, at(duration))
|
|
640
|
+
: { ...track, times, values: times.map((time) => at(time * duration)) },
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** Internal native-adapter bridge. Mappings are sampled at boundaries, never per frame. */
|
|
646
|
+
export function valuePlayback(source: DerivedValue) {
|
|
647
|
+
if (!(source instanceof Derived) || source.color)
|
|
648
|
+
throw new TypeError('A native binding needs a numeric Jolt value.')
|
|
649
|
+
const { root, read } = source
|
|
650
|
+
return {
|
|
651
|
+
snapshot() {
|
|
652
|
+
return {
|
|
653
|
+
track: root.plan({ property: 'opacity', read } as Binding).track!(),
|
|
654
|
+
elapsed: root.elapsed(),
|
|
655
|
+
paused: root.paused,
|
|
656
|
+
running: root.running,
|
|
657
|
+
value: read(root.raw()),
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
subscribe(listener: () => void) {
|
|
661
|
+
root.listeners.add(listener)
|
|
662
|
+
return () => {
|
|
663
|
+
root.listeners.delete(listener)
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
447
669
|
function release(binding: Binding) {
|
|
448
670
|
binding.root.bindings.delete(binding)
|
|
449
671
|
binding.view.off('loaded', binding.sync)
|
|
@@ -472,12 +694,22 @@ function mapping(
|
|
|
472
694
|
throw new RangeError(
|
|
473
695
|
`to() has ${input.length} inputs but ${output.length} outputs; give one output per input.`,
|
|
474
696
|
)
|
|
697
|
+
if (input.some((value) => !Number.isFinite(value)))
|
|
698
|
+
throw new RangeError('to() inputs must all be finite numbers.')
|
|
699
|
+
const descending = input[input.length - 1]! < input[0]!
|
|
475
700
|
if (
|
|
476
701
|
input.some(
|
|
477
|
-
(value, i) =>
|
|
702
|
+
(value, i) => i > 0 && (descending ? value > input[i - 1]! : value < input[i - 1]!),
|
|
478
703
|
)
|
|
479
704
|
)
|
|
480
|
-
throw new RangeError(
|
|
705
|
+
throw new RangeError(
|
|
706
|
+
'to() inputs must be monotonic (ascending or descending); ranges cannot change direction.',
|
|
707
|
+
)
|
|
708
|
+
// Normalize pairs together, without changing caller-owned arrays or duplicate-stop rules.
|
|
709
|
+
if (descending) {
|
|
710
|
+
input = [...input].reverse()
|
|
711
|
+
output = [...output].reverse()
|
|
712
|
+
}
|
|
481
713
|
const color = typeof output[0] !== 'number'
|
|
482
714
|
const numbers = color ? output.map((value) => parseColor(value)) : (output as number[])
|
|
483
715
|
if (
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { registerNativeProperty, checkNativeProperty } from '../native-property.js'
|
|
2
|
+
export type { NativeProperty, NativePropertyDefinition } from '../native-property.js'
|
|
3
|
+
export type { NativeSink } from '../native-property-driver.js'
|
|
4
|
+
export {
|
|
5
|
+
mapKitCoordinate,
|
|
6
|
+
mapKitCamera,
|
|
7
|
+
mapLibreCoordinate,
|
|
8
|
+
mapLibreCamera,
|
|
9
|
+
sceneKitPose,
|
|
10
|
+
} from '../native-properties.js'
|
|
11
|
+
export type {
|
|
12
|
+
Coordinate,
|
|
13
|
+
MapKitCamera,
|
|
14
|
+
MapLibreCamera,
|
|
15
|
+
ScenePose,
|
|
16
|
+
} from '../native-properties.js'
|