@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
@@ -136,6 +136,33 @@ declare namespace org.rafaelaugusto.jolt {
136
136
  })
137
137
  }
138
138
  }
139
+ class KeyboardFrame {
140
+ constructor(root: android.view.View, listener: KeyboardFrame.Listener)
141
+ attach(): void
142
+ detach(): void
143
+ height(): number
144
+ }
145
+ namespace KeyboardFrame {
146
+ class Listener {
147
+ constructor(implementation: { onChange(height: number): void })
148
+ }
149
+ }
150
+ class KeyboardFrameFollower {
151
+ constructor(
152
+ view: android.view.View,
153
+ offset: number,
154
+ listener: KeyboardFrameFollower.Listener,
155
+ )
156
+ attach(): void
157
+ detach(): void
158
+ }
159
+ namespace KeyboardFrameFollower {
160
+ class Listener {
161
+ constructor(implementation: {
162
+ onMove(from: number, to: number, keyboard: number): void
163
+ })
164
+ }
165
+ }
139
166
  class TextReveal {
140
167
  constructor(
141
168
  view: android.widget.TextView,
@@ -182,3 +209,40 @@ declare namespace org.rafaelaugusto.jolt {
182
209
  }
183
210
  }
184
211
  }
212
+
213
+ declare namespace org.rafaelaugusto.jolt {
214
+ class PropertySink extends java.lang.Object {
215
+ static scalar(target: any, getter: string, setter: string): PropertySink
216
+ getDimensions(): number
217
+ isAlive(): boolean
218
+ isCompiled(): boolean
219
+ read(): native.Array<number>
220
+ write(values: native.Array<number>): void
221
+ dispose(): void
222
+ }
223
+ class MapLibrePropertySink extends PropertySink {
224
+ constructor(target: any, camera: boolean)
225
+ }
226
+ class PropertyPlayer extends java.lang.Object {
227
+ constructor(
228
+ sink: PropertySink,
229
+ times: native.Array<number>,
230
+ values: native.Array<number>,
231
+ finalValue: native.Array<number>,
232
+ duration: number,
233
+ delay: number,
234
+ repeat: number,
235
+ reverse: boolean,
236
+ listener: PropertyPlayer.Listener,
237
+ )
238
+ static activeCount(): number
239
+ getFrames(): number
240
+ control(time: number, paused: boolean): void
241
+ cancel(): void
242
+ }
243
+ namespace PropertyPlayer {
244
+ class Listener extends java.lang.Object {
245
+ constructor(implementation: { onEnd(completed: boolean): void })
246
+ }
247
+ }
248
+ }
package/src/animate.ts CHANGED
@@ -17,16 +17,26 @@ import { DEFAULT_FRICTION, solveDecay } from './decay.js'
17
17
  import {
18
18
  arrivalOf,
19
19
  setArrival,
20
+ stopLayer,
20
21
  stopView,
21
22
  ViewAnimation,
22
23
  type Animation,
23
24
  type Plan,
24
25
  } from './engine.js'
25
26
  import { getPreset, type PresetName } from './presets.js'
27
+ import {
28
+ asRoot,
29
+ detachBindings,
30
+ isMotionValue,
31
+ type MotionValue,
32
+ type ValueAnimationOptions,
33
+ } from './motion-value.js'
26
34
  import { resolveHaptic, type Haptic } from './haptics.js'
35
+ import { layerable } from './mixer.js'
27
36
  import { isProperty, precision, type Property } from './properties.js'
