@rafaelaugusto/jolt 0.1.0-beta.15 → 0.1.0-beta.17

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 (132) hide show
  1. package/README.md +9 -2
  2. package/dist/bottom-sheet/geometry.d.ts +18 -0
  3. package/dist/bottom-sheet/geometry.d.ts.map +1 -0
  4. package/dist/bottom-sheet/geometry.js +51 -0
  5. package/dist/bottom-sheet/geometry.js.map +1 -0
  6. package/dist/bottom-sheet/index.d.ts +56 -0
  7. package/dist/bottom-sheet/index.d.ts.map +1 -0
  8. package/dist/bottom-sheet/index.js +462 -0
  9. package/dist/bottom-sheet/index.js.map +1 -0
  10. package/dist/bottom-sheet/native.d.ts +11 -0
  11. package/dist/bottom-sheet/native.d.ts.map +1 -0
  12. package/dist/bottom-sheet/native.js +173 -0
  13. package/dist/bottom-sheet/native.js.map +1 -0
  14. package/dist/drag.js +1 -1
  15. package/dist/drag.js.map +1 -1
  16. package/dist/driver/android.d.ts.map +1 -1
  17. package/dist/driver/android.js +2 -0
  18. package/dist/driver/android.js.map +1 -1
  19. package/dist/driver/ios-scroll-delegate.d.ts +4 -0
  20. package/dist/driver/ios-scroll-delegate.d.ts.map +1 -0
  21. package/dist/driver/ios-scroll-delegate.js +26 -0
  22. package/dist/driver/ios-scroll-delegate.js.map +1 -0
  23. package/dist/driver/ios.d.ts.map +1 -1
  24. package/dist/driver/ios.js +2 -0
  25. package/dist/driver/ios.js.map +1 -1
  26. package/dist/gesture-native.d.ts +28 -0
  27. package/dist/gesture-native.d.ts.map +1 -0
  28. package/dist/gesture-native.js +61 -0
  29. package/dist/gesture-native.js.map +1 -0
  30. package/dist/gestures/index.d.ts +69 -0
  31. package/dist/gestures/index.d.ts.map +1 -0
  32. package/dist/gestures/index.js +262 -0
  33. package/dist/gestures/index.js.map +1 -0
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/live-spring-driver.d.ts +52 -0
  39. package/dist/live-spring-driver.d.ts.map +1 -0
  40. package/dist/live-spring-driver.js +104 -0
  41. package/dist/live-spring-driver.js.map +1 -0
  42. package/dist/mixer.d.ts +6 -0
  43. package/dist/mixer.d.ts.map +1 -1
  44. package/dist/mixer.js +41 -1
  45. package/dist/mixer.js.map +1 -1
  46. package/dist/motion-value.d.ts +34 -2
  47. package/dist/motion-value.d.ts.map +1 -1
  48. package/dist/motion-value.js +277 -10
  49. package/dist/motion-value.js.map +1 -1
  50. package/dist/native/index.d.ts +2 -0
  51. package/dist/native/index.d.ts.map +1 -1
  52. package/dist/native/index.js +1 -0
  53. package/dist/native/index.js.map +1 -1
  54. package/dist/native-property.d.ts +2 -0
  55. package/dist/native-property.d.ts.map +1 -1
  56. package/dist/native-property.js +12 -4
  57. package/dist/native-property.js.map +1 -1
  58. package/dist/reorder/handle.d.ts +6 -0
  59. package/dist/reorder/handle.d.ts.map +1 -0
  60. package/dist/reorder/handle.js +10 -0
  61. package/dist/reorder/handle.js.map +1 -0
  62. package/dist/reorder/index.d.ts +10 -0
  63. package/dist/reorder/index.d.ts.map +1 -0
  64. package/dist/reorder/index.js +233 -0
  65. package/dist/reorder/index.js.map +1 -0
  66. package/dist/reorder/list-view.d.ts +5 -0
  67. package/dist/reorder/list-view.d.ts.map +1 -0
  68. package/dist/reorder/list-view.js +238 -0
  69. package/dist/reorder/list-view.js.map +1 -0
  70. package/dist/reorder/model.d.ts +6 -0
  71. package/dist/reorder/model.d.ts.map +1 -0
  72. package/dist/reorder/model.js +30 -0
  73. package/dist/reorder/model.js.map +1 -0
  74. package/dist/reorder/surface.d.ts +6 -0
  75. package/dist/reorder/surface.d.ts.map +1 -0
  76. package/dist/reorder/surface.js +10 -0
  77. package/dist/reorder/surface.js.map +1 -0
  78. package/dist/reorder/types.d.ts +92 -0
  79. package/dist/reorder/types.d.ts.map +1 -0
  80. package/dist/reorder/types.js +2 -0
  81. package/dist/reorder/types.js.map +1 -0
  82. package/dist/scene-shader.d.ts +18 -0
  83. package/dist/scene-shader.d.ts.map +1 -0
  84. package/dist/scene-shader.js +76 -0
  85. package/dist/scene-shader.js.map +1 -0
  86. package/dist/value-observers.d.ts +3 -1
  87. package/dist/value-observers.d.ts.map +1 -1
  88. package/dist/value-observers.js +10 -2
  89. package/dist/value-observers.js.map +1 -1
  90. package/package.json +13 -1
  91. package/platforms/android/include.gradle +9 -0
  92. package/platforms/android/java/org/rafaelaugusto/jolt/GestureSession.java +198 -0
  93. package/platforms/android/java/org/rafaelaugusto/jolt/GestureSurface.java +53 -0
  94. package/platforms/android/java/org/rafaelaugusto/jolt/LiveSpring.java +270 -0
  95. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderHandle.java +25 -0
  96. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSession.java +213 -0
  97. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSurface.java +27 -0
  98. package/platforms/android/java/org/rafaelaugusto/jolt/SpringState.java +39 -0
  99. package/platforms/android/proguard-rules.pro +5 -0
  100. package/platforms/ios/src/JoltGestureSession.h +18 -0
  101. package/platforms/ios/src/JoltGestureSession.m +222 -0
  102. package/platforms/ios/src/JoltLiveSpring.h +28 -0
  103. package/platforms/ios/src/JoltLiveSpring.m +236 -0
  104. package/platforms/ios/src/JoltReorderSession.h +15 -0
  105. package/platforms/ios/src/JoltReorderSession.m +205 -0
  106. package/platforms/ios/src/JoltSpringState.h +35 -0
  107. package/platforms/ios/src/module.modulemap +3 -0
  108. package/src/android-plugin.d.ts +93 -0
  109. package/src/bottom-sheet/geometry.ts +84 -0
  110. package/src/bottom-sheet/index.ts +536 -0
  111. package/src/bottom-sheet/native.ts +214 -0
  112. package/src/drag.ts +1 -1
  113. package/src/driver/android.ts +2 -0
  114. package/src/driver/ios-scroll-delegate.ts +23 -0
  115. package/src/driver/ios.ts +2 -0
  116. package/src/gesture-native.ts +139 -0
  117. package/src/gestures/index.ts +365 -0
  118. package/src/index.ts +3 -1
  119. package/src/ios-plugin.d.ts +90 -0
  120. package/src/live-spring-driver.ts +181 -0
  121. package/src/mixer.ts +43 -1
  122. package/src/motion-value.ts +318 -11
  123. package/src/native/index.ts +6 -0
  124. package/src/native-property.ts +17 -4
  125. package/src/reorder/handle.ts +9 -0
  126. package/src/reorder/index.ts +243 -0
  127. package/src/reorder/list-view.ts +262 -0
  128. package/src/reorder/model.ts +47 -0
  129. package/src/reorder/surface.ts +10 -0
  130. package/src/reorder/types.ts +87 -0
  131. package/src/scene-shader.ts +139 -0
  132. package/src/value-observers.ts +12 -2
