@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
package/src/engine.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  createClock,
3
+ registerPlayback,
3
4
  validateProgress,
4
5
  type PlaybackAnimation,
5
6
  type PlaybackClock,
6
7
  type PlaybackCommand,
7
8
  } from './playback.js'
8
- import type { View } from '@nativescript/core'
9
+ import { Application, type View } from '@nativescript/core'
9
10
  import { getDriver } from './driver/index.js'
10
11
  import { playHaptics, type HapticOptions } from './haptics.js'
11
12
  import { readValue, writeValue, type Property } from './properties.js'
@@ -13,6 +14,7 @@ import { createTrack, type Track } from './track.js'
13
14
  import type { ResolvedTransition } from './transition.js'
14
15
  import { isColorProperty } from './color.js'
15
16
  import type { PreparedPath, PathProperty } from './path.js'
17
+ import { BASE, identity, MixHandle, mixFor, mixOf, mixesOf } from './mixer.js'
16
18
 
17
19
  export interface Animation extends PromiseLike<boolean> {
18
20
  /**
@@ -32,10 +34,17 @@ export interface Plan {
32
34
  transition: ResolvedTransition
33
35
  relative?: boolean
34
36
  path?: PreparedPath
37
+ /** A track that is already sampled, like a motion value mapped onto a property. */
38
+ track?: () => Track
39
+ /** A named layer composed with the property's base instead of replacing it. */
40
+ layer?: string
35
41
  }
36
42
 
