@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
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Color,
|
|
3
|
+
ScrollView,
|
|
4
|
+
type EventData,
|
|
5
|
+
type ScrollEventData,
|
|
6
|
+
type View,
|
|
7
|
+
} from '@nativescript/core'
|
|
8
|
+
import { toViews } from './animate.js'
|
|
9
|
+
import {
|
|
10
|
+
colorMixer,
|
|
11
|
+
isColorProperty,
|
|
12
|
+
parseColor,
|
|
13
|
+
validateColorTarget,
|
|
14
|
+
type ColorValue,
|
|
15
|
+
} from './color.js'
|
|
16
|
+
import { reducedMotion } from './config.js'
|
|
17
|
+
import {
|
|
18
|
+
clampDecoration,
|
|
19
|
+
isDecorationProperty,
|
|
20
|
+
validateDecorationTarget,
|
|
21
|
+
} from './decoration.js'
|
|
22
|
+
import { getDriver } from './driver/index.js'
|
|
23
|
+
import { setBase, stopBase, ViewAnimation, type Animation, type Plan } from './engine.js'
|
|
24
|
+
import { linear } from './easing.js'
|
|
25
|
+
import { isProperty, type Property } from './properties.js'
|
|
26
|
+
import { createTrack, instantTrack, type Track } from './track.js'
|
|
27
|
+
import {
|
|
28
|
+
resolveTransition,
|
|
29
|
+
springs,
|
|
30
|
+
type ResolvedTransition,
|
|
31
|
+
type SpringName,
|
|
32
|
+
type Transition,
|
|
33
|
+
} from './transition.js'
|
|
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'
|
|
43
|
+
|
|
44
|
+
export interface MapOptions {
|
|
45
|
+
/** Hold the first and last outputs outside the input range. Defaults to `true`. */
|
|
46
|
+
clamp?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Named motion value layers add scalar offsets before derived mappings. */
|
|
50
|
+
export type ValueAnimationOptions = Transition & { layer?: string }
|
|
51
|
+
|
|
52
|
+
declare const motionValueBrand: unique symbol
|
|
53
|
+
|
|
54
|
+
/** A number derived from a motion value, ready to bind to a view. */
|
|
55
|
+
export interface DerivedValue {
|
|
56
|
+
/** The value now, while it animates too. */
|
|
57
|
+
get(): number
|
|
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`. */
|
|
61
|
+
to(
|
|
62
|
+
input: readonly number[],
|
|
63
|
+
output: readonly number[],
|
|
64
|
+
options?: MapOptions,
|
|
65
|
+
): DerivedValue
|
|
66
|
+
to(input: readonly number[], output: readonly ColorValue[]): DerivedColor
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A color derived from a motion value, for `backgroundColor`, `borderColor` or `color`. */
|
|
70
|
+
export interface DerivedColor {
|
|
71
|
+
get(): Color
|
|
72
|
+
/** Observe mapped color changes in JavaScript on the native frame clock. */
|
|
73
|
+
subscribe(listener: ValueListener<Color>, options?: SubscribeOptions): () => void
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A number that animates natively and drives every view property bound to it. */
|
|
77
|
+
export interface MotionValue extends DerivedValue {
|
|
78
|
+
readonly [motionValueBrand]: true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type ColorKey = 'backgroundColor' | 'borderColor' | 'color'
|
|
82
|
+
export type Bindings = {
|
|
83
|
+
[K in Exclude<KeyframeProperty, ColorKey>]?: DerivedValue
|
|
84
|
+
} & { [K in ColorKey]?: DerivedColor }
|
|
85
|
+
|
|
86
|
+
interface Binding {
|
|
87
|
+
readonly view: View
|
|
88
|
+
readonly property: Property
|
|
89
|
+
readonly root: Root
|
|
90
|
+
readonly read: (value: number) => number
|
|
91
|
+
animation?: ViewAnimation
|
|
92
|
+
sync(): void
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface Run {
|
|
96
|
+
readonly track: Track
|
|
97
|
+
readonly base: number
|
|
98
|
+
readonly span: number
|
|
99
|
+
readonly animation: TimedAnimation
|
|
100
|
+
readonly startedAt: number
|
|
101
|
+
readonly clock?: Pick<PlaybackClock, 'time'>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type Presentation = Pick<Run, 'track' | 'base' | 'span' | 'startedAt' | 'clock'>
|
|
105
|
+
|
|
106
|
+
const bound = new WeakMap<View, Map<Property, Binding>>()
|
|
107
|
+
|
|
108
|
+
class Derived {
|
|
109
|
+
constructor(
|
|
110
|
+
private readonly source: Root | undefined,
|
|
111
|
+
readonly read: (value: number) => number,
|
|
112
|
+
readonly color: boolean,
|
|
113
|
+
) {}
|
|
114
|
+
|
|
115
|
+
get root(): Root {
|
|
116
|
+
return this.source ?? (this as unknown as Root)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get(): number | Color {
|
|
120
|
+
const value = this.read(this.root.raw())
|
|
121
|
+
return this.color ? new Color(value) : value
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
subscribe(listener: ValueListener<number | Color>, options?: SubscribeOptions) {
|
|
125
|
+
return this.root.observers.subscribe(this.read, this.color, listener, options)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
to(
|
|
129
|
+
input: readonly number[],
|
|
130
|
+
output: readonly (number | ColorValue)[],
|
|
131
|
+
options?: MapOptions,
|
|
132
|
+
): Derived {
|
|
133
|
+
if (this.color) throw new TypeError('A color cannot be mapped again.')
|
|
134
|
+
const map = mapping(input, output, options)
|
|
135
|
+
const read = this.read
|
|
136
|
+
return new Derived(this.root, (value) => map.read(read(value)), map.color)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class Root extends Derived {
|
|
141
|
+
readonly bindings = new Set<Binding>()
|
|
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
|
|
148
|
+
|
|
149
|
+
constructor(initial: number) {
|
|
150
|
+
super(undefined, (value) => value, false)
|
|
151
|
+
if (!Number.isFinite(initial))
|
|
152
|
+
throw new RangeError(`A motion value starts from a finite number, got ${initial}.`)
|
|
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('')
|
|
166
|
+
}
|
|
167
|
+
|
|
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)
|
|
173
|
+
}
|
|
174
|
+
|
|
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))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
animate(
|
|
186
|
+
target: number | readonly (number | null)[],
|
|
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)
|
|
193
|
+
const frames = (typeof target === 'number' ? [null, target] : [...target]).map(
|
|
194
|
+
(frame) => (frame === null ? from : frame),
|
|
195
|
+
)
|
|
196
|
+
if (frames.length < 2) frames.unshift(from)
|
|
197
|
+
if (frames.some((frame) => typeof frame !== 'number' || !Number.isFinite(frame)))
|
|
198
|
+
throw new RangeError(`Invalid motion value keyframes: ${JSON.stringify(target)}.`)
|
|
199
|
+
const resolved = resolveTransition(options, 'x', frames.length)
|
|
200
|
+
const base = Math.min(...frames)
|
|
201
|
+
const span = Math.max(...frames) - base
|
|
202
|
+
const velocity = this.velocity(key)
|
|
203
|
+
if (span === 0) {
|
|
204
|
+
this.halt(key)
|
|
205
|
+
return this.jump(frames.at(-1)!, key)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Sampled in the value's own range so springs settle relative to the distance.
|
|
209
|
+
const track = createTrack(
|
|
210
|
+
'opacity',
|
|
211
|
+
frames.map((frame) => (frame - base) / span),
|
|
212
|
+
resolved,
|
|
213
|
+
velocity / span,
|
|
214
|
+
)
|
|
215
|
+
this.halt(key)
|
|
216
|
+
return track.end === 0
|
|
217
|
+
? this.jump(base + span * track.to, key)
|
|
218
|
+
: this.start(track, base, span, key)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Plays a track sampled in the value's own units, such as a drag release. */
|
|
222
|
+
play(track: Track): PlaybackAnimation {
|
|
223
|
+
this.halt('')
|
|
224
|
+
return track.end === 0 ? this.jump(track.to, '') : this.start(track, 0, 1, '')
|
|
225
|
+
}
|
|
226
|
+
|
|
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()
|
|
237
|
+
return animation
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private start(
|
|
241
|
+
track: Track,
|
|
242
|
+
base: number,
|
|
243
|
+
span: number,
|
|
244
|
+
key: string,
|
|
245
|
+
): PlaybackAnimation {
|
|
246
|
+
const driver = getDriver()
|
|
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 = {
|
|
261
|
+
track,
|
|
262
|
+
base,
|
|
263
|
+
span,
|
|
264
|
+
animation,
|
|
265
|
+
startedAt: driver.now(),
|
|
266
|
+
clock: { time: () => animation.time },
|
|
267
|
+
}
|
|
268
|
+
this.runs.set(key, run)
|
|
269
|
+
this.sync()
|
|
270
|
+
animation.start()
|
|
271
|
+
return animation
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
set(value: number) {
|
|
275
|
+
if (!Number.isFinite(value))
|
|
276
|
+
throw new RangeError(`A motion value takes a finite number, got ${value}.`)
|
|
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('')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
stop(layer?: string) {
|
|
296
|
+
if (layer !== undefined) return this.stopChannel(layerName(layer))
|
|
297
|
+
if (!this.runs.size && this.rests.size === 1) return
|
|
298
|
+
const value = this.raw()
|
|
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() {
|
|
352
|
+
for (const binding of this.bindings) binding.sync()
|
|
353
|
+
for (const listener of [...this.listeners]) listener()
|
|
354
|
+
this.observers.changed(this.running)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** The plan that plays this value's run, mapped, on one bound property. */
|
|
358
|
+
plan(binding: Binding): Plan {
|
|
359
|
+
const run = this.presentation
|
|
360
|
+
const { property, read } = binding
|
|
361
|
+
const write = (value: number) =>
|
|
362
|
+
isDecorationProperty(property)
|
|
363
|
+
? clampDecoration(property, read(value))
|
|
364
|
+
: read(value)
|
|
365
|
+
if (!run) {
|
|
366
|
+
const value = write(this.raw())
|
|
367
|
+
return {
|
|
368
|
+
property,
|
|
369
|
+
keyframes: [],
|
|
370
|
+
transition: still,
|
|
371
|
+
track: () => instantTrack(property, value),
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const { track, base, span } = run
|
|
375
|
+
const at = (progress: number) => write(base + span * progress)
|
|
376
|
+
const transition: ResolvedTransition = {
|
|
377
|
+
curve: {
|
|
378
|
+
type: 'tween',
|
|
379
|
+
duration: track.duration,
|
|
380
|
+
ease: [linear],
|
|
381
|
+
times: undefined,
|
|
382
|
+
},
|
|
383
|
+
delay: track.delay,
|
|
384
|
+
repeat: track.repeat,
|
|
385
|
+
reverse: track.reverse,
|
|
386
|
+
}
|
|
387
|
+
const mapped = (): Track => {
|
|
388
|
+
if (reducedMotion() && property !== 'opacity' && !isColorProperty(property))
|
|
389
|
+
return instantTrack(property, at(track.to))
|
|
390
|
+
return {
|
|
391
|
+
property,
|
|
392
|
+
delay: track.delay,
|
|
393
|
+
duration: track.duration,
|
|
394
|
+
repeat: track.repeat,
|
|
395
|
+
reverse: track.reverse,
|
|
396
|
+
times: track.times,
|
|
397
|
+
values: track.values.map(at),
|
|
398
|
+
to: at(track.to),
|
|
399
|
+
end: track.end,
|
|
400
|
+
arrival: track.arrival,
|
|
401
|
+
valueAt: (elapsed) => at(track.valueAt(elapsed)),
|
|
402
|
+
velocityAt: isColorProperty(property)
|
|
403
|
+
? () => 0
|
|
404
|
+
: (elapsed) =>
|
|
405
|
+
(at(track.valueAt(elapsed + 0.5)) - at(track.valueAt(elapsed - 0.5))) *
|
|
406
|
+
1000,
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return { property, keyframes: [], transition, track: mapped }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
elapsed(): number {
|
|
413
|
+
return this.presentation ? this.runTime(this.presentation) : 0
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
get paused(): boolean {
|
|
417
|
+
return this.runs.size > 0 && !this.running
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
get running(): boolean {
|
|
421
|
+
return [...this.runs.values()].some((run) => !run.animation.paused)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private halt(key: string) {
|
|
425
|
+
const run = this.runs.get(key)
|
|
426
|
+
if (!run) return
|
|
427
|
+
this.runs.delete(key)
|
|
428
|
+
run.animation.stop()
|
|
429
|
+
}
|
|
430
|
+
}
|
|
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
|
+
|
|
438
|
+
const still: ResolvedTransition = {
|
|
439
|
+
curve: { type: 'tween', duration: 0, ease: [linear], times: undefined },
|
|
440
|
+
delay: 0,
|
|
441
|
+
repeat: 0,
|
|
442
|
+
reverse: false,
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* A number that animates natively and drives the view properties bound to it.
|
|
447
|
+
*
|
|
448
|
+
* ```ts
|
|
449
|
+
* const progress = motionValue(0)
|
|
450
|
+
* bind(sheet, { y: progress.to([0, 1], [600, 0]) })
|
|
451
|
+
* animate(progress, 1, 'snappy')
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
454
|
+
export function motionValue(initial = 0): MotionValue {
|
|
455
|
+
return new Root(initial) as unknown as MotionValue
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function isMotionValue(target: unknown): target is MotionValue {
|
|
459
|
+
return target instanceof Root
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function asRoot(value: MotionValue): Root {
|
|
463
|
+
return value as unknown as Root
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Lets motion values drive a view's properties. The view takes their current values
|
|
468
|
+
* at once and follows every change on the same native clock. Returns a cleanup function.
|
|
469
|
+
*
|
|
470
|
+
* ```ts
|
|
471
|
+
* const release = bind(backdrop, { opacity: progress.to([0, 1], [0, 0.4]) })
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
474
|
+
export function bind(target: Target | readonly Target[], bindings: Bindings): () => void {
|
|
475
|
+
if (!bindings || typeof bindings !== 'object' || Array.isArray(bindings))
|
|
476
|
+
throw new TypeError('bind() takes an object of motion values.')
|
|
477
|
+
const views = toViews(target)
|
|
478
|
+
const entries: [Property, Derived][] = []
|
|
479
|
+
for (const [key, source] of Object.entries(bindings)) {
|
|
480
|
+
if (source === undefined) continue
|
|
481
|
+
if (!(source instanceof Derived))
|
|
482
|
+
throw new TypeError(`"${key}" needs a motion value, got ${String(source)}.`)
|
|
483
|
+
const properties: Property[] =
|
|
484
|
+
key === 'scale' ? ['scaleX', 'scaleY'] : isProperty(key) ? [key] : []
|
|
485
|
+
if (!properties.length) throw new Error(`Can't bind "${key}".`)
|
|
486
|
+
for (const property of properties) {
|
|
487
|
+
if (isColorProperty(property) !== source.color)
|
|
488
|
+
throw new TypeError(
|
|
489
|
+
source.color
|
|
490
|
+
? `"${key}" takes numbers; map the value to numbers.`
|
|
491
|
+
: `"${key}" is a color; map the value to colors with to(input, colors).`,
|
|
492
|
+
)
|
|
493
|
+
entries.push([property, source])
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const view of views)
|
|
497
|
+
for (const [property] of entries)
|
|
498
|
+
if (isColorProperty(property)) validateColorTarget(view, property)
|
|
499
|
+
else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
|
|
500
|
+
|
|
501
|
+
const made = views.flatMap((view) =>
|
|
502
|
+
entries.map(([property, source]) => attach(view, property, source)),
|
|
503
|
+
)
|
|
504
|
+
return () => made.forEach((release) => release())
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function attach(view: View, property: Property, source: Derived) {
|
|
508
|
+
const existing = bound.get(view)?.get(property)
|
|
509
|
+
if (existing) release(existing)
|
|
510
|
+
const root = source.root
|
|
511
|
+
const binding: Binding = {
|
|
512
|
+
view,
|
|
513
|
+
property,
|
|
514
|
+
root,
|
|
515
|
+
read: source.read,
|
|
516
|
+
sync() {
|
|
517
|
+
if (!view.isLoaded || !view.nativeViewProtected) return
|
|
518
|
+
if (!root.running) {
|
|
519
|
+
// A value at rest writes straight through, so scroll offsets cost one write per event.
|
|
520
|
+
binding.animation?.stop()
|
|
521
|
+
binding.animation = undefined
|
|
522
|
+
stopBase(view, [property])
|
|
523
|
+
const value = source.read(root.raw())
|
|
524
|
+
setBase(
|
|
525
|
+
view,
|
|
526
|
+
property,
|
|
527
|
+
isDecorationProperty(property) ? clampDecoration(property, value) : value,
|
|
528
|
+
)
|
|
529
|
+
if (isColorProperty(property) || isDecorationProperty(property))
|
|
530
|
+
getDriver().sync?.(view)
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
const animation = new ViewAnimation(view, [root.plan(binding)])
|
|
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)
|
|
540
|
+
binding.animation = animation
|
|
541
|
+
},
|
|
542
|
+
}
|
|
543
|
+
let properties = bound.get(view)
|
|
544
|
+
if (!properties) bound.set(view, (properties = new Map()))
|
|
545
|
+
properties.set(property, binding)
|
|
546
|
+
root.bindings.add(binding)
|
|
547
|
+
view.on('loaded', binding.sync)
|
|
548
|
+
binding.sync()
|
|
549
|
+
return () => {
|
|
550
|
+
if (root.bindings.has(binding)) {
|
|
551
|
+
release(binding)
|
|
552
|
+
binding.animation?.stop()
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
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
|
+
|
|
669
|
+
function release(binding: Binding) {
|
|
670
|
+
binding.root.bindings.delete(binding)
|
|
671
|
+
binding.view.off('loaded', binding.sync)
|
|
672
|
+
const properties = bound.get(binding.view)
|
|
673
|
+
if (properties?.get(binding.property) === binding) properties.delete(binding.property)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** A regular animation on a bound property takes it over; the value keeps its other bindings. */
|
|
677
|
+
export function detachBindings(view: View, properties: readonly Property[]) {
|
|
678
|
+
const map = bound.get(view)
|
|
679
|
+
if (!map) return
|
|
680
|
+
for (const property of properties) {
|
|
681
|
+
const binding = map.get(property)
|
|
682
|
+
if (binding) release(binding)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function mapping(
|
|
687
|
+
input: readonly number[],
|
|
688
|
+
output: readonly (number | ColorValue)[],
|
|
689
|
+
options: MapOptions = {},
|
|
690
|
+
) {
|
|
691
|
+
if (!Array.isArray(input) || !Array.isArray(output) || input.length < 2)
|
|
692
|
+
throw new RangeError('to() needs at least two input and output values.')
|
|
693
|
+
if (input.length !== output.length)
|
|
694
|
+
throw new RangeError(
|
|
695
|
+
`to() has ${input.length} inputs but ${output.length} outputs; give one output per input.`,
|
|
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]!
|
|
700
|
+
if (
|
|
701
|
+
input.some(
|
|
702
|
+
(value, i) => i > 0 && (descending ? value > input[i - 1]! : value < input[i - 1]!),
|
|
703
|
+
)
|
|
704
|
+
)
|
|
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
|
+
}
|
|
713
|
+
const color = typeof output[0] !== 'number'
|
|
714
|
+
const numbers = color ? output.map((value) => parseColor(value)) : (output as number[])
|
|
715
|
+
if (
|
|
716
|
+
!color &&
|
|
717
|
+
numbers.some((value) => typeof value !== 'number' || !Number.isFinite(value))
|
|
718
|
+
)
|
|
719
|
+
throw new RangeError('to() outputs must all be finite numbers, or all colors.')
|
|
720
|
+
const mixers = color
|
|
721
|
+
? numbers.slice(1).map((value, i) => colorMixer(numbers[i]!, value))
|
|
722
|
+
: undefined
|
|
723
|
+
const clamp = color || options.clamp !== false
|
|
724
|
+
const last = input.length - 1
|
|
725
|
+
const read = (value: number) => {
|
|
726
|
+
if (clamp && value <= input[0]!) return numbers[0]!
|
|
727
|
+
if (clamp && value >= input[last]!) return numbers[last]!
|
|
728
|
+
let i = 0
|
|
729
|
+
while (i < last - 1 && value > input[i + 1]!) i++
|
|
730
|
+
const span = input[i + 1]! - input[i]!
|
|
731
|
+
const t = span > 0 ? (value - input[i]!) / span : value < input[i]! ? 0 : 1
|
|
732
|
+
if (mixers) return mixers[i]!(Math.min(1, Math.max(0, t)))
|
|
733
|
+
return numbers[i]! + (numbers[i + 1]! - numbers[i]!) * t
|
|
734
|
+
}
|
|
735
|
+
return { read, color }
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function springOf(name: SpringName): Transition {
|
|
739
|
+
if (!Object.hasOwn(springs, name)) throw new Error(`Unknown spring "${name}".`)
|
|
740
|
+
return springs[name]
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** A number that follows a scroll view's offset. Bind it like a motion value. */
|
|
744
|
+
export interface ScrollValue extends DerivedValue {
|
|
745
|
+
/** Follows this scroll view instead, or none with `null`. Stable, so it works as a template ref. */
|
|
746
|
+
readonly ref: (scrollView: Target | null) => void
|
|
747
|
+
/** Stops following; bound views stay where they are. */
|
|
748
|
+
dispose(): void
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* The scroll offset of a ScrollView, in DIPs, as a value that bindings follow.
|
|
753
|
+
*
|
|
754
|
+
* ```ts
|
|
755
|
+
* const offset = scrollValue(scroll)
|
|
756
|
+
* bind(header, { y: offset.to([0, 120], [0, -60]), opacity: offset.to([0, 80], [1, 0]) })
|
|
757
|
+
* ```
|
|
758
|
+
*/
|
|
759
|
+
export function scrollValue(
|
|
760
|
+
scrollView?: Target | null,
|
|
761
|
+
options: { axis?: 'x' | 'y' } = {},
|
|
762
|
+
): ScrollValue {
|
|
763
|
+
const axis = options.axis ?? 'y'
|
|
764
|
+
if (axis !== 'x' && axis !== 'y') throw new TypeError('Scroll axis must be "x" or "y".')
|
|
765
|
+
const root = new Root(0)
|
|
766
|
+
let source: ScrollView | undefined
|
|
767
|
+
const read = () => {
|
|
768
|
+
if (!source) return
|
|
769
|
+
const offset = axis === 'x' ? source.horizontalOffset : source.verticalOffset
|
|
770
|
+
if (Number.isFinite(offset) && offset !== root.raw()) root.set(offset)
|
|
771
|
+
}
|
|
772
|
+
const onScroll = (event: EventData) => {
|
|
773
|
+
const { scrollX, scrollY } = event as ScrollEventData
|
|
774
|
+
const offset = axis === 'x' ? scrollX : scrollY
|
|
775
|
+
if (Number.isFinite(offset) && offset !== root.raw()) root.set(offset)
|
|
776
|
+
}
|
|
777
|
+
const detach = () => {
|
|
778
|
+
source?.off(ScrollView.scrollEvent, onScroll)
|
|
779
|
+
source?.off('loaded', read)
|
|
780
|
+
source = undefined
|
|
781
|
+
}
|
|
782
|
+
const ref = (target: Target | null) => {
|
|
783
|
+
const next = target === null ? undefined : toViews(target)[0]
|
|
784
|
+
if (next === source) return
|
|
785
|
+
if (next !== undefined && !(next instanceof ScrollView))
|
|
786
|
+
throw new TypeError('scrollValue() follows a NativeScript ScrollView.')
|
|
787
|
+
detach()
|
|
788
|
+
source = next
|
|
789
|
+
source?.on(ScrollView.scrollEvent, onScroll)
|
|
790
|
+
source?.on('loaded', read)
|
|
791
|
+
read()
|
|
792
|
+
}
|
|
793
|
+
if (scrollView) ref(scrollView)
|
|
794
|
+
return Object.assign(root as unknown as DerivedValue, { ref, dispose: detach })
|
|
795
|
+
}
|