@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.
Files changed (151) hide show
  1. package/README.md +42 -8
  2. package/dist/animate.d.ts +15 -3
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +52 -34
  5. package/dist/animate.js.map +1 -1
  6. package/dist/drag.d.ts +17 -3
  7. package/dist/drag.d.ts.map +1 -1
  8. package/dist/drag.js +34 -11
  9. package/dist/drag.js.map +1 -1
  10. package/dist/driver/android.d.ts.map +1 -1
  11. package/dist/driver/android.js +2 -0
  12. package/dist/driver/android.js.map +1 -1
  13. package/dist/driver/frame.d.ts +3 -0
  14. package/dist/driver/frame.d.ts.map +1 -0
  15. package/dist/driver/frame.js +7 -0
  16. package/dist/driver/frame.js.map +1 -0
  17. package/dist/driver/index.d.ts +2 -0
  18. package/dist/driver/index.d.ts.map +1 -1
  19. package/dist/driver/index.js.map +1 -1
  20. package/dist/driver/ios.d.ts.map +1 -1
  21. package/dist/driver/ios.js +2 -0
  22. package/dist/driver/ios.js.map +1 -1
  23. package/dist/easing.d.ts +1 -1
  24. package/dist/easing.d.ts.map +1 -1
  25. package/dist/easing.js +11 -0
  26. package/dist/easing.js.map +1 -1
  27. package/dist/engine.d.ts +20 -2
  28. package/dist/engine.d.ts.map +1 -1
  29. package/dist/engine.js +166 -38
  30. package/dist/engine.js.map +1 -1
  31. package/dist/gradient.d.ts.map +1 -1
  32. package/dist/gradient.js +10 -4
  33. package/dist/gradient.js.map +1 -1
  34. package/dist/image-effect.d.ts +1 -0
  35. package/dist/image-effect.d.ts.map +1 -1
  36. package/dist/image-effect.js +10 -2
  37. package/dist/image-effect.js.map +1 -1
  38. package/dist/index.d.ts +8 -2
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/keyboard.d.ts.map +1 -1
  43. package/dist/keyboard.js +63 -10
  44. package/dist/keyboard.js.map +1 -1
  45. package/dist/mixer.d.ts +49 -0
  46. package/dist/mixer.d.ts.map +1 -0
  47. package/dist/mixer.js +238 -0
  48. package/dist/mixer.js.map +1 -0
  49. package/dist/motion-scope.d.ts +6 -2
  50. package/dist/motion-scope.d.ts.map +1 -1
  51. package/dist/motion-scope.js +12 -4
  52. package/dist/motion-scope.js.map +1 -1
  53. package/dist/motion-states.d.ts +10 -5
  54. package/dist/motion-states.d.ts.map +1 -1
  55. package/dist/motion-states.js +55 -22
  56. package/dist/motion-states.js.map +1 -1
  57. package/dist/motion-value.d.ts +152 -0
  58. package/dist/motion-value.d.ts.map +1 -0
  59. package/dist/motion-value.js +635 -0
  60. package/dist/motion-value.js.map +1 -0
  61. package/dist/native/index.d.ts +6 -0
  62. package/dist/native/index.d.ts.map +1 -0
  63. package/dist/native/index.js +3 -0
  64. package/dist/native/index.js.map +1 -0
  65. package/dist/native-properties.d.ts +28 -0
  66. package/dist/native-properties.d.ts.map +1 -0
  67. package/dist/native-properties.js +154 -0
  68. package/dist/native-properties.js.map +1 -0
  69. package/dist/native-property-driver.d.ts +19 -0
  70. package/dist/native-property-driver.d.ts.map +1 -0
  71. package/dist/native-property-driver.js +69 -0
  72. package/dist/native-property-driver.js.map +1 -0
  73. package/dist/native-property.d.ts +35 -0
  74. package/dist/native-property.d.ts.map +1 -0
  75. package/dist/native-property.js +277 -0
  76. package/dist/native-property.js.map +1 -0
  77. package/dist/path.d.ts.map +1 -1
  78. package/dist/path.js +2 -2
  79. package/dist/path.js.map +1 -1
  80. package/dist/playback-group.d.ts.map +1 -1
  81. package/dist/playback-group.js +17 -1
  82. package/dist/playback-group.js.map +1 -1
  83. package/dist/playback.d.ts +15 -0
  84. package/dist/playback.d.ts.map +1 -1
  85. package/dist/playback.js +23 -0
  86. package/dist/playback.js.map +1 -1
  87. package/dist/sequence.d.ts +24 -0
  88. package/dist/sequence.d.ts.map +1 -1
  89. package/dist/sequence.js +181 -19
  90. package/dist/sequence.js.map +1 -1
  91. package/dist/timed-animation.d.ts +38 -0
  92. package/dist/timed-animation.d.ts.map +1 -0
  93. package/dist/timed-animation.js +177 -0
  94. package/dist/timed-animation.js.map +1 -0
  95. package/dist/track.d.ts.map +1 -1
  96. package/dist/track.js +31 -12
  97. package/dist/track.js.map +1 -1
  98. package/dist/transition.d.ts +15 -4
  99. package/dist/transition.d.ts.map +1 -1
  100. package/dist/transition.js +46 -7
  101. package/dist/transition.js.map +1 -1
  102. package/dist/types.d.ts +5 -0
  103. package/dist/types.d.ts.map +1 -1
  104. package/dist/value-observers.d.ts +19 -0
  105. package/dist/value-observers.d.ts.map +1 -0
  106. package/dist/value-observers.js +111 -0
  107. package/dist/value-observers.js.map +1 -0
  108. package/dist/value-track.d.ts +13 -0
  109. package/dist/value-track.d.ts.map +1 -0
  110. package/dist/value-track.js +59 -0
  111. package/dist/value-track.js.map +1 -0
  112. package/dist/vue/index.d.ts +28 -2
  113. package/dist/vue/index.d.ts.map +1 -1
  114. package/dist/vue/index.js +78 -2
  115. package/dist/vue/index.js.map +1 -1
  116. package/package.json +6 -2
  117. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrame.java +53 -0
  118. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrameFollower.java +57 -0
  119. package/src/android-plugin.d.ts +64 -0
  120. package/src/animate.ts +106 -17
  121. package/src/drag.ts +67 -21
  122. package/src/driver/android.ts +2 -0
  123. package/src/driver/frame.ts +10 -0
  124. package/src/driver/index.ts +2 -0
  125. package/src/driver/ios.ts +2 -0
  126. package/src/easing.ts +14 -0
  127. package/src/engine.ts +181 -55
  128. package/src/gradient.ts +10 -4
  129. package/src/image-effect.ts +10 -2
  130. package/src/index.ts +23 -1
  131. package/src/ios-plugin.d.ts +33 -0
  132. package/src/keyboard.ts +83 -18
  133. package/src/mixer.ts +264 -0
  134. package/src/motion-scope.ts +17 -5
  135. package/src/motion-states.ts +79 -29
  136. package/src/motion-value.ts +795 -0
  137. package/src/native/index.ts +16 -0
  138. package/src/native-properties.ts +188 -0
  139. package/src/native-property-driver.ts +123 -0
  140. package/src/native-property.ts +353 -0
  141. package/src/path.ts +2 -1
  142. package/src/playback-group.ts +17 -1
  143. package/src/playback.ts +41 -0
  144. package/src/sequence.ts +209 -20
  145. package/src/timed-animation.ts +192 -0
  146. package/src/track.ts +37 -12
  147. package/src/transition.ts +69 -10
  148. package/src/types.ts +5 -0
  149. package/src/value-observers.ts +133 -0
  150. package/src/value-track.ts +71 -0
  151. 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
+ }