package/src/mixer.ts CHANGED
@@ -2,6 +2,7 @@ import type { View } from '@nativescript/core'
2
2
  import { getDriver } from './driver/index.js'
3
3
  import { readValue, writeValue, type Property } from './properties.js'
4
4
  import type { Track } from './track.js'
5
+ import type { LiveInput } from './live-spring-driver.js'
5
6
 
6
7
  /** The channel of regular animations; layers are named next to it. */
7
8
  export const BASE = ''
@@ -67,6 +68,7 @@ export function mixFor(view: View, property: Property): Mix {
67
68
 
68
69
  /** One property of one view: a base and named layers, played natively as one composite track. */
69
70
  export class Mix {
71
+ private live: { read(): number; layers(inputs: readonly LiveInput[]): void } | undefined
70
72
  private readonly rests = new Map<string, number>()
71
73
  private readonly channels = new Map<string, Channel>()
72
74
  private native: unknown
@@ -80,12 +82,27 @@ export class Mix {
80
82
  }
81
83
 
82
84
  rest(key: string): number {
85
+ if (key === BASE && this.live) return this.live.read()
83
86
  return (
84
87
  this.rests.get(key) ??
85
88
  (key === BASE ? readValue(this.view, this.property) : identity(this.property))
86
89
  )
87
90
  }
88
91
 
92
+ attachLive(base: {
93
+ read(): number
94
+ layers(inputs: readonly LiveInput[]): void
95
+ }): () => void {
96
+ this.live = base
97
+ this.compose()
98
+ return () => {
99
+ if (this.live !== base) return
100
+ this.rests.set(BASE, base.read())
101
+ this.live = undefined
102
+ this.compose()
103
+ }
104
+ }
105
+
89
106
  setRest(key: string, value: number) {
90
107
  this.rests.set(key, value)
91
108
  this.compose()
@@ -192,6 +209,31 @@ export class Mix {
192
209
  const previous = this.native
193
210
  this.native = undefined
194
211
 
212
+ if (this.live) {
213
+ if (previous !== undefined) driver.stop(this.view, previous)
214
+ const layers: LiveInput[] = []
215
+ for (const key of new Set([...this.rests.keys(), ...this.channels.keys()])) {
216
+ if (key === BASE) continue
217
+ const channel = this.channels.get(key)
218
+ const value = this.part(key, now, now)
219
+ const track = channel?.track
220
+ layers.push({
221
+ value,
222
+ running: !!channel && !channel.paused,
223
+ elapsed: this.elapsed(key, now),
224
+ duration: track?.duration ?? 0,
225
+ delay: track?.delay ?? 0,
226
+ repeat: track ? (Number.isFinite(track.repeat) ? track.repeat : -1) : 0,
227
+ reverse: track?.reverse ?? false,
228
+ to: track?.to ?? value,
229
+ times: track?.times ?? [],
230
+ values: track?.values ?? [],
231
+ })
232
+ }
233
+ this.live.layers(layers)
234
+ return
235
+ }
236
+
195
237
  let finite = 0
196
238
  const periods: number[] = []
197
239
  for (const [key, channel] of this.channels) {
@@ -245,7 +287,7 @@ export class Mix {
245
287
  }
246
288
 
247
289
  private dissolve() {
248
- if (this.channels.size) return
290
+ if (this.live || this.channels.size) return
249
291
  for (const [key, value] of this.rests)
250
292
  if (key !== BASE && value !== identity(this.property)) return
251
293
  const properties = mixes.get(this.view)
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Application,
2
3
  Color,
3
4
  ScrollView,
4
5
  type EventData,
@@ -13,7 +14,16 @@ import {
13
14
  validateColorTarget,
14
15
  type ColorValue,
15
16
  } from './color.js'
16
- import { reducedMotion } from './config.js'
17
+ import { onReducedMotion, reducedMotion, timeScale } from './config.js'
18
+ import { springPhysics, type SpringParams } from './spring.js'
19
+ import { mixFor } from './mixer.js'
20
+ import {
21
+ liveSpringDriver,
22
+ syncLiveSprings,
23
+ type LiveNode,
24
+ type LiveMapping,
25
+ type LiveBinding,
26
+ } from './live-spring-driver.js'
17
27
  import {
18
28
  clampDecoration,
19
29
  isDecorationProperty,
@@ -22,7 +32,7 @@ import {
22
32
  import { getDriver } from './driver/index.js'
23
33
  import { setBase, stopBase, ViewAnimation, type Animation, type Plan } from './engine.js'
24
34
  import { linear } from './easing.js'
25
- import { isProperty, type Property } from './properties.js'
35
+ import { isProperty, type NumericProperty, type Property } from './properties.js'
26
36
  import { createTrack, instantTrack, type Track } from './track.js'
27
37
  import {
28
38
  resolveTransition,
@@ -89,6 +99,7 @@ interface Binding {
89
99
  readonly root: Root
90
100
  readonly read: (value: number) => number
91
101
  animation?: ViewAnimation
102
+ cleanup?: () => void
92
103
  sync(): void
93
104
  }
94
105
 
@@ -110,6 +121,7 @@ class Derived {
110
121
  private readonly source: Root | undefined,
111
122
  readonly read: (value: number) => number,
112
123
  readonly color: boolean,
124
+ readonly mappings: readonly LiveMapping[] = [],
113
125
  ) {}
114
126
 
115
127
  get root(): Root {
@@ -132,15 +144,36 @@ class Derived {
132
144
  ): Derived {
133
145
  if (this.color) throw new TypeError('A color cannot be mapped again.')
134
146
  const map = mapping(input, output, options)
147
+ if (this.root instanceof SpringRoot && map.color)
148
+ throw new TypeError(
149
+ 'Live spring bindings currently support numeric transforms and opacity.',
150
+ )
135
151
  const read = this.read
136
- return new Derived(this.root, (value) => map.read(read(value)), map.color)
152
+ return new Derived(
153
+ this.root,
154
+ (value) => map.read(read(value)),
155
+ map.color,
156
+ map.color
157
+ ? []
158
+ : [
159
+ ...this.mappings,
160
+ {
161
+ input: [...map.input],
162
+ output: [...map.numbers],
163
+ clamp: options?.clamp !== false,
164
+ },
165
+ ],
166
+ )
137
167
  }
138
168
  }
139
169
 
140
170
  class Root extends Derived {
141
171
  readonly bindings = new Set<Binding>()
142
172
  readonly listeners = new Set<() => void>()
143
- readonly observers = new ValueObservers((time) => this.raw(time))
173
+ readonly observers = new ValueObservers(
174
+ (time) => this.raw(time),
175
+ () => this.running,
176
+ )
144
177
  private readonly rests = new Map<string, number>()
145
178
  private readonly runs = new Map<string, Run>()
146
179
  private presentation: Presentation | undefined
@@ -219,7 +252,7 @@ class Root extends Derived {
219
252
  }
220
253
 
221
254
  /** Plays a track sampled in the value's own units, such as a drag release. */
222
- play(track: Track): PlaybackAnimation {
255
+ playTrack(track: Track): PlaybackAnimation {
223
256
  this.halt('')
224
257
  return track.end === 0 ? this.jump(track.to, '') : this.start(track, 0, 1, '')
225
258
  }
@@ -435,6 +468,205 @@ class Root extends Derived {
435
468
  }
436
469
  }
437
470
 
471
+ export interface SpringValueOptions extends SpringParams {
472
+ /** Initial follower position. Defaults to the source's current value. */
473
+ initial?: number
474
+ /** Absolute source units. Defaults to 0.001. */
475
+ restDelta?: number
476
+ /** Source units per second. Defaults to 0.01. */
477
+ restSpeed?: number
478
+ }
479
+
480
+ /** A retained native follower. Settling does not dispose its source subscription. */
481
+ export interface SpringValue extends DerivedValue {
482
+ readonly disposed: boolean
483
+ readonly moving: boolean
484
+ readonly paused: boolean
485
+ /** Hold position and velocity while retaining the current source target. */
486
+ pause(): void
487
+ /** Resume from the held state toward the current source. */
488
+ play(): void
489
+ /** Current velocity in source units per second. */
490
+ getVelocity(): number
491
+ dispose(): void
492
+ }
493
+
494
+ class SpringRoot extends Root {
495
+ readonly node: LiveNode
496
+ private releaseSource: () => void = () => {}
497
+ private releaseReduced: () => void = () => {}
498
+ private ended = false
499
+ private held = false
500
+ private background = false
501
+ private suspend = () => {
502
+ this.background = true
503
+ this.updatePause()
504
+ }
505
+ private resume = () => {
506
+ this.background = false
507
+ this.updatePause()
508
+ }
509
+
510
+ constructor(source: Derived, options: SpringValueOptions) {
511
+ const initial = options.initial ?? (source.get() as number)
512
+ const instant = options.duration === 0
513
+ const params = springPhysics(instant ? { ...options, duration: undefined } : options)
514
+ const speed = timeScale()
515
+ const physics = {
516
+ stiffness: params.stiffness * speed * speed,
517
+ damping: params.damping * speed,
518
+ mass: params.mass,
519
+ restDelta: options.restDelta ?? 0.001,
520
+ restSpeed: options.restSpeed ?? 0.01,
521
+ }
522
+ for (const [key, value] of Object.entries(physics))
523
+ if (!Number.isFinite(value) || (key === 'damping' ? value < 0 : value <= 0))
524
+ throw new RangeError(`Invalid springValue ${key}: ${value}.`)
525
+ if (
526
+ options.duration !== undefined &&
527
+ (!Number.isFinite(options.duration) || options.duration < 0)
528
+ )
529
+ throw new RangeError('springValue duration must be finite and nonnegative.')
530
+ if (options.bounce !== undefined && !Number.isFinite(options.bounce))
531
+ throw new RangeError('springValue bounce must be finite.')
532
+ super(initial)
533
+ this.node = liveSpringDriver().create(initial, physics, instant || reducedMotion())
534
+ const root = source.root
535
+ try {
536
+ let update: () => void
537
+ if (root instanceof SpringRoot) {
538
+ if (root.disposed) throw new Error('Cannot follow a disposed spring value.')
539
+ this.node.follow(root.node, source.mappings)
540
+ update = () => this.notify()
541
+ } else {
542
+ const playback = valuePlayback(source as unknown as DerivedValue)
543
+ update = () => {
544
+ if (!root.running) {
545
+ this.node.update({
546
+ value: source.get() as number,
547
+ running: false,
548
+ elapsed: 0,
549
+ duration: 0,
550
+ delay: 0,
551
+ repeat: 0,
552
+ reverse: false,
553
+ to: 0,
554
+ times: [],
555
+ values: [],
556
+ })
557
+ this.notify()
558
+ return
559
+ }
560
+ const { track, elapsed, running, value } = playback.snapshot()
561
+ this.node.update({
562
+ value,
563
+ elapsed,
564
+ running,
565
+ duration: track.duration,
566
+ delay: track.delay,
567
+ repeat: Number.isFinite(track.repeat) ? track.repeat : -1,
568
+ reverse: track.reverse,
569
+ to: track.to,
570
+ times: track.times,
571
+ values: track.values,
572
+ })
573
+ this.notify()
574
+ }
575
+ }
576
+ root.listeners.add(update)
577
+ this.releaseSource = () => {
578
+ root.listeners.delete(update)
579
+ }
580
+ this.releaseReduced = onReducedMotion((reduced) => {
581
+ this.node.reduced(instant || reduced)
582
+ this.notify()
583
+ })
584
+ Application.on(Application.suspendEvent, this.suspend)
585
+ Application.on(Application.resumeEvent, this.resume)
586
+ update()
587
+ } catch (error) {
588
+ this.dispose()
589
+ throw error
590
+ }
591
+ }
592
+ override raw(): number {
593
+ return this.node?.position() ?? super.raw()
594
+ }
595
+ override get running(): boolean {
596
+ return !this.ended && (this.node?.moving() ?? false)
597
+ }
598
+ get moving(): boolean {
599
+ return this.running
600
+ }
601
+ get disposed(): boolean {
602
+ return this.ended
603
+ }
604
+ override get paused(): boolean {
605
+ return this.held
606
+ }
607
+ pause() {
608
+ if (!this.ended) {
609
+ this.held = true
610
+ this.updatePause()
611
+ }
612
+ }
613
+ play() {
614
+ if (!this.ended) {
615
+ this.held = false
616
+ this.updatePause()
617
+ }
618
+ }
619
+ private updatePause() {
620
+ this.node.paused(this.held || this.background)
621
+ this.notify()
622
+ }
623
+ getVelocity(): number {
624
+ return this.ended ? 0 : this.node.velocity()
625
+ }
626
+ override plan(): Plan {
627
+ throw new TypeError(
628
+ 'A live spring cannot be sampled as a fixed track. Bind it to transforms or opacity.',
629
+ )
630
+ }
631
+ notify() {
632
+ this.observers.changed(this.running)
633
+ for (const listener of [...this.listeners]) listener()
634
+ }
635
+ dispose() {
636
+ if (this.ended) return
637
+ this.ended = true
638
+ this.releaseSource()
639
+ this.releaseReduced()
640
+ Application.off(Application.suspendEvent, this.suspend)
641
+ Application.off(Application.resumeEvent, this.resume)
642
+ for (const binding of [...this.bindings]) release(binding)
643
+ this.node.dispose()
644
+ this.observers.clear()
645
+ for (const listener of [...this.listeners]) listener()
646
+ this.listeners.clear()
647
+ }
648
+ }
649
+
650
+ /** Follow changing targets without rebuilding sampled animation tracks. Dispose with the owning scope. */
651
+ export function springValue(
652
+ source: DerivedValue,
653
+ options: SpringValueOptions = {},
654
+ ): SpringValue {
655
+ if (!(source instanceof Derived) || source.color)
656
+ throw new TypeError('springValue() needs a numeric Jolt motion value.')
657
+ if (!options || typeof options !== 'object')
658
+ throw new TypeError('springValue() options must be an object.')
659
+ return new SpringRoot(source, options) as unknown as SpringValue
660
+ }
661
+
662
+ /** Internal boundary notification for values driven directly by native input. */
663
+ export function liveValueInput(value: DerivedValue) {
664
+ if (!(value instanceof Derived) || !(value.root instanceof SpringRoot))
665
+ throw new TypeError('Native input requires a live value.')
666
+ const root = value.root
667
+ return { node: root.node, changed: () => root.notify() }
668
+ }
669
+
438
670
  function layerName(name: string): string {
439
671
  if (typeof name !== 'string' || !name.length)
440
672
  throw new TypeError('A layer is named by a nonempty string.')
@@ -462,7 +694,7 @@ export function motionValue(initial = 0): MotionValue {
462
694
  }
463
695
 
464
696
  export function isMotionValue(target: unknown): target is MotionValue {
465
- return target instanceof Root
697
+ return target instanceof Root && !(target instanceof SpringRoot)
466
698
  }
467
699
 
468
700
  export function asRoot(value: MotionValue): Root {
@@ -490,6 +722,13 @@ export function bind(target: Target | readonly Target[], bindings: Bindings): ()
490
722
  key === 'scale' ? ['scaleX', 'scaleY'] : isProperty(key) ? [key] : []
491
723
  if (!properties.length) throw new Error(`Can't bind "${key}".`)
492
724
  for (const property of properties) {
725
+ if (source.root instanceof SpringRoot) {
726
+ if (source.root.disposed) throw new Error('Cannot bind a disposed spring value.')
727
+ if (isColorProperty(property) || isDecorationProperty(property))
728
+ throw new TypeError(
729
+ 'Live spring bindings currently support numeric transforms and opacity.',
730
+ )
731
+ }
493
732
  if (isColorProperty(property) !== source.color)
494
733
  throw new TypeError(
495
734
  source.color
@@ -504,9 +743,14 @@ export function bind(target: Target | readonly Target[], bindings: Bindings): ()
504
743
  if (isColorProperty(property)) validateColorTarget(view, property)
505
744
  else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
506
745
 
507
- const made = views.flatMap((view) =>
508
- entries.map(([property, source]) => attach(view, property, source)),
509
- )
746
+ const made: (() => void)[] = []
747
+ try {
748
+ for (const view of views)
749
+ for (const [property, source] of entries) made.push(attach(view, property, source))
750
+ } catch (error) {
751
+ for (const cleanup of made.reverse()) cleanup()
752
+ throw error
753
+ }
510
754
  return () => made.forEach((release) => release())
511
755
  }
512
756
 
@@ -514,6 +758,8 @@ function attach(view: View, property: Property, source: Derived) {
514
758
  const existing = bound.get(view)?.get(property)
515
759
  if (existing) release(existing)
516
760
  const root = source.root
761
+ if (root instanceof SpringRoot)
762
+ return attachSpring(view, property as NumericProperty, source, root)
517
763
  const binding: Binding = {
518
764
  view,
519
765
  property,
@@ -551,7 +797,12 @@ function attach(view: View, property: Property, source: Derived) {
551
797
  properties.set(property, binding)
552
798
  root.bindings.add(binding)
553
799
  view.on('loaded', binding.sync)
554
- binding.sync()
800
+ try {
801
+ binding.sync()
802
+ } catch (error) {
803
+ release(binding)
804
+ throw error
805
+ }
555
806
  return () => {
556
807
  if (root.bindings.has(binding)) {
557
808
  release(binding)
@@ -560,6 +811,61 @@ function attach(view: View, property: Property, source: Derived) {
560
811
  }
561
812
  }
562
813
 
814
+ function attachSpring(
815
+ view: View,
816
+ property: NumericProperty,
817
+ source: Derived,
818
+ root: SpringRoot,
819
+ ) {
820
+ let native: LiveBinding | undefined
821
+ let releaseMix: (() => void) | undefined
822
+ const detach = () => {
823
+ if (!native) return
824
+ native.cancel()
825
+ releaseMix?.()
826
+ releaseMix = undefined
827
+ native = undefined
828
+ syncLiveSprings(view)
829
+ }
830
+ const binding: Binding = {
831
+ view,
832
+ property,
833
+ root,
834
+ read: source.read,
835
+ sync() {
836
+ if (!view.isLoaded || !view.nativeViewProtected || root.disposed) return
837
+ detach()
838
+ stopBase(view, [property])
839
+ native = root.node.bind(view, property, source.mappings)
840
+ const handle = native
841
+ releaseMix = mixFor(view, property).attachLive({
842
+ read: () => handle.value(),
843
+ layers: (layers) => handle.layers(layers),
844
+ })
845
+ syncLiveSprings(view)
846
+ },
847
+ cleanup() {
848
+ view.off('unloaded', detach)
849
+ detach()
850
+ },
851
+ }
852
+ let properties = bound.get(view)
853
+ if (!properties) bound.set(view, (properties = new Map()))
854
+ properties.set(property, binding)
855
+ root.bindings.add(binding)
856
+ view.on('loaded', binding.sync)
857
+ view.on('unloaded', detach)
858
+ try {
859
+ binding.sync()
860
+ } catch (error) {
861
+ release(binding)
862
+ throw error
863
+ }
864
+ return () => {
865
+ if (root.bindings.has(binding)) release(binding)
866
+ }
867
+ }
868
+
563
869
  /** A read-only value on an existing finite animation's clock. No second animation or timer. */
564
870
  export function playbackProgress(animation: Animation): DerivedValue {
565
871
  const source = playbackSource(animation)
@@ -714,6 +1020,7 @@ function release(binding: Binding) {
714
1020
  binding.view.off('loaded', binding.sync)
715
1021
  const properties = bound.get(binding.view)
716
1022
  if (properties?.get(binding.property) === binding) properties.delete(binding.property)
1023
+ binding.cleanup?.()
717
1024
  }
718
1025
 
719
1026
  /** A regular animation on a bound property takes it over; the value keeps its other bindings. */
@@ -775,7 +1082,7 @@ function mapping(
775
1082
  if (mixers) return mixers[i]!(Math.min(1, Math.max(0, t)))
776
1083
  return numbers[i]! + (numbers[i + 1]! - numbers[i]!) * t
777
1084
  }
778
- return { read, color }
1085
+ return { read, color, input, numbers }
779
1086
  }
780
1087
 
781
1088
  function springOf(name: SpringName): Transition {
@@ -22,3 +22,9 @@ export {
22
22
  sceneKitMaterialDiffuse,
23
23
  sceneKitMaterialEmission,
24
24
  } from '../scene-appearance.js'
25
+ export { sceneKitEnvironmentIntensity, sceneKitUniform } from '../scene-shader.js'
26
+ export type {
27
+ SceneUniformType,
28
+ SceneUniformValue,
29
+ SceneUniformOptions,
30
+ } from '../scene-shader.js'
@@ -56,10 +56,16 @@ const owners = new WeakMap<object, Map<string, Owner>>()
56
56
  export function registerNativeProperty<T>(
57
57
  definition: NativePropertyDefinition<T>,
58
58
  ): (target: object) => NativeProperty<T> {
59
- if (!/^[\w.-]+\/[\w.-]+$/.test(definition.name))
60
- throw new TypeError('Native property names must be namespaced: plugin/property.')
59
+ validateDefinition(definition)
61
60
  if (definitions.has(definition.name))
62
61
  throw new Error(`Native property "${definition.name}" is already registered.`)
62
+ definitions.add(definition.name)
63
+ return (target) => new PropertyTarget(target, definition)
64
+ }
65
+
66
+ function validateDefinition<T>(definition: NativePropertyDefinition<T>) {
67
+ if (!/^[\w.-]+\/[\w.-]+$/.test(definition.name))
68
+ throw new TypeError('Native property names must be namespaced: plugin/property.')
63
69
  if (
64
70
  !definition.units.length ||
65
71
  definition.units.some((unit) => typeof unit !== 'string' || !unit)
@@ -76,8 +82,15 @@ export function registerNativeProperty<T>(
76
82
  typeof definition.decode !== 'function'
77
83
  )
78
84
  throw new TypeError('A native property needs create(), encode() and decode().')
79
- definitions.add(definition.name)
80
- return (target) => new PropertyTarget(target, definition)
85
+ }
86
+
87
+ /** Internal factory for per-target schemas; identity still shares the registry ownership map. */
88
+ export function nativePropertyTarget<T>(
89
+ target: object,
90
+ definition: NativePropertyDefinition<T>,
91
+ ): NativeProperty<T> {
92
+ validateDefinition(definition)
93
+ return new PropertyTarget(target, definition)
81
94
  }
82
95
 
83
96
  class PropertyTarget<T> implements NativeProperty<T> {
@@ -0,0 +1,9 @@
1
+ import { Button, isIOS } from '@nativescript/core'
2
+
3
+ /** Focusable native handle: Alt/Option + ↑/↓ moves its current item. */
4
+ export class ReorderHandle extends Button {
5
+ override createNativeView(): ReturnType<Button['createNativeView']> {
6
+ if (isIOS) return JoltReorderHandle.new()
7
+ return new org.rafaelaugusto.jolt.ReorderHandle(this._context)
8
+ }
9
+ }