@rafaelaugusto/jolt 0.1.0-beta.10 → 0.1.0-beta.12
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 +55 -6
- package/dist/animate.d.ts +14 -4
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +50 -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-gradient.d.ts +5 -0
- package/dist/driver/android-gradient.d.ts.map +1 -0
- package/dist/driver/android-gradient.js +89 -0
- package/dist/driver/android-gradient.js.map +1 -0
- package/dist/driver/ios-gradient.d.ts +5 -0
- package/dist/driver/ios-gradient.d.ts.map +1 -0
- package/dist/driver/ios-gradient.js +109 -0
- package/dist/driver/ios-gradient.js.map +1 -0
- 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 +18 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +143 -36
- package/dist/engine.js.map +1 -1
- package/dist/gradient.d.ts +90 -0
- package/dist/gradient.d.ts.map +1 -0
- package/dist/gradient.js +253 -0
- package/dist/gradient.js.map +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- 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 +4 -1
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +8 -3
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-states.d.ts +18 -6
- package/dist/motion-states.d.ts.map +1 -1
- package/dist/motion-states.js +114 -26
- package/dist/motion-states.js.map +1 -1
- package/dist/motion-value.d.ts +115 -0
- package/dist/motion-value.d.ts.map +1 -0
- package/dist/motion-value.js +437 -0
- package/dist/motion-value.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/track.d.ts.map +1 -1
- package/dist/track.js +31 -12
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +30 -4
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +63 -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/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 +1 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +195 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrame.java +53 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrameFollower.java +57 -0
- package/platforms/android/proguard-rules.pro +2 -0
- package/src/android-plugin.d.ts +56 -0
- package/src/animate.ts +99 -16
- package/src/drag.ts +67 -21
- package/src/driver/android-gradient.ts +109 -0
- package/src/driver/ios-gradient.ts +114 -0
- package/src/easing.ts +14 -0
- package/src/engine.ts +158 -53
- package/src/gradient.ts +378 -0
- package/src/index.ts +34 -3
- package/src/keyboard.ts +83 -18
- package/src/mixer.ts +264 -0
- package/src/motion-scope.ts +14 -6
- package/src/motion-states.ts +145 -41
- package/src/motion-value.ts +563 -0
- package/src/path.ts +2 -1
- package/src/track.ts +37 -12
- package/src/transition.ts +99 -10
- package/src/types.ts +5 -0
- package/src/vue/index.ts +98 -1
|
@@ -0,0 +1,563 @@
|
|
|
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 {
|
|
24
|
+
setArrival,
|
|
25
|
+
setBase,
|
|
26
|
+
stopBase,
|
|
27
|
+
ViewAnimation,
|
|
28
|
+
type Animation,
|
|
29
|
+
type Plan,
|
|
30
|
+
} from './engine.js'
|
|
31
|
+
import { linear } from './easing.js'
|
|
32
|
+
import { isProperty, type Property } from './properties.js'
|
|
33
|
+
import { createTrack, instantTrack, type Track } from './track.js'
|
|
34
|
+
import {
|
|
35
|
+
resolveTransition,
|
|
36
|
+
springs,
|
|
37
|
+
type ResolvedTransition,
|
|
38
|
+
type SpringName,
|
|
39
|
+
type Transition,
|
|
40
|
+
} from './transition.js'
|
|
41
|
+
import type { KeyframeProperty, Target } from './types.js'
|
|
42
|
+
|
|
43
|
+
export interface MapOptions {
|
|
44
|
+
/** Hold the first and last outputs outside the input range. Defaults to `true`. */
|
|
45
|
+
clamp?: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
declare const motionValueBrand: unique symbol
|
|
49
|
+
|
|
50
|
+
/** A number derived from a motion value, ready to bind to a view. */
|
|
51
|
+
export interface DerivedValue {
|
|
52
|
+
/** The value now, while it animates too. */
|
|
53
|
+
get(): number
|
|
54
|
+
/** Maps through input and output ranges, like React Native's `interpolate`. */
|
|
55
|
+
to(
|
|
56
|
+
input: readonly number[],
|
|
57
|
+
output: readonly number[],
|
|
58
|
+
options?: MapOptions,
|
|
59
|
+
): DerivedValue
|
|
60
|
+
to(input: readonly number[], output: readonly ColorValue[]): DerivedColor
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A color derived from a motion value, for `backgroundColor`, `borderColor` or `color`. */
|
|
64
|
+
export interface DerivedColor {
|
|
65
|
+
get(): Color
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A number that animates natively and drives every view property bound to it. */
|
|
69
|
+
export interface MotionValue extends DerivedValue {
|
|
70
|
+
readonly [motionValueBrand]: true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type ColorKey = 'backgroundColor' | 'borderColor' | 'color'
|
|
74
|
+
export type Bindings = {
|
|
75
|
+
[K in Exclude<KeyframeProperty, ColorKey>]?: DerivedValue
|
|
76
|
+
} & { [K in ColorKey]?: DerivedColor }
|
|
77
|
+
|
|
78
|
+
interface Binding {
|
|
79
|
+
readonly view: View
|
|
80
|
+
readonly property: Property
|
|
81
|
+
readonly root: Root
|
|
82
|
+
readonly read: (value: number) => number
|
|
83
|
+
animation?: ViewAnimation
|
|
84
|
+
sync(): void
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface Run {
|
|
88
|
+
readonly track: Track
|
|
89
|
+
readonly base: number
|
|
90
|
+
readonly span: number
|
|
91
|
+
readonly animation: Animation
|
|
92
|
+
readonly startedAt: number
|
|
93
|
+
readonly settle: (finished: boolean) => void
|
|
94
|
+
readonly cancel: () => void
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const bound = new WeakMap<View, Map<Property, Binding>>()
|
|
98
|
+
|
|
99
|
+
class Derived {
|
|
100
|
+
constructor(
|
|
101
|
+
private readonly source: Root | undefined,
|
|
102
|
+
readonly read: (value: number) => number,
|
|
103
|
+
readonly color: boolean,
|
|
104
|
+
) {}
|
|
105
|
+
|
|
106
|
+
get root(): Root {
|
|
107
|
+
return this.source ?? (this as unknown as Root)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get(): number | Color {
|
|
111
|
+
const value = this.read(this.root.raw())
|
|
112
|
+
return this.color ? new Color(value) : value
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
to(
|
|
116
|
+
input: readonly number[],
|
|
117
|
+
output: readonly (number | ColorValue)[],
|
|
118
|
+
options?: MapOptions,
|
|
119
|
+
): Derived {
|
|
120
|
+
if (this.color) throw new TypeError('A color cannot be mapped again.')
|
|
121
|
+
const map = mapping(input, output, options)
|
|
122
|
+
const read = this.read
|
|
123
|
+
return new Derived(this.root, (value) => map.read(read(value)), map.color)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class Root extends Derived {
|
|
128
|
+
readonly bindings = new Set<Binding>()
|
|
129
|
+
private current: number
|
|
130
|
+
private run: Run | undefined
|
|
131
|
+
|
|
132
|
+
constructor(initial: number) {
|
|
133
|
+
super(undefined, (value) => value, false)
|
|
134
|
+
if (!Number.isFinite(initial))
|
|
135
|
+
throw new RangeError(`A motion value starts from a finite number, got ${initial}.`)
|
|
136
|
+
this.current = initial
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
raw(): number {
|
|
140
|
+
const run = this.run
|
|
141
|
+
if (!run) return this.current
|
|
142
|
+
return run.base + run.span * run.track.valueAt(getDriver().now() - run.startedAt)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private velocity(): number {
|
|
146
|
+
const run = this.run
|
|
147
|
+
if (!run) return 0
|
|
148
|
+
return run.span * run.track.velocityAt(getDriver().now() - run.startedAt)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
animate(
|
|
152
|
+
target: number | readonly (number | null)[],
|
|
153
|
+
transition: Transition | SpringName = {},
|
|
154
|
+
): Animation {
|
|
155
|
+
const from = this.raw()
|
|
156
|
+
const frames = (typeof target === 'number' ? [null, target] : [...target]).map(
|
|
157
|
+
(frame) => (frame === null ? from : frame),
|
|
158
|
+
)
|
|
159
|
+
if (frames.length < 2) frames.unshift(from)
|
|
160
|
+
if (frames.some((frame) => typeof frame !== 'number' || !Number.isFinite(frame)))
|
|
161
|
+
throw new RangeError(`Invalid motion value keyframes: ${JSON.stringify(target)}.`)
|
|
162
|
+
const options = typeof transition === 'string' ? springOf(transition) : transition
|
|
163
|
+
const resolved = resolveTransition(options, 'x', frames.length)
|
|
164
|
+
const base = Math.min(...frames)
|
|
165
|
+
const span = Math.max(...frames) - base
|
|
166
|
+
const velocity = this.velocity()
|
|
167
|
+
this.halt()
|
|
168
|
+
if (span === 0) return this.jump(frames.at(-1)!)
|
|
169
|
+
|
|
170
|
+
// Sampled in the value's own range so springs settle relative to the distance.
|
|
171
|
+
const track = createTrack(
|
|
172
|
+
'opacity',
|
|
173
|
+
frames.map((frame) => (frame - base) / span),
|
|
174
|
+
resolved,
|
|
175
|
+
velocity / span,
|
|
176
|
+
)
|
|
177
|
+
return this.start(track, base, span)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Plays a track sampled in the value's own units, such as a drag release. */
|
|
181
|
+
play(track: Track): Animation {
|
|
182
|
+
this.halt()
|
|
183
|
+
return track.end === 0 ? this.jump(track.to) : this.start(track, 0, 1)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private jump(value: number): Animation {
|
|
187
|
+
const { animation, settle } = this.handle()
|
|
188
|
+
this.current = value
|
|
189
|
+
for (const binding of this.bindings) binding.sync()
|
|
190
|
+
settle(true)
|
|
191
|
+
setArrival(animation, Promise.resolve())
|
|
192
|
+
return animation
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private handle() {
|
|
196
|
+
let settle!: (finished: boolean) => void
|
|
197
|
+
const finished = new Promise<boolean>((resolve) => (settle = resolve))
|
|
198
|
+
const animation: Animation = {
|
|
199
|
+
finished,
|
|
200
|
+
stop: () => {
|
|
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
|
|
212
|
+
const driver = getDriver()
|
|
213
|
+
setArrival(animation, new Promise<void>((resolve) => (arrive = resolve)))
|
|
214
|
+
const timers = [
|
|
215
|
+
Number.isFinite(track.end)
|
|
216
|
+
? driver.after(track.end, () => {
|
|
217
|
+
if (this.run !== run) return
|
|
218
|
+
this.current = base + span * track.to
|
|
219
|
+
this.run = undefined
|
|
220
|
+
arrive()
|
|
221
|
+
settle(true)
|
|
222
|
+
})
|
|
223
|
+
: undefined,
|
|
224
|
+
Number.isFinite(track.arrival)
|
|
225
|
+
? driver.after(track.arrival, () => arrive())
|
|
226
|
+
: undefined,
|
|
227
|
+
]
|
|
228
|
+
this.run = run = {
|
|
229
|
+
track,
|
|
230
|
+
base,
|
|
231
|
+
span,
|
|
232
|
+
animation,
|
|
233
|
+
startedAt: driver.now(),
|
|
234
|
+
settle: (completed) => {
|
|
235
|
+
arrive()
|
|
236
|
+
settle(completed)
|
|
237
|
+
},
|
|
238
|
+
cancel: () => timers.forEach((cancel) => cancel?.()),
|
|
239
|
+
}
|
|
240
|
+
for (const binding of this.bindings) binding.sync()
|
|
241
|
+
return animation
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
set(value: number) {
|
|
245
|
+
if (!Number.isFinite(value))
|
|
246
|
+
throw new RangeError(`A motion value takes a finite number, got ${value}.`)
|
|
247
|
+
this.halt()
|
|
248
|
+
this.current = value
|
|
249
|
+
for (const binding of this.bindings) binding.sync()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
stop() {
|
|
253
|
+
if (!this.run) return
|
|
254
|
+
const value = this.raw()
|
|
255
|
+
this.halt()
|
|
256
|
+
this.current = value
|
|
257
|
+
for (const binding of this.bindings) binding.sync()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The plan that plays this value's run, mapped, on one bound property. */
|
|
261
|
+
plan(binding: Binding): Plan {
|
|
262
|
+
const run = this.run
|
|
263
|
+
const { property, read } = binding
|
|
264
|
+
const write = (value: number) =>
|
|
265
|
+
isDecorationProperty(property)
|
|
266
|
+
? clampDecoration(property, read(value))
|
|
267
|
+
: read(value)
|
|
268
|
+
if (!run) {
|
|
269
|
+
const value = write(this.current)
|
|
270
|
+
return {
|
|
271
|
+
property,
|
|
272
|
+
keyframes: [],
|
|
273
|
+
transition: still,
|
|
274
|
+
track: () => instantTrack(property, value),
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const { track, base, span } = run
|
|
278
|
+
const at = (progress: number) => write(base + span * progress)
|
|
279
|
+
const transition: ResolvedTransition = {
|
|
280
|
+
curve: {
|
|
281
|
+
type: 'tween',
|
|
282
|
+
duration: track.duration,
|
|
283
|
+
ease: [linear],
|
|
284
|
+
times: undefined,
|
|
285
|
+
},
|
|
286
|
+
delay: track.delay,
|
|
287
|
+
repeat: track.repeat,
|
|
288
|
+
reverse: track.reverse,
|
|
289
|
+
}
|
|
290
|
+
const mapped = (): Track => {
|
|
291
|
+
if (reducedMotion() && property !== 'opacity' && !isColorProperty(property))
|
|
292
|
+
return instantTrack(property, at(track.to))
|
|
293
|
+
return {
|
|
294
|
+
property,
|
|
295
|
+
delay: track.delay,
|
|
296
|
+
duration: track.duration,
|
|
297
|
+
repeat: track.repeat,
|
|
298
|
+
reverse: track.reverse,
|
|
299
|
+
times: track.times,
|
|
300
|
+
values: track.values.map(at),
|
|
301
|
+
to: at(track.to),
|
|
302
|
+
end: track.end,
|
|
303
|
+
arrival: track.arrival,
|
|
304
|
+
valueAt: (elapsed) => at(track.valueAt(elapsed)),
|
|
305
|
+
velocityAt: isColorProperty(property)
|
|
306
|
+
? () => 0
|
|
307
|
+
: (elapsed) =>
|
|
308
|
+
(at(track.valueAt(elapsed + 0.5)) - at(track.valueAt(elapsed - 0.5))) *
|
|
309
|
+
1000,
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return { property, keyframes: [], transition, track: mapped }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
elapsed(): number {
|
|
316
|
+
return this.run ? getDriver().now() - this.run.startedAt : 0
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
get running(): boolean {
|
|
320
|
+
return this.run !== undefined
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private halt() {
|
|
324
|
+
const run = this.run
|
|
325
|
+
if (!run) return
|
|
326
|
+
this.run = undefined
|
|
327
|
+
run.cancel()
|
|
328
|
+
run.settle(false)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const still: ResolvedTransition = {
|
|
333
|
+
curve: { type: 'tween', duration: 0, ease: [linear], times: undefined },
|
|
334
|
+
delay: 0,
|
|
335
|
+
repeat: 0,
|
|
336
|
+
reverse: false,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* A number that animates natively and drives the view properties bound to it.
|
|
341
|
+
*
|
|
342
|
+
* ```ts
|
|
343
|
+
* const progress = motionValue(0)
|
|
344
|
+
* bind(sheet, { y: progress.to([0, 1], [600, 0]) })
|
|
345
|
+
* animate(progress, 1, 'snappy')
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
export function motionValue(initial = 0): MotionValue {
|
|
349
|
+
return new Root(initial) as unknown as MotionValue
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function isMotionValue(target: unknown): target is MotionValue {
|
|
353
|
+
return target instanceof Root
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function asRoot(value: MotionValue): Root {
|
|
357
|
+
return value as unknown as Root
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Lets motion values drive a view's properties. The view takes their current values
|
|
362
|
+
* at once and follows every change on the same native clock. Returns a cleanup function.
|
|
363
|
+
*
|
|
364
|
+
* ```ts
|
|
365
|
+
* const release = bind(backdrop, { opacity: progress.to([0, 1], [0, 0.4]) })
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
export function bind(target: Target | readonly Target[], bindings: Bindings): () => void {
|
|
369
|
+
if (!bindings || typeof bindings !== 'object' || Array.isArray(bindings))
|
|
370
|
+
throw new TypeError('bind() takes an object of motion values.')
|
|
371
|
+
const views = toViews(target)
|
|
372
|
+
const entries: [Property, Derived][] = []
|
|
373
|
+
for (const [key, source] of Object.entries(bindings)) {
|
|
374
|
+
if (source === undefined) continue
|
|
375
|
+
if (!(source instanceof Derived))
|
|
376
|
+
throw new TypeError(`"${key}" needs a motion value, got ${String(source)}.`)
|
|
377
|
+
const properties: Property[] =
|
|
378
|
+
key === 'scale' ? ['scaleX', 'scaleY'] : isProperty(key) ? [key] : []
|
|
379
|
+
if (!properties.length) throw new Error(`Can't bind "${key}".`)
|
|
380
|
+
for (const property of properties) {
|
|
381
|
+
if (isColorProperty(property) !== source.color)
|
|
382
|
+
throw new TypeError(
|
|
383
|
+
source.color
|
|
384
|
+
? `"${key}" takes numbers; map the value to numbers.`
|
|
385
|
+
: `"${key}" is a color; map the value to colors with to(input, colors).`,
|
|
386
|
+
)
|
|
387
|
+
entries.push([property, source])
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (const view of views)
|
|
391
|
+
for (const [property] of entries)
|
|
392
|
+
if (isColorProperty(property)) validateColorTarget(view, property)
|
|
393
|
+
else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
|
|
394
|
+
|
|
395
|
+
const made = views.flatMap((view) =>
|
|
396
|
+
entries.map(([property, source]) => attach(view, property, source)),
|
|
397
|
+
)
|
|
398
|
+
return () => made.forEach((release) => release())
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function attach(view: View, property: Property, source: Derived) {
|
|
402
|
+
const existing = bound.get(view)?.get(property)
|
|
403
|
+
if (existing) release(existing)
|
|
404
|
+
const root = source.root
|
|
405
|
+
const binding: Binding = {
|
|
406
|
+
view,
|
|
407
|
+
property,
|
|
408
|
+
root,
|
|
409
|
+
read: source.read,
|
|
410
|
+
sync() {
|
|
411
|
+
if (!view.isLoaded || !view.nativeViewProtected) return
|
|
412
|
+
if (!root.running) {
|
|
413
|
+
// A value at rest writes straight through, so scroll offsets cost one write per event.
|
|
414
|
+
binding.animation?.stop()
|
|
415
|
+
binding.animation = undefined
|
|
416
|
+
stopBase(view, [property])
|
|
417
|
+
const value = source.read(root.raw())
|
|
418
|
+
setBase(
|
|
419
|
+
view,
|
|
420
|
+
property,
|
|
421
|
+
isDecorationProperty(property) ? clampDecoration(property, value) : value,
|
|
422
|
+
)
|
|
423
|
+
if (isColorProperty(property) || isDecorationProperty(property))
|
|
424
|
+
getDriver().sync?.(view)
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
const elapsed = root.elapsed()
|
|
428
|
+
const animation = new ViewAnimation(view, [root.plan(binding)])
|
|
429
|
+
if (elapsed > 1 && animation.duration > 0) animation.seekTime(elapsed)
|
|
430
|
+
binding.animation = animation
|
|
431
|
+
},
|
|
432
|
+
}
|
|
433
|
+
let properties = bound.get(view)
|
|
434
|
+
if (!properties) bound.set(view, (properties = new Map()))
|
|
435
|
+
properties.set(property, binding)
|
|
436
|
+
root.bindings.add(binding)
|
|
437
|
+
view.on('loaded', binding.sync)
|
|
438
|
+
binding.sync()
|
|
439
|
+
return () => {
|
|
440
|
+
if (root.bindings.has(binding)) {
|
|
441
|
+
release(binding)
|
|
442
|
+
binding.animation?.stop()
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function release(binding: Binding) {
|
|
448
|
+
binding.root.bindings.delete(binding)
|
|
449
|
+
binding.view.off('loaded', binding.sync)
|
|
450
|
+
const properties = bound.get(binding.view)
|
|
451
|
+
if (properties?.get(binding.property) === binding) properties.delete(binding.property)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** A regular animation on a bound property takes it over; the value keeps its other bindings. */
|
|
455
|
+
export function detachBindings(view: View, properties: readonly Property[]) {
|
|
456
|
+
const map = bound.get(view)
|
|
457
|
+
if (!map) return
|
|
458
|
+
for (const property of properties) {
|
|
459
|
+
const binding = map.get(property)
|
|
460
|
+
if (binding) release(binding)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function mapping(
|
|
465
|
+
input: readonly number[],
|
|
466
|
+
output: readonly (number | ColorValue)[],
|
|
467
|
+
options: MapOptions = {},
|
|
468
|
+
) {
|
|
469
|
+
if (!Array.isArray(input) || !Array.isArray(output) || input.length < 2)
|
|
470
|
+
throw new RangeError('to() needs at least two input and output values.')
|
|
471
|
+
if (input.length !== output.length)
|
|
472
|
+
throw new RangeError(
|
|
473
|
+
`to() has ${input.length} inputs but ${output.length} outputs; give one output per input.`,
|
|
474
|
+
)
|
|
475
|
+
if (
|
|
476
|
+
input.some(
|
|
477
|
+
(value, i) => !Number.isFinite(value) || value < (input[i - 1] ?? -Infinity),
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
throw new RangeError('to() inputs must be finite and never decrease.')
|
|
481
|
+
const color = typeof output[0] !== 'number'
|
|
482
|
+
const numbers = color ? output.map((value) => parseColor(value)) : (output as number[])
|
|
483
|
+
if (
|
|
484
|
+
!color &&
|
|
485
|
+
numbers.some((value) => typeof value !== 'number' || !Number.isFinite(value))
|
|
486
|
+
)
|
|
487
|
+
throw new RangeError('to() outputs must all be finite numbers, or all colors.')
|
|
488
|
+
const mixers = color
|
|
489
|
+
? numbers.slice(1).map((value, i) => colorMixer(numbers[i]!, value))
|
|
490
|
+
: undefined
|
|
491
|
+
const clamp = color || options.clamp !== false
|
|
492
|
+
const last = input.length - 1
|
|
493
|
+
const read = (value: number) => {
|
|
494
|
+
if (clamp && value <= input[0]!) return numbers[0]!
|
|
495
|
+
if (clamp && value >= input[last]!) return numbers[last]!
|
|
496
|
+
let i = 0
|
|
497
|
+
while (i < last - 1 && value > input[i + 1]!) i++
|
|
498
|
+
const span = input[i + 1]! - input[i]!
|
|
499
|
+
const t = span > 0 ? (value - input[i]!) / span : value < input[i]! ? 0 : 1
|
|
500
|
+
if (mixers) return mixers[i]!(Math.min(1, Math.max(0, t)))
|
|
501
|
+
return numbers[i]! + (numbers[i + 1]! - numbers[i]!) * t
|
|
502
|
+
}
|
|
503
|
+
return { read, color }
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function springOf(name: SpringName): Transition {
|
|
507
|
+
if (!Object.hasOwn(springs, name)) throw new Error(`Unknown spring "${name}".`)
|
|
508
|
+
return springs[name]
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** A number that follows a scroll view's offset. Bind it like a motion value. */
|
|
512
|
+
export interface ScrollValue extends DerivedValue {
|
|
513
|
+
/** Follows this scroll view instead, or none with `null`. Stable, so it works as a template ref. */
|
|
514
|
+
readonly ref: (scrollView: Target | null) => void
|
|
515
|
+
/** Stops following; bound views stay where they are. */
|
|
516
|
+
dispose(): void
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* The scroll offset of a ScrollView, in DIPs, as a value that bindings follow.
|
|
521
|
+
*
|
|
522
|
+
* ```ts
|
|
523
|
+
* const offset = scrollValue(scroll)
|
|
524
|
+
* bind(header, { y: offset.to([0, 120], [0, -60]), opacity: offset.to([0, 80], [1, 0]) })
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
export function scrollValue(
|
|
528
|
+
scrollView?: Target | null,
|
|
529
|
+
options: { axis?: 'x' | 'y' } = {},
|
|
530
|
+
): ScrollValue {
|
|
531
|
+
const axis = options.axis ?? 'y'
|
|
532
|
+
if (axis !== 'x' && axis !== 'y') throw new TypeError('Scroll axis must be "x" or "y".')
|
|
533
|
+
const root = new Root(0)
|
|
534
|
+
let source: ScrollView | undefined
|
|
535
|
+
const read = () => {
|
|
536
|
+
if (!source) return
|
|
537
|
+
const offset = axis === 'x' ? source.horizontalOffset : source.verticalOffset
|
|
538
|
+
if (Number.isFinite(offset) && offset !== root.raw()) root.set(offset)
|
|
539
|
+
}
|
|
540
|
+
const onScroll = (event: EventData) => {
|
|
541
|
+
const { scrollX, scrollY } = event as ScrollEventData
|
|
542
|
+
const offset = axis === 'x' ? scrollX : scrollY
|
|
543
|
+
if (Number.isFinite(offset) && offset !== root.raw()) root.set(offset)
|
|
544
|
+
}
|
|
545
|
+
const detach = () => {
|
|
546
|
+
source?.off(ScrollView.scrollEvent, onScroll)
|
|
547
|
+
source?.off('loaded', read)
|
|
548
|
+
source = undefined
|
|
549
|
+
}
|
|
550
|
+
const ref = (target: Target | null) => {
|
|
551
|
+
const next = target === null ? undefined : toViews(target)[0]
|
|
552
|
+
if (next === source) return
|
|
553
|
+
if (next !== undefined && !(next instanceof ScrollView))
|
|
554
|
+
throw new TypeError('scrollValue() follows a NativeScript ScrollView.')
|
|
555
|
+
detach()
|
|
556
|
+
source = next
|
|
557
|
+
source?.on(ScrollView.scrollEvent, onScroll)
|
|
558
|
+
source?.on('loaded', read)
|
|
559
|
+
read()
|
|
560
|
+
}
|
|
561
|
+
if (scrollView) ref(scrollView)
|
|
562
|
+
return Object.assign(root as unknown as DerivedValue, { ref, dispose: detach })
|
|
563
|
+
}
|
package/src/path.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { parsePath, type MotionPath } from './svg-path.js'
|
|
|
2
2
|
import { reducedMotion, timeScale } from './config.js'
|
|
3
3
|
import { createTrack, instantTrack, type Track } from './track.js'
|
|
4
4
|
import {
|
|
5
|
+
fitTransition,
|
|
5
6
|
mergeTransitions,
|
|
6
7
|
resolveTransition,
|
|
7
8
|
type Transition,
|
|
@@ -37,7 +38,7 @@ export function preparePath(
|
|
|
37
38
|
const geometry = parsePath(keyframes.path)
|
|
38
39
|
const orient = keyframes.orient === true
|
|
39
40
|
const transition = resolveTransition(
|
|
40
|
-
overrides.path ? mergeTransitions(base, overrides.path) : base,
|
|
41
|
+
fitTransition(overrides.path ? mergeTransitions(base, overrides.path) : base, 2),
|
|
41
42
|
'opacity',
|
|
42
43
|
2,
|
|
43
44
|
)
|
package/src/track.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { solveDecay } from './decay.js'
|
|
|
4
4
|
import { solveDragRelease } from './drag-physics.js'
|
|
5
5
|
import { precision, type Property } from './properties.js'
|
|
6
6
|
import { solveSpring, springPhysics } from './spring.js'
|
|
7
|
-
import type { Curve, ResolvedTransition } from './transition.js'
|
|
7
|
+
import type { Curve, RepeatDelayRange, ResolvedTransition } from './transition.js'
|
|
8
8
|
import { colorMixer, isColorProperty, type ColorProperty } from './color.js'
|
|
9
9
|
import { reducedMotion, timeScale } from './config.js'
|
|
10
10
|
|
|
@@ -74,7 +74,9 @@ export function createTrack(
|
|
|
74
74
|
played,
|
|
75
75
|
transition.repeat,
|
|
76
76
|
transition.reverse,
|
|
77
|
-
|
|
77
|
+
transition.repeatDelay ?? 0,
|
|
78
|
+
slowdown,
|
|
79
|
+
transition.seed,
|
|
78
80
|
)
|
|
79
81
|
const { duration } = motion
|
|
80
82
|
const first = motion.values[0]!
|
|
@@ -222,32 +224,44 @@ function colorTrack(
|
|
|
222
224
|
}
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
/** An endless loop with drawn pauses plays this many of them, then starts over. */
|
|
228
|
+
const DRAWN_CYCLES = 16
|
|
229
|
+
|
|
225
230
|
/**
|
|
226
|
-
* Holds the end value for `
|
|
231
|
+
* Holds the end value for `delay` ms before each repeat, inside the keyframes, so both
|
|
227
232
|
* platforms play it natively. Finite repeats are laid out one after another, with no
|
|
228
|
-
* wait after the last; endless ones loop a cycle that waits at each turn
|
|
233
|
+
* wait after the last; endless ones loop a cycle that waits at each turn,
|
|
234
|
+
* or DRAWN_CYCLES cycles when the waits are drawn from a range.
|
|
229
235
|
*/
|
|
230
236
|
function pauseBetweenRepeats(
|
|
231
237
|
motion: Motion,
|
|
232
238
|
repeat: number,
|
|
233
239
|
reverse: boolean,
|
|
234
|
-
|
|
240
|
+
delay: number | RepeatDelayRange,
|
|
241
|
+
slowdown: number,
|
|
242
|
+
seed = Math.random(),
|
|
235
243
|
): { motion: Motion; repeat: number; reverse: boolean } {
|
|
236
|
-
|
|
244
|
+
const [shortest, longest] = typeof delay === 'number' ? [delay, delay] : delay
|
|
245
|
+
if (longest <= 0 || repeat <= 0 || motion.duration === 0)
|
|
237
246
|
return { motion, repeat, reverse }
|
|
247
|
+
const draw = random(seed)
|
|
248
|
+
const pause = () => (shortest + draw() * (longest - shortest)) * slowdown
|
|
238
249
|
type Segment = { kind: 'forward' | 'backward' | 'hold'; duration: number }
|
|
239
250
|
const play = (backward: boolean): Segment => ({
|
|
240
251
|
kind: backward ? 'backward' : 'forward',
|
|
241
252
|
duration: motion.duration,
|
|
242
253
|
})
|
|
243
|
-
const hold: Segment
|
|
254
|
+
const hold = (): Segment => ({ kind: 'hold', duration: pause() })
|
|
244
255
|
const segments: Segment[] = []
|
|
245
256
|
if (Number.isFinite(repeat))
|
|
246
257
|
for (let i = 0; i <= repeat; i++) {
|
|
247
|
-
if (i > 0) segments.push(hold)
|
|
258
|
+
if (i > 0) segments.push(hold())
|
|
248
259
|
segments.push(play(reverse && i % 2 === 1))
|
|
249
260
|
}
|
|
250
|
-
else
|
|
261
|
+
else {
|
|
262
|
+
const cycles = shortest === longest ? (reverse ? 2 : 1) : DRAWN_CYCLES
|
|
263
|
+
for (let i = 0; i < cycles; i++) segments.push(play(reverse && i % 2 === 1), hold())
|
|
264
|
+
}
|
|
251
265
|
|
|
252
266
|
const total = segments.reduce((sum, segment) => sum + segment.duration, 0)
|
|
253
267
|
const starts = segments.map((_, i) =>
|
|
@@ -260,8 +274,6 @@ function pauseBetweenRepeats(
|
|
|
260
274
|
if (segment.kind === 'backward') return motion.values[0]!
|
|
261
275
|
return endOf(i - 1)
|
|
262
276
|
}
|
|
263
|
-
// A loop jumps back to its start at the end of the hold, not across it.
|
|
264
|
-
const jump = Math.min(1, pause / 10)
|
|
265
277
|
|
|
266
278
|
const times: number[] = []
|
|
267
279
|
const values: number[] = []
|
|
@@ -273,7 +285,9 @@ function pauseBetweenRepeats(
|
|
|
273
285
|
const jumps =
|
|
274
286
|
next !== undefined &&
|
|
275
287
|
(next.kind === 'forward' ? motion.values[0] : motion.values[last]) !== held
|
|
276
|
-
|
|
288
|
+
// A loop jumps back to its start at the end of the hold, not across it.
|
|
289
|
+
const jump = jumps ? Math.min(1, segment.duration / 10) : 0
|
|
290
|
+
times.push((start + segment.duration - jump) / total)
|
|
277
291
|
values.push(held)
|
|
278
292
|
return
|
|
279
293
|
}
|
|
@@ -319,6 +333,17 @@ function pauseBetweenRepeats(
|
|
|
319
333
|
}
|
|
320
334
|
}
|
|
321
335
|
|
|
336
|
+
/** Mulberry32: the same seed draws the same pauses for every track of one animation. */
|
|
337
|
+
function random(seed: number) {
|
|
338
|
+
let state = Math.floor(seed * 2 ** 32) >>> 0
|
|
339
|
+
return () => {
|
|
340
|
+
state = (state + 0x6d2b79f5) >>> 0
|
|
341
|
+
let t = Math.imul(state ^ (state >>> 15), state | 1)
|
|
342
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
343
|
+
return ((t ^ (t >>> 14)) >>> 0) / 2 ** 32
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
322
347
|
/** Plays the same trajectory over a longer or shorter time. */
|
|
323
348
|
function slow(motion: Motion, factor: number): Motion {
|
|
324
349
|
if (factor === 1) return motion
|