37
43
  interface Entry {
38
44
  readonly track: Track
45
+ /** The property, or the property and its layer. */
46
+ readonly key: string
47
+ readonly layer?: string
39
48
  readonly startedAt: number
40
49
  readonly owner: ViewAnimation
41
50
  handle: unknown
@@ -48,7 +57,7 @@ interface Entry {
48
57
 
49
58
  const revisions = new WeakMap<View, Map<Property, symbol>>()
50
59
 
51
- const running = new WeakMap<View, Map<Property, Entry>>()
60
+ const running = new WeakMap<View, Map<string, Entry>>()
52
61
  const arrivals = new WeakMap<Animation, Promise<unknown>>()
53
62
 
54
63
  /** Internal forecast for starting a timeline step before another one arrives. */
@@ -101,7 +110,9 @@ export class ViewAnimation implements PlaybackAnimation {
101
110
  private readyListener: (() => void) | undefined
102
111
  private readonly endless: boolean
103
112
  private readonly timingListeners = new Set<() => void>()
113
+ private readonly playbackChanged: () => void
104
114
  private timingChanged() {
115
+ this.playbackChanged()
105
116
  for (const callback of [...this.timingListeners]) callback()
106
117
  }
107
118
 
@@ -134,6 +145,7 @@ export class ViewAnimation implements PlaybackAnimation {
134
145
  if (this.settled) return
135
146
  if (!this.started) {
136
147
  this.pendingProgress = value
148
+ this.timingChanged()
137
149
  return
138
150
  }
139
151
  this.seekTime(this.duration * value)
@@ -188,7 +200,7 @@ export class ViewAnimation implements PlaybackAnimation {
188
200
  entry.done = false
189
201
  entry.finished = false
190
202
  this.pending++
191
- entriesOf(this.view).set(entry.track.property, entry)
203
+ entriesOf(this.view).set(entry.key, entry)
192
204
  this.playEntry(entry, position)
193
205
  }
194
206
  }
@@ -199,9 +211,9 @@ export class ViewAnimation implements PlaybackAnimation {
199
211
  this.timingChanged()
200
212
  }
201
213
  elapsed(entry: Entry, now: number) {
202
- return this.controlled
203
- ? this.time()
204
- : (getDriver().elapsed?.(entry.handle) ?? now - entry.startedAt)
214
+ if (this.controlled) return this.time()
215
+ if (entry.handle instanceof MixHandle) return entry.handle.elapsed()
216
+ return getDriver().elapsed?.(entry.handle) ?? now - entry.startedAt
205
217
  }
206
218
  private time() {
207
219
  return this.clock?.time() ?? 0
@@ -209,8 +221,10 @@ export class ViewAnimation implements PlaybackAnimation {
209
221
  private controlTracks(position: number, command: PlaybackCommand = 'seek') {
210
222
  const driver = getDriver()
211
223
  for (const entry of this.entries)
212
- if (!entry.done)
213
- driver.control?.(this.view, entry.handle, position, this._paused, command)
224
+ if (entry.done) continue
225
+ else if (entry.handle instanceof MixHandle)
226
+ entry.handle.control(position, this._paused)
227
+ else driver.control?.(this.view, entry.handle, position, this._paused, command)
214
228
  }
215
229
  private scheduleArrival() {
216
230
  this.cancelArrival?.()
@@ -244,21 +258,38 @@ export class ViewAnimation implements PlaybackAnimation {
244
258
  const initial = entry.generation === 0
245
259
  const instance = ++entry.generation
246
260
  const driver = getDriver()
247
- entry.handle = driver.play(
248
- this.view,
249
- entry.track,
250
- initial ? entry.startedAt : driver.now() - elapsed,
251
- (finished = true) => {
252
- if (entry.done || instance !== entry.generation) return
253
- entry.done = true
254
- entry.finished = finished
255
- entry.completedValue = readValue(this.view, entry.track.property)
256
- forget(entry)
257
- this.ended(finished)
258
- },
259
- )
261
+ const startedAt = initial ? entry.startedAt : driver.now() - elapsed
262
+ const onEnd = (finished = true) => {
263
+ if (entry.done || instance !== entry.generation) return
264
+ entry.done = true
265
+ entry.finished = finished
266
+ entry.completedValue = readValue(this.view, entry.track.property)
267
+ forget(entry)
268
+ this.ended(finished)
269
+ }
270
+ const { property } = entry.track
271
+ const mix =
272
+ entry.layer !== undefined ? mixing(this.view, property) : mixOf(this.view, property)
273
+ entry.handle = mix
274
+ ? mix.adopt(entry.layer ?? BASE, entry.track, startedAt, onEnd)
275
+ : driver.play(this.view, entry.track, startedAt, onEnd)
276
+ }
277
+
278
+ /** Moves a natively playing entry into its property's mix, without restarting it. */
279
+ rehome(entry: Entry) {
280
+ if (entry.done || entry.handle instanceof MixHandle) return
281
+ const elapsed = this.elapsed(entry, getDriver().now())
282
+ const handle = entry.handle
283
+ entry.generation++
284
+ getDriver().stop(this.view, handle)
285
+ this.playEntry(entry, elapsed)
286
+ }
287
+ private unloaded = () => {
288
+ // NativeScript unloads the root view when iOS enters the background.
289
+ // An explicitly paused owner can resume after that temporary unload.
290
+ if (this._paused && Application.suspended) return
291
+ this.stop()
260
292
  }
261
- private unloaded = () => this.stop()
262
293
 
263
294
  constructor(
264
295
  readonly view: View,
@@ -273,6 +304,18 @@ export class ViewAnimation implements PlaybackAnimation {
273
304
  !Number.isFinite(plan.transition.curve.duration)),
274
305
  )
275
306
  this.finished = new Promise((resolve) => (this.resolve = resolve))
307
+ this.playbackChanged = registerPlayback(this, () => ({
308
+ position: this.pendingProgress,
309
+ time:
310
+ this.pendingProgress === undefined
311
+ ? this.time()
312
+ : this.pendingProgress * this.duration,
313
+ duration: this.duration,
314
+ paused: this._paused,
315
+ ready: this.ready,
316
+ settled: this.settled,
317
+ completed: this.settled && this.complete,
318
+ }))
276
319
  setArrival(this, new Promise<void>((resolve) => (this.arrive = resolve)))
277
320
  setArrivalTiming(this, {
278
321
  read: () => ({
@@ -322,52 +365,78 @@ export class ViewAnimation implements PlaybackAnimation {
322
365
  const now = driver.now()
323
366
  const entries = entriesOf(this.view)
324
367
 
325
- const tracks = plans.map(({ property, keyframes, transition, relative, path }) => {
326
- const previous = entries.get(property)
368
+ const made = plans.map((plan) => {
369
+ const { property, keyframes, transition, relative, path, layer } = plan
370
+ const key = layer === undefined ? property : `${property}#${layer}`
371
+ const mixed = layer !== undefined || mixOf(this.view, property) !== undefined
372
+ const previous = entries.get(key)
327
373
  let current: number
328
374
  let velocity = 0
329
375
  if (previous) {
330
376
  const elapsed = elapsedOf(previous, now)
331
- current = driver.presented(this.view, property) ?? previous.track.valueAt(elapsed)
377
+ current =
378
+ (mixed ? undefined : driver.presented(this.view, property)) ??
379
+ previous.track.valueAt(elapsed)
332
380
  velocity = previous.owner.paused ? 0 : previous.track.velocityAt(elapsed)
333
381
  release(previous, now, false)
382
+ } else if (mixed) {
383
+ current =
384
+ mixOf(this.view, property)?.rest(layer ?? BASE) ??
385
+ (layer === undefined ? readValue(this.view, property) : identity(property))
334
386
  } else {
335
387
  current =
336
388
  (isColorProperty(property)
337
389
  ? driver.presented(this.view, property)
338
390
  : undefined) ?? readValue(this.view, property)
339
391
  }
340
- if (path)
341
- return path.track(
342
- property as PathProperty,
343
- relative ? current : 0,
344
- transition.delay,
392
+ const build = () => {
393
+ if (plan.track) return plan.track()
394
+ if (path)
395
+ return path.track(
396
+ property as PathProperty,
397
+ relative ? current : 0,
398
+ transition.delay,
399
+ )
400
+ return createTrack(
401
+ property,
402
+ keyframes.map((value) => {
403
+ if (value === null) return current
404
+ if (layer !== undefined || !relative || isColorProperty(property))
405
+ return value
406
+ return property === 'opacity' || property.startsWith('scale')
407
+ ? current * value
408
+ : current + value
409
+ }),
410
+ transition,
411
+ velocity,
345
412
  )
346
- return createTrack(
347
- property,
348
- keyframes.map((value) => {
349
- if (value === null) return current
350
- if (!relative || isColorProperty(property)) return value
351
- return property === 'opacity' || property.startsWith('scale')
352
- ? current * value
353
- : current + value
354
- }),
355
- transition,
356
- velocity,
357
- )
413
+ }
414
+ return { track: build(), key, layer }
358
415
  })
359
-
360
- for (const track of tracks) writeValue(this.view, track.property, track.to)
416
+ const tracks = made.map(({ track }) => track)
417
+
418
+ for (const { track, layer } of made) {
419
+ const mix =
420
+ layer !== undefined
421
+ ? mixing(this.view, track.property)
422
+ : mixOf(this.view, track.property)
423
+ if (!mix) writeValue(this.view, track.property, track.to)
424
+ else if (track.end === 0) mix.setRest(layer ?? BASE, track.to)
425
+ }
361
426
  driver.sync?.(this.view)
362
427
 
363
- for (const track of tracks) {
364
- let tokens = revisions.get(this.view)
365
- if (!tokens) revisions.set(this.view, (tokens = new Map()))
428
+ for (const { track, key, layer } of made) {
366
429
  const token = Symbol(track.property)
367
- tokens.set(track.property, token)
430
+ if (layer === undefined) {
431
+ let tokens = revisions.get(this.view)
432
+ if (!tokens) revisions.set(this.view, (tokens = new Map()))
433
+ tokens.set(track.property, token)
434
+ }
368
435
  if (track.end === 0) continue
369
436
  const entry: Entry = {
370
437
  track,
438
+ key,
439
+ layer,
371
440
  startedAt: now,
372
441
  owner: this,
373
442
  handle: undefined,
@@ -376,7 +445,7 @@ export class ViewAnimation implements PlaybackAnimation {
376
445
  generation: 0,
377
446
  token,
378
447
  }
379
- entries.set(track.property, entry)
448
+ entries.set(key, entry)
380
449
  this.entries.push(entry)
381
450
  this.pending++
382
451
  }
@@ -405,8 +474,8 @@ export class ViewAnimation implements PlaybackAnimation {
405
474
  }
406
475
 
407
476
  private waitForLoad(plans: readonly Plan[]) {
408
- for (const { property, keyframes, relative } of plans) {
409
- if (relative && !isColorProperty(property)) continue
477
+ for (const { property, keyframes, relative, layer } of plans) {
478
+ if (layer !== undefined || (relative && !isColorProperty(property))) continue
410
479
  const first = keyframes[0]
411
480
  if (first !== null && first !== undefined) writeValue(this.view, property, first)
412
481
  }
@@ -438,6 +507,8 @@ export class ViewAnimation implements PlaybackAnimation {
438
507
 
439
508
  /** Current native presentation when available, otherwise the active curve or model. */
440
509
  export function currentValue(view: View, property: Property): number {
510
+ const mix = mixOf(view, property)
511
+ if (mix) return mix.value()
441
512
  const entry = running.get(view)?.get(property)
442
513
  if (!entry) return readValue(view, property)
443
514
  const driver = getDriver()
@@ -465,19 +536,69 @@ export function captureValue(view: View, property: Property): () => number | und
465
536
  }
466
537
  }
467
538
 
468
- /** Stops whatever is animating these properties of the view, or all of them. */
539
+ /** Stops whatever is animating these properties of the view, or all of them, layers included. */
469
540
  export function stopView(view: View, properties?: readonly Property[]) {
470
541
  const tokens = revisions.get(view)
471
542
  for (const property of properties ?? tokens?.keys() ?? [])
472
543
  tokens?.set(property, Symbol(property))
544
+ for (const mix of mixesOf(view))
545
+ if (!properties || properties.includes(mix.property)) mix.freeze()
473
546
  const entries = running.get(view)
474
547
  if (!entries) return
475
548
  const now = getDriver().now()
476
- for (const [property, entry] of [...entries]) {
477
- if (!properties || properties.includes(property)) release(entry, now, true)
549
+ for (const entry of [...entries.values()]) {
550
+ if (!properties || properties.includes(entry.track.property))
551
+ release(entry, now, true)
552
+ }
553
+ }
554
+
555
+ /** Stops the regular animations of these properties and leaves their layers running. */
556
+ export function stopBase(view: View, properties: readonly Property[]) {
557
+ const tokens = revisions.get(view)
558
+ for (const property of properties) tokens?.set(property, Symbol(property))
559
+ const now = getDriver().now()
560
+ for (const property of properties) {
561
+ const entry = running.get(view)?.get(property)
562
+ if (entry) release(entry, now, true)
478
563
  }
479
564
  }
480
565
 
566
+ /** Stops one layer and takes away what it adds, also after it finished. */
567
+ export function stopLayer(view: View, layer: string, properties?: readonly Property[]) {
568
+ const now = getDriver().now()
569
+ for (const entry of [...(running.get(view)?.values() ?? [])])
570
+ if (
571
+ entry.layer === layer &&
572
+ (!properties || properties.includes(entry.track.property))
573
+ )
574
+ release(entry, now, true)
575
+ for (const mix of mixesOf(view))
576
+ if (!properties || properties.includes(mix.property))
577
+ mix.setRest(layer, identity(mix.property))
578
+ }
579
+
580
+ /** Writes a property's base value, under its layers when it has any. */
581
+ export function setBase(view: View, property: Property, value: number) {
582
+ const mix = mixOf(view, property)
583
+ if (mix) mix.setRest(BASE, value)
584
+ else writeValue(view, property, value)
585
+ }
586
+
587
+ /** A property's base value, without what its layers add. */
588
+ export function baseValue(view: View, property: Property): number {
589
+ return mixOf(view, property)?.rest(BASE) ?? readValue(view, property)
590
+ }
591
+
592
+ /** The property's mix, created on its first layer; a base playing natively moves into it. */
593
+ function mixing(view: View, property: Property) {
594
+ const existing = mixOf(view, property)
595
+ if (existing) return existing
596
+ const mix = mixFor(view, property)
597
+ const base = running.get(view)?.get(property)
598
+ if (base) base.owner.rehome(base)
599
+ return mix
600
+ }
601
+
481
602
  function entriesOf(view: View) {
482
603
  let entries = running.get(view)
483
604
  if (!entries) running.set(view, (entries = new Map()))
@@ -486,7 +607,7 @@ function entriesOf(view: View) {
486
607
 
487
608
  function forget(entry: Entry) {
488
609
  const entries = running.get(entry.owner.view)
489
- if (entries?.get(entry.track.property) === entry) entries.delete(entry.track.property)
610
+ if (entries?.get(entry.key) === entry) entries.delete(entry.key)
490
611
  }
491
612
 
492
613
  function release(entry: Entry, now: number, commit: boolean) {
@@ -495,6 +616,11 @@ function release(entry: Entry, now: number, commit: boolean) {
495
616
  const { property } = entry.track
496
617
  entry.done = true
497
618
  forget(entry)
619
+ if (entry.handle instanceof MixHandle) {
620
+ entry.handle.mix.detach(entry.layer ?? BASE, commit)
621
+ entry.owner.ended(false)
622
+ return
623
+ }
498
624
  if (commit) {
499
625
  const value =
500
626
  driver.presented(view, property) ?? entry.track.valueAt(elapsedOf(entry, now))
package/src/gradient.ts CHANGED
@@ -345,10 +345,16 @@ export function gradientPoints(
345
345
  const sin = Math.sin(radians)
346
346
  const cos = Math.cos(radians)
347
347
  if (type === 'linear') {
348
- const length = Math.abs(width * sin) + Math.abs(height * cos)
349
- const dx = width > 0 ? (sin * length) / (2 * width) : 0
350
- const dy = height > 0 ? (-cos * length) / (2 * height) : 0
351
- return { start: { x: 0.5 - dx, y: 0.5 - dy }, end: { x: 0.5 + dx, y: 0.5 + dy } }
348
+ // Core Animation keeps an axial gradient's bands square to its line in unit space, so
349
+ // on a box that isn't square the line leans to keep them square to `angle` on screen.
350
+ const ux = height > 0 ? sin / height : 0
351
+ const uy = width > 0 ? -cos / width : 0
352
+ const squared = ux * ux + uy * uy
353
+ const reach = squared > 0 ? (Math.abs(ux) + Math.abs(uy)) / (2 * squared) : 0
354
+ return {
355
+ start: { x: 0.5 - ux * reach, y: 0.5 - uy * reach },
356
+ end: { x: 0.5 + ux * reach, y: 0.5 + uy * reach },
357
+ }
352
358
  }
353
359
  const center = { x: frame.x, y: frame.y }
354
360
  const shorter = Math.min(width, height)
@@ -43,6 +43,7 @@ export interface ImageEffectDiagnostics {
43
43
  export interface ImageEffect extends Animation {
44
44
  readonly effect: ImageEffectName
45
45
  readonly paused: boolean
46
+ readonly duration: number
46
47
  /** Finite progress; distortion rejects seeking because it follows native touch input. */
47
48
  progress: number
48
49
  readonly diagnostics: Readonly<ImageEffectDiagnostics>
@@ -157,7 +158,7 @@ class Effect implements ImageEffect {
157
158
  for (const event of ['loaded', 'layoutChanged']) image.on(event, this.schedule)
158
159
  for (const event of ['imageSourceChange', 'stretchChange'])
159
160
  image.on(event, this.changed)
160
- image.on('unloaded', this.stop)
161
+ image.on('unloaded', this.unloaded)
161
162
  image.style.on('visibilityChange', this.visible)
162
163
  for (const event of surfaceChanges) image.style.on(event, this.changed)
163
164
  Application.on(Application.suspendEvent, this.suspend)
@@ -178,6 +179,9 @@ class Effect implements ImageEffect {
178
179
  get paused() {
179
180
  return this.userPaused
180
181
  }
182
+ get duration() {
183
+ return this.effect === 'distortion' ? Infinity : this.options.duration
184
+ }
181
185
  get progress() {
182
186
  return this.renderer?.progress ?? this.position
183
187
  }
@@ -240,7 +244,7 @@ class Effect implements ImageEffect {
240
244
  for (const event of ['loaded', 'layoutChanged']) this.image.off(event, this.schedule)
241
245
  for (const event of ['imageSourceChange', 'stretchChange'])
242
246
  this.image.off(event, this.changed)
243
- this.image.off('unloaded', this.stop)
247
+ this.image.off('unloaded', this.unloaded)
244
248
  this.image.style.off('visibilityChange', this.visible)
245
249
  for (const event of surfaceChanges) this.image.style.off(event, this.changed)
246
250
  Application.off(Application.suspendEvent, this.suspend)
@@ -272,6 +276,10 @@ class Effect implements ImageEffect {
272
276
  this.suspended = true
273
277
  this.renderer?.pause()
274
278
  }
279
+ private unloaded = () => {
280
+ if (Application.suspended && this.userPaused) return
281
+ this.stop()
282
+ }
275
283
  private resume = () => {
276
284
  this.suspended = false
277
285
  this.schedule()
package/src/index.ts CHANGED
@@ -1,11 +1,32 @@
1
+ export { timeline } from './sequence.js'
2
+ export type { TimelineAnimation, TimelineStep, PlayableAnimation } from './sequence.js'
3
+ export { playbackProgress } from './motion-value.js'
1
4
  export { animate, decay, set, stop } from './animate.js'
5
+ export { bind, motionValue, scrollValue } from './motion-value.js'
6
+ export type {
7
+ Bindings,
8
+ DerivedColor,
9
+ DerivedValue,
10
+ MapOptions,
11
+ MotionValue,
12
+ ScrollValue,
13
+ ValueAnimationOptions,
14
+ } from './motion-value.js'
15
+ export type { SubscribeOptions, ValueListener } from './value-observers.js'
2
16
  export { sequence } from './sequence.js'
3
17
  export type { Step, SequenceOptions, SequencePosition } from './sequence.js'
4
18
  export { press } from './press.js'
5
19
  export { interactionSurface } from './interaction-surface.js'
6
20
  export type { InteractionSurface } from './interaction-surface.js'
7
21
  export { drag } from './drag.js'
8
- export type { Drag, DragOptions, DragPoint, DragEvent, DragRelease } from './drag.js'
22
+ export type {
23
+ Drag,
24
+ DragOptions,
25
+ DragValueOptions,
26
+ DragPoint,
27
+ DragEvent,
28
+ DragRelease,
29
+ } from './drag.js'
9
30
  export { scrollLinked } from './scroll-linked.js'
10
31
  export type { ScrollLinkedOptions } from './scroll-linked.js'
11
32
  export { animateItems } from './items.js'
@@ -56,6 +77,7 @@ export type { Preset, PresetName, Presets } from './presets.js'
56
77
  export { origamiSpring, springs } from './transition.js'
57
78
  export type {
58
79
  OrigamiSpring,
80
+ RepeatDelayRange,
59
81
  Spring,
60
82
  SpringName,
61
83
  Timing,
@@ -79,3 +79,36 @@ declare class JoltPhotoZoom extends NSObject {
79
79
  layout(): void
80
80
  stop(): void
81
81
  }
82
+
83
+ declare class JoltPropertySink extends NSObject {
84
+ static scalarTargetKeyPath(target: NSObject, keyPath: string): JoltPropertySink
85
+ readonly dimensions: number
86
+ readonly alive: boolean
87
+ readonly compiled: boolean
88
+ read(): NSArray<number>
89
+ write(values: NSArray<number>): void
90
+ dispose(): void
91
+ }
92
+ declare class JoltPropertyPlayer extends NSObject {
93
+ static startSinkTimesValuesFinalValueDurationDelayRepeatReverseCompletion(
94
+ sink: JoltPropertySink,
95
+ times: NSArray<number>,
96
+ values: NSArray<number>,
97
+ finalValue: NSArray<number>,
98
+ duration: number,
99
+ delay: number,
100
+ repeat: number,
101
+ reverse: boolean,
102
+ completion: (completed: boolean) => void,
103
+ ): JoltPropertyPlayer
104
+ readonly elapsed: number
105
+ readonly frames: number
106
+ static activeCount(): number
107
+ controlPaused(time: number, paused: boolean): void
108
+ cancel(): void
109
+ }
110
+ declare class JoltReferenceProperties extends NSObject {
111
+ static mapCoordinate(target: NSObject): JoltPropertySink
112
+ static mapCamera(target: NSObject): JoltPropertySink
113
+ static scenePose(target: NSObject): JoltPropertySink
114
+ }
package/src/keyboard.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Application, Utils, isAndroid, isIOS, type View } from '@nativescript/core'
2
- import { stop, toView } from './animate.js'
2
+ import { animate, stop, toView } from './animate.js'
3
3
  import type { EasingFunction, EasingName } from './easing.js'
4
- import type { Spring, Transition } from './transition.js'
4
+ import type { Spring, Transition, Tween } from './transition.js'
5
5
  import type { Target } from './types.js'
6
6
 
7
7
  export interface KeyboardOptions {
@@ -30,6 +30,12 @@ const iosKeyboard: Spring = {
30
30
  damping: 2 * Math.sqrt(3000),
31
31
  }
32
32
  const iosCurves: readonly EasingName[] = ['easeInOut', 'easeIn', 'easeOut', 'linear']
33
+ // Before Android 11 nothing reports the keyboard's timing. Measured on an Android 9 emulator,
34
+ // its window fades and slides into place in about 150 ms, decelerating, and leaves as quickly.
35
+ const LEGACY_DURATION = 150
36
+ const legacyShow: Tween = { duration: LEGACY_DURATION, ease: 'quadOut' }
37
+ const legacyHide: Tween = { duration: LEGACY_DURATION, ease: 'quadIn' }
38
+ const legacy = () => isAndroid && android.os.Build.VERSION.SDK_INT < 30
33
39
  const hidden: KeyboardState = Object.freeze({
34
40
  visible: false,
35
41
  height: 0,
@@ -109,9 +115,11 @@ function watchIOS() {
109
115
  }
110
116
 
111
117
  function watchAndroid() {
112
- if (android.os.Build.VERSION.SDK_INT < 30) return undefined
113
118
  const density = Utils.layout.getDisplayDensity()
114
- let observer: org.rafaelaugusto.jolt.KeyboardObserver | undefined
119
+ let observer:
120
+ | org.rafaelaugusto.jolt.KeyboardObserver
121
+ | org.rafaelaugusto.jolt.KeyboardFrame
122
+ | undefined
115
123
  let stopped = false
116
124
  const attach = () => {
117
125
  Application.off(Application.launchEvent, attach)
@@ -121,19 +129,31 @@ function watchAndroid() {
121
129
  const root = activity?.getWindow()?.getDecorView()
122
130
  if (!root) return
123
131
  try {
124
- observer = new org.rafaelaugusto.jolt.KeyboardObserver(
125
- root,
126
- new org.rafaelaugusto.jolt.KeyboardObserver.Listener({
127
- onChange: (height, duration, curve) =>
128
- update(
129
- height / density,
130
- duration,
131
- duration
132
- ? { duration, ease: curve ? sampled(curve) : 'linear' }
133
- : { duration: 0 },
134
- ),
135
- }),
136
- )
132
+ observer = legacy()
133
+ ? new org.rafaelaugusto.jolt.KeyboardFrame(
134
+ root,
135
+ new org.rafaelaugusto.jolt.KeyboardFrame.Listener({
136
+ onChange: (height) =>
137
+ update(
138
+ height / density,
139
+ LEGACY_DURATION,
140
+ height ? legacyShow : legacyHide,
141
+ ),
142
+ }),
143
+ )
144
+ : new org.rafaelaugusto.jolt.KeyboardObserver(
145
+ root,
146
+ new org.rafaelaugusto.jolt.KeyboardObserver.Listener({
147
+ onChange: (height, duration, curve) =>
148
+ update(
149
+ height / density,
150
+ duration,
151
+ duration
152
+ ? { duration, ease: curve ? sampled(curve) : 'linear' }
153
+ : { duration: 0 },
154
+ ),
155
+ }),
156
+ )
137
157
  } catch {
138
158
  // A JS-only update cannot install the native plugin.
139
159
  return
@@ -241,7 +261,7 @@ function followIOS(view: View, offset: number) {
241
261
  }
242
262
 
243
263
  function followAndroid(view: View, offset: number) {
244
- if (android.os.Build.VERSION.SDK_INT < 30) return () => {}
264
+ if (legacy()) return followLegacy(view, offset)
245
265
  const density = Utils.layout.getDisplayDensity()
246
266
  let disposed = false
247
267
  let follower: org.rafaelaugusto.jolt.KeyboardFollower | undefined
@@ -283,6 +303,51 @@ function followAndroid(view: View, offset: number) {
283
303
  return cleanup
284
304
  }
285
305
 
306
+ /** Android 7 to 10: no frame-by-frame progress, so the view animates with the keyboard's usual timing. */
307
+ function followLegacy(view: View, offset: number) {
308
+ const density = Utils.layout.getDisplayDensity()
309
+ let disposed = false
310
+ let follower: org.rafaelaugusto.jolt.KeyboardFrameFollower | undefined
311
+ const attach = () => {
312
+ if (disposed || follower) return
313
+ const native = view.nativeViewProtected as android.view.View | undefined
314
+ if (!native) return
315
+ try {
316
+ follower = new org.rafaelaugusto.jolt.KeyboardFrameFollower(
317
+ native,
318
+ offset * density,
319
+ new org.rafaelaugusto.jolt.KeyboardFrameFollower.Listener({
320
+ onMove: (from, to, keyboard) => {
321
+ stop(view, ['y'])
322
+ view.translateY = from / density
323
+ animate(view, { y: to / density }, keyboard ? legacyShow : legacyHide)
324
+ },
325
+ }),
326
+ )
327
+ } catch {
328
+ // A JS-only update cannot install the native plugin. Leave the platform's resize in place.
329
+ return
330
+ }
331
+ follower.attach()
332
+ }
333
+ const cleanup = () => {
334
+ if (disposed) return
335
+ disposed = true
336
+ view.off('loaded', attach)
337
+ view.off('unloaded', cleanup)
338
+ if (follower) {
339
+ follower.detach()
340
+ follower = undefined
341
+ stop(view, ['y'])
342
+ view.translateY = 0
343
+ }
344
+ }
345
+ view.on('loaded', attach)
346
+ view.on('unloaded', cleanup)
347
+ if (view.isLoaded) attach()
348
+ return cleanup
349
+ }
350
+
286
351
  /** The runtime hands NSNumber values over as plain numbers. */
287
352
  function numberIn(info: NSDictionary<unknown, unknown>, key: string): number {
288
353
  const value = info.objectForKey(key) as number | NSNumber | null | undefined