28
37
  import {
29
38
  hasCurve,
39
+ fitTransition,
30
40
  mergeTransitions,
31
41
  resolveTransition,
32
42
  springs,
@@ -51,24 +61,45 @@ interface Options {
51
61
  stagger: number | Stagger | undefined
52
62
  relative: boolean | undefined
53
63
  haptic: Haptic | undefined
64
+ layer?: string
54
65
  }
55
66
 
56
67
  /**
57
68
  * Animates one view, or several. Takes keyframes or the name of a preset,
58
- * and a transition or the name of a spring.
69
+ * and a transition or the name of a spring. A motion value animates to a number.
59
70
  *
60
71
  * ```ts
61
72
  * animate(card, 'fadeInUp')
62
73
  * animate(card, { scale: 1.1, rotate: [0, 90] }, 'bouncy')
63
74
  * animate(rows, 'fadeInUp', { stagger: 40 })
75
+ * animate(progress, 1, 'snappy')
64
76
  * ```
65
77
  */
78
+ export function animate(
79
+ value: MotionValue,
80
+ to: number | readonly (number | null)[],
81
+ transition?: ValueAnimationOptions | SpringName,
82
+ ): PlaybackAnimation
66
83
  export function animate(
67
84
  target: Target | readonly Target[],
68
85
  animation: Keyframes | PresetName,
69
86
  options?: AnimateOptions | SpringName,
70
- ): PlaybackAnimation {
71
- return prepareAnimation(target, animation, options)()
87
+ ): PlaybackAnimation
88
+ export function animate(
89
+ target: Target | readonly Target[] | MotionValue,
90
+ animation: Keyframes | PresetName | number | readonly (number | null)[],
91
+ options?: AnimateOptions | SpringName,
92
+ ): PlaybackAnimation | Animation {
93
+ if (isMotionValue(target))
94
+ return asRoot(target).animate(
95
+ animation as number | readonly (number | null)[],
96
+ options as ValueAnimationOptions | SpringName | undefined,
97
+ )
98
+ return prepareAnimation(
99
+ target as Target | readonly Target[],
100
+ animation as Keyframes | PresetName,
101
+ options,
102
+ )()
72
103
  }
73
104
 
74
105
  /** Validate and snapshot a complete animation before changing any view ownership. */
@@ -77,7 +108,7 @@ export function prepareAnimation(
77
108
  animation: Keyframes | PresetName,
78
109
  options?: AnimateOptions | SpringName,
79
110
  ): () => PlaybackAnimation {
80
- const { keyframes, base, overrides, stagger, relative, haptic, path } = prepare(
111
+ const { keyframes, base, overrides, stagger, relative, haptic, path, layer } = prepare(
81
112
  animation,
82
113
  options,
83
114
  )
@@ -88,8 +119,15 @@ export function prepareAnimation(
88
119
  for (const property of Object.keys(keyframes))
89
120
  if (isColorProperty(property)) validateColorTarget(view, property)
90
121
  else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
91
- const plans = plan(keyframes, base, overrides, 0, relative, path)
92
- return () => new ViewAnimation(view, plans, feedback)
122
+ const plans = plan(keyframes, base, overrides, 0, relative, path, layer)
123
+ return () => {
124
+ if (layer === undefined)
125
+ detachBindings(
126
+ view,
127
+ plans.map(({ property }) => property),
128
+ )
129
+ return new ViewAnimation(view, plans, feedback)
130
+ }
93
131
  }
94
132
 
95
133
  const views = target.map(toView)
@@ -97,6 +135,7 @@ export function prepareAnimation(
97
135
  for (const property of Object.keys(keyframes))
98
136
  if (isColorProperty(property)) validateColorTarget(view, property)
99
137
  else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
138
+ const seed = Math.random()
100
139
  const plans = views.map((_, index) =>
101
140
  plan(
102
141
  keyframes,
@@ -105,11 +144,20 @@ export function prepareAnimation(
105
144
  staggerDelay(stagger, index, views.length),
106
145
  relative,
107
146
  path,
147
+ layer,
148
+ seed,
108
149
  ),
109
150
  )
110
151
  return () =>
111
152
  playbackGroup(
112
- views.map((view, index) => new ViewAnimation(view, plans[index]!, feedback, true)),
153
+ views.map((view, index) => {
154
+ if (layer === undefined)
155
+ detachBindings(
156
+ view,
157
+ plans[index]!.map(({ property }) => property),
158
+ )
159
+ return new ViewAnimation(view, plans[index]!, feedback, true)
160
+ }),
113
161
  )
114
162
  }
115
163
 
@@ -186,15 +234,33 @@ export function decay(
186
234
  )
187
235
  }
188
236
 
189
- /** Stops all or selected properties at their current values. `scale` stops both axes. */
237
+ /**
238
+ * Stops all or selected properties at their current values. `scale` stops both axes.
239
+ * A motion value stops where it is, and so do the views bound to it.
240
+ */
241
+ export function stop(value: MotionValue, options?: { layer?: string }): void
190
242
  export function stop(
191
243
  target: Target | readonly Target[],
192
244
  properties?: readonly KeyframeProperty[],
245
+ options?: { layer?: string },
246
+ ): void
247
+ export function stop(
248
+ target: Target | readonly Target[] | MotionValue,
249
+ properties?: readonly KeyframeProperty[] | { layer?: string },
250
+ options?: { layer?: string },
193
251
  ) {
194
- const expanded = properties?.flatMap<Property>((property) =>
252
+ if (isMotionValue(target))
253
+ return asRoot(target).stop((properties as { layer?: string } | undefined)?.layer)
254
+ if (properties && !Array.isArray(properties))
255
+ throw new TypeError('View properties must be an array.')
256
+ const expanded = (
257
+ properties as readonly KeyframeProperty[] | undefined
258
+ )?.flatMap<Property>((property) =>
195
259
  property === 'scale' ? ['scaleX', 'scaleY'] : [property],
196
260
  )
197
- for (const view of toViews(target)) stopView(view, expanded)
261
+ for (const view of toViews(target as Target | readonly Target[]))
262
+ if (options?.layer !== undefined) stopLayer(view, options.layer, expanded)
263
+ else stopView(view, expanded)
198
264
  }
199
265
 
200
266
  /**
@@ -205,7 +271,13 @@ export function stop(
205
271
  * set(logo, { opacity: 1, scale: 1, y: 0 })
206
272
  * ```
207
273
  */
208
- export function set(target: Target | readonly Target[], values: Values): void {
274
+ export function set(value: MotionValue, to: number): void
275
+ export function set(target: Target | readonly Target[], values: Values): void
276
+ export function set(
277
+ target: Target | readonly Target[] | MotionValue,
278
+ values: Values | number,
279
+ ) {
280
+ if (isMotionValue(target)) return asRoot(target).set(values as number)
209
281
  if (values === null || typeof values !== 'object' || Array.isArray(values))
210
282
  throw new TypeError('set() takes an object of values.')
211
283
  const keyframes: Record<string, unknown> = {}
@@ -218,7 +290,7 @@ export function set(target: Target | readonly Target[], values: Values): void {
218
290
  )
219
291
  keyframes[key] = [value, value]
220
292
  }
221
- animate(target, keyframes as Keyframes, { duration: 0 })
293
+ animate(target as Target | readonly Target[], keyframes as Keyframes, { duration: 0 })
222
294
  }
223
295
 
224
296
  /** Plays several animations as one: finished once they all are, and stopped together. */
@@ -281,6 +353,7 @@ function resolveAnimation(
281
353
  stagger: user.stagger ?? own.stagger,
282
354
  relative: user.relative ?? own.relative,
283
355
  haptic: user.haptic ?? own.haptic,
356
+ layer: user.layer,
284
357
  }
285
358
  }
286
359
 
@@ -307,16 +380,21 @@ function normalize(options: AnimateOptions | SpringName | undefined): Options {
307
380
  let stagger: Options['stagger']
308
381
  let relative: Options['relative']
309
382
  let haptic: Options['haptic']
383
+ let layer: Options['layer']
310
384
  for (const [key, value] of Object.entries(options)) {
311
385
  if (value === undefined) continue
312
- if (key === 'stagger') stagger = value as Options['stagger']
386
+ if (key === 'layer') {
387
+ if (typeof value !== 'string' || !value)
388
+ throw new TypeError('A layer is named by a nonempty string.')
389
+ layer = value
390
+ } else if (key === 'stagger') stagger = value as Options['stagger']
313
391
  else if (key === 'relative') relative = value as Options['relative']
314
392
  else if (key === 'haptic') haptic = value as Options['haptic']
315
393
  else if (key === 'path' || key === 'scale' || isProperty(key)) {
316
394
  overrides[key] = typeof value === 'string' ? spring(value as SpringName) : value
317
395
  } else base[key] = value
318
396
  }
319
- return { base, overrides, stagger, relative, haptic }
397
+ return { base, overrides, stagger, relative, haptic, layer }
320
398
  }
321
399
 
322
400
  function plan(
@@ -326,8 +404,16 @@ function plan(
326
404
  offset: number,
327
405
  relative = false,
328
406
  path?: PreparedPath,
407
+ layer?: string,
408
+ seed = Math.random(),
329
409
  ): Plan[] {
410
+ if (layer !== undefined && path)
411
+ throw new Error("A path owns x and y; it can't play on a layer.")
330
412
  return expand(keyframes, path).map(([property, frames]) => {
413
+ if (layer !== undefined && !layerable(property))
414
+ throw new Error(
415
+ `Layers apply to x, y, scale, rotation and opacity; "${property}" has a single owner.`,
416
+ )
331
417
  if (path?.properties.includes(property as 'x' | 'y' | 'rotate')) {
332
418
  if (
333
419
  !Number.isFinite(path.transition.delay + offset) ||
@@ -347,12 +433,13 @@ function plan(
347
433
  const override =
348
434
  overrides[property] ?? (property.startsWith('scale') ? overrides.scale : undefined)
349
435
  const transition = resolveTransition(
350
- override ? mergeTransitions(base, override) : base,
436
+ fitTransition(override ? mergeTransitions(base, override) : base, frames.length),
351
437
  property,
352
438
  frames.length,
353
439
  )
354
440
  transition.delay += offset
355
- return { property, keyframes: frames, transition, relative }
441
+ if (transition.seed !== undefined) transition.seed = seed
442
+ return { property, keyframes: frames, transition, relative, layer }
356
443
  })
357
444
  }
358
445
 
@@ -361,8 +448,10 @@ export function startingValues(
361
448
  animation: Keyframes | PresetName,
362
449
  options?: AnimateOptions | SpringName,
363
450
  ) {
364
- const { keyframes, relative, path } = prepare(animation, options)
451
+ const { keyframes, relative, path, layer } = prepare(animation, options)
365
452
  if (!keyframes) throw new Error(`Unknown preset "${animation}".`)
453
+ if (layer !== undefined)
454
+ return new Map(expand(keyframes, path).map(([property]) => [property, null]))
366
455
  return new Map(
367
456
  expand(keyframes, path).map(([property, [first]]) => [
368
457
  property,
package/src/drag.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  type TouchGestureEventData,
6
6
  type View,
7
7
  } from '@nativescript/core'
8
- import { stop, toView } from './animate.js'
8
+ import { toView } from './animate.js'
9
9
  import { DEFAULT_FRICTION } from './decay.js'
10
10
  import { DragInput } from './drag-input.js'
11
11
  import {
@@ -15,9 +15,18 @@ import {
15
15
  solveDragRelease,
16
16
  type DragCurve,
17
17
  } from './drag-physics.js'
18
- import { currentValue, ViewAnimation, type Plan } from './engine.js'
18
+ import {
19
+ baseValue,
20
+ currentValue,
21
+ setBase,
22
+ stopBase,
23
+ ViewAnimation,
24
+ type Animation,
25
+ type Plan,
26
+ } from './engine.js'
27
+ import { asRoot, isMotionValue, type MotionValue } from './motion-value.js'
19
28
  import type { PlaybackAnimation } from './playback.js'
20
- import { readValue, writeValue } from './properties.js'
29
+ import { createTrack } from './track.js'
21
30
  import type { Target } from './types.js'
22
31
 
23
32
  export interface DragPoint {
@@ -29,10 +38,10 @@ export interface DragEvent extends DragPoint {
29
38
  readonly velocityX: number
30
39
  readonly velocityY: number
31
40
  }
32
- export interface DragRelease extends DragEvent {
41
+ export interface DragRelease<A extends Animation = PlaybackAnimation> extends DragEvent {
33
42
  readonly target: DragPoint
34
43
  /** False on interruption/cancellation. Check before applying completion effects. */
35
- readonly animation: PlaybackAnimation
44
+ readonly animation: A
36
45
  }
37
46
  export interface DragOptions {
38
47
  /** Defaults to both. One finger only. */
@@ -51,6 +60,17 @@ export interface DragOptions {
51
60
  onCancel?: (event: DragEvent) => void
52
61
  onSettle?: (position: DragPoint) => void
53
62
  }
63
+ /** Drags a motion value instead of the view. Bounds and snap points are in the value's units. */
64
+ export interface DragValueOptions extends Omit<
65
+ DragOptions,
66
+ 'axis' | 'snap' | 'onRelease'
67
+ > {
68
+ /** Takes the position on every move and plays the release; bind views to it to follow. */
69
+ value: MotionValue
70
+ axis: 'x' | 'y'
71
+ snap?: readonly number[]
72
+ onRelease?: (event: DragRelease<Animation>) => void
73
+ }
54
74
  export interface Drag {
55
75
  readonly dragging: boolean
56
76
  readonly settling: boolean
@@ -67,22 +87,28 @@ export interface Drag {
67
87
  }
68
88
 
69
89
  type Axis = 'x' | 'y'
90
+ type Root = ReturnType<typeof asRoot>
70
91
  interface Settings {
71
92
  axes: Axis[]
72
93
  curves: Record<Axis, DragCurve>
73
- callbacks: Pick<
74
- DragOptions,
75
- 'onStart' | 'onMove' | 'onRelease' | 'onCancel' | 'onSettle'
76
- >
94
+ value: Root | undefined
95
+ callbacks: Pick<DragOptions, 'onStart' | 'onMove' | 'onCancel' | 'onSettle'> & {
96
+ onRelease?: (event: DragRelease<Animation>) => void
97
+ }
77
98
  }
78
99
  const owners = new WeakMap<View, Drag>()
79
100
 
80
- function settings(options: DragOptions): Settings {
101
+ function settings(options: DragOptions | DragValueOptions): Settings {
81
102
  if (!options || typeof options !== 'object')
82
103
  throw new TypeError('Drag options must be an object.')
83
104
  const axis = options.axis ?? 'both'
84
105
  if (!['x', 'y', 'both'].includes(axis))
85
106
  throw new TypeError('Drag axis must be x, y or both.')
107
+ const value = 'value' in options ? options.value : undefined
108
+ if (value !== undefined && !isMotionValue(value))
109
+ throw new TypeError('Drag value must be a motion value.')
110
+ if (value !== undefined && axis === 'both')
111
+ throw new TypeError('Dragging a motion value needs axis "x" or "y".')
86
112
  const friction = options.friction ?? DEFAULT_FRICTION
87
113
  if (!Number.isFinite(friction) || friction <= 0 || friction >= 1)
88
114
  throw new RangeError('Drag friction must be between 0 and 1 exclusively.')
@@ -152,18 +178,23 @@ function settings(options: DragOptions): Settings {
152
178
  const callbacks = {
153
179
  onStart: options.onStart,
154
180
  onMove: options.onMove,
155
- onRelease: options.onRelease,
181
+ onRelease: options.onRelease as Settings['callbacks']['onRelease'],
156
182
  onCancel: options.onCancel,
157
183
  onSettle: options.onSettle,
158
184
  }
159
185
  for (const callback of Object.values(callbacks))
160
186
  if (callback !== undefined && typeof callback !== 'function')
161
187
  throw new TypeError('Drag callbacks must be functions.')
162
- return { axes, curves, callbacks }
188
+ return { axes, curves, value: value && asRoot(value), callbacks }
163
189
  }
164
190
 
165
- /** Main-thread pan updates; release physics play through Jolt's native animation drivers. */
166
- export function drag(target: Target, options: DragOptions = {}): Drag {
191
+ /**
192
+ * Main-thread pan updates; release physics play through Jolt's native animation drivers.
193
+ * With `value`, the gesture drives a motion value and the views bound to it.
194
+ */
195
+ export function drag(target: Target, options: DragValueOptions): Drag
196
+ export function drag(target: Target, options?: DragOptions): Drag
197
+ export function drag(target: Target, options: DragOptions | DragValueOptions = {}): Drag {
167
198
  const view = toView(target)
168
199
  const config = settings(options) // Invalid replacements leave the existing owner intact.
169
200
  const previous = owners.get(view)
@@ -187,7 +218,7 @@ class DragController implements Drag {
187
218
  private attached = false
188
219
  private blocked = false
189
220
  private generation = 0
190
- private release: PlaybackAnimation | undefined
221
+ private release: Animation | undefined
191
222
  private input = new DragInput()
192
223
  private origin = { x: 0, y: 0 }
193
224
  private last: DragEvent = { x: 0, y: 0, velocityX: 0, velocityY: 0 }
@@ -215,6 +246,8 @@ class DragController implements Drag {
215
246
  return this.destroyed
216
247
  }
217
248
  get position(): DragPoint {
249
+ const value = this.config.value
250
+ if (value) return { x: 0, y: 0, [this.config.axes[0]!]: value.base() }
218
251
  return { x: currentValue(this.view, 'x'), y: currentValue(this.view, 'y') }
219
252
  }
220
253
  cancel = () => {
@@ -303,9 +336,15 @@ class DragController implements Drag {
303
336
  this.cancel()
304
337
  if (!this.attached || this.destroyed || this.disabled) return
305
338
  this.blocked = false
306
- // Claim just the enabled axes, preserving the native presentation on takeover.
307
- stop(this.view, this.config.axes)
308
- this.origin = { x: readValue(this.view, 'x'), y: readValue(this.view, 'y') }
339
+ const value = this.config.value
340
+ if (value) {
341
+ value.stopBase()
342
+ this.origin = this.position
343
+ } else {
344
+ // Claim just the enabled axes, preserving the native presentation on takeover.
345
+ stopBase(this.view, this.config.axes)
346
+ this.origin = { x: baseValue(this.view, 'x'), y: baseValue(this.view, 'y') }
347
+ }
309
348
  const size =
310
349
  (this.view.parent as View | undefined)?.getActualSize() ??
311
350
  this.view.getActualSize()
@@ -346,8 +385,11 @@ class DragController implements Drag {
346
385
  : clampDrag(raw, curve.bounds)
347
386
  if (axis === 'x') vx = input.velocityX * resistance
348
387
  else vy = input.velocityY * resistance
349
- stop(this.view, [axis])
350
- writeValue(this.view, axis, position[axis])
388
+ if (this.config.value) this.config.value.setBase(position[axis])
389
+ else {
390
+ stopBase(this.view, [axis])
391
+ setBase(this.view, axis, position[axis])
392
+ }
351
393
  }
352
394
  this.last = { ...position, velocityX: vx, velocityY: vy }
353
395
  if (event.state !== 3)
@@ -383,7 +425,11 @@ class DragController implements Drag {
383
425
  transition: { curve, delay: 0, repeat: 0, reverse: false },
384
426
  }
385
427
  })
386
- const animation = new ViewAnimation(this.view, plans)
428
+ const [axis] = this.config.axes
429
+ const value = this.config.value
430
+ const animation = value
431
+ ? value.play(createTrack(axis!, [event[axis!]], plans[0]!.transition, 0))
432
+ : new ViewAnimation(this.view, plans)
387
433
  this.release = animation
388
434
  void animation.finished.then((finished) => {
389
435
  if (generation !== this.generation || this.destroyed) return
@@ -12,6 +12,7 @@ import {
12
12
  import { Application, Utils, type View } from '@nativescript/core'
13
13
  import type { NumericProperty } from '../properties.js'
14
14
  import type { Driver } from './index.js'
15
+ import { frame } from './frame.js'
15
16
  import { isColorProperty } from '../color.js'
16
17
  import {
17
18
  playAndroidColor,
@@ -109,6 +110,7 @@ export function createAndroidDriver(): Driver {
109
110
 
110
111
  return {
111
112
  now: () => android.os.SystemClock.uptimeMillis(),
113
+ frame,
112
114
  clock,
113
115
  control(_view, handle, elapsed, paused, command) {
114
116
  ;(handle as Handle).control(elapsed, paused, command)
@@ -0,0 +1,10 @@
1
+ import {
2
+ cancelAnimationFrame,
3
+ requestAnimationFrame,
4
+ } from '@nativescript/core/animation-frame'
5
+
6
+ /** NativeScript schedules subsequent frames with CADisplayLink / Choreographer. */
7
+ export function frame(callback: () => void): () => void {
8
+ const id = requestAnimationFrame(callback)
9
+ return () => cancelAnimationFrame(id)
10
+ }
@@ -15,6 +15,8 @@ export interface LayoutFrame {
15
15
  export interface Driver {
16
16
  /** Milliseconds, on the clock the platform's animations run on. */
17
17
  now(): number
18
+ /** One native animation frame; returns an idempotent cancellation function. */
19
+ frame(callback: () => void): () => void
18
20
  /** Starts the track at `startedAt + track.delay` and calls `onEnd` once, however it ends. */
19
21
  play(
20
22
  view: View,
package/src/driver/ios.ts CHANGED
@@ -13,6 +13,7 @@ import { playIOSColor, presentedIOSColor } from './ios-color.js'
13
13
  import { after, type Driver } from './index.js'
14
14
  import { hasInteractionSurface } from '../interaction-surface.js'
15
15
  import { retainInteractiveLayout } from './ios-layout.js'
16
+ import { frame } from './frame.js'
16
17
 
17
18
  interface Handle {
18
19
  stop(): void
@@ -59,6 +60,7 @@ function ensurePerspective(view: View, layers: CALayer[]) {
59
60
  export function createIOSDriver(): Driver {
60
61
  return {
61
62
  now: () => CACurrentMediaTime() * 1000,
63
+ frame,
62
64
 
63
65
  play(view, track, startedAt, onEnd): Handle {
64
66
  if (isDecorationProperty(track.property))
package/src/easing.ts CHANGED
@@ -15,6 +15,7 @@ export type EasingName =
15
15
  | 'backOut'
16
16
  | 'backInOut'
17
17
  | 'anticipate'
18
+ | `${'quad' | 'cubic' | 'quart' | 'quint' | 'sine' | 'expo'}${'In' | 'Out' | 'InOut'}`
18
19
 
19
20
  export type Easing = EasingName | CubicBezier | EasingFunction
20
21
 
@@ -73,6 +74,13 @@ const mirror =
73
74
  (p) =>
74
75
  p <= 0.5 ? easing(2 * p) / 2 : (2 - easing(2 * (1 - p))) / 2
75
76
 
77
+ const family = <N extends string>(name: N, easeIn: EasingFunction) =>
78
+ ({
79
+ [`${name}In`]: easeIn,
80
+ [`${name}Out`]: reverse(easeIn),
81
+ [`${name}InOut`]: mirror(easeIn),
82
+ }) as Record<`${N}${'In' | 'Out' | 'InOut'}`, EasingFunction>
83
+
76
84
  const easings: Record<EasingName, EasingFunction> = {
77
85
  linear,
78
86
  ease: cubicBezier(0.25, 0.1, 0.25, 1),
@@ -87,6 +95,12 @@ const easings: Record<EasingName, EasingFunction> = {
87
95
  backInOut: mirror(backIn),
88
96
  anticipate: (p) =>
89
97
  p <= 0.5 ? backIn(2 * p) / 2 : p >= 1 ? 1 : 1 - Math.pow(2, 10 - 20 * p) / 2,
98
+ ...family('quad', (p) => p ** 2),
99
+ ...family('cubic', (p) => p ** 3),
100
+ ...family('quart', (p) => p ** 4),
101
+ ...family('quint', (p) => p ** 5),
102
+ ...family('sine', (p) => (p >= 1 ? 1 : 1 - Math.cos((p * Math.PI) / 2))),
103
+ ...family('expo', (p) => (p <= 0 ? 0 : 2 ** (10 * p - 10))),
90
104
  }
91
105
 
92
106
  export function resolveEasing(easing: Easing): EasingFunction {