@rafaelaugusto/jolt 0.1.0-beta.14 → 0.1.0-beta.16

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 (91) hide show
  1. package/README.md +11 -4
  2. package/dist/drag.js +1 -1
  3. package/dist/drag.js.map +1 -1
  4. package/dist/driver/android.d.ts.map +1 -1
  5. package/dist/driver/android.js +2 -0
  6. package/dist/driver/android.js.map +1 -1
  7. package/dist/driver/ios-scroll-delegate.d.ts +4 -0
  8. package/dist/driver/ios-scroll-delegate.d.ts.map +1 -0
  9. package/dist/driver/ios-scroll-delegate.js +26 -0
  10. package/dist/driver/ios-scroll-delegate.js.map +1 -0
  11. package/dist/driver/ios.d.ts.map +1 -1
  12. package/dist/driver/ios.js +2 -0
  13. package/dist/driver/ios.js.map +1 -1
  14. package/dist/gesture-native.d.ts +27 -0
  15. package/dist/gesture-native.d.ts.map +1 -0
  16. package/dist/gesture-native.js +55 -0
  17. package/dist/gesture-native.js.map +1 -0
  18. package/dist/gestures/index.d.ts +65 -0
  19. package/dist/gestures/index.d.ts.map +1 -0
  20. package/dist/gestures/index.js +251 -0
  21. package/dist/gestures/index.js.map +1 -0
  22. package/dist/index.d.ts +4 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/live-spring-driver.d.ts +52 -0
  27. package/dist/live-spring-driver.d.ts.map +1 -0
  28. package/dist/live-spring-driver.js +104 -0
  29. package/dist/live-spring-driver.js.map +1 -0
  30. package/dist/mixer.d.ts +6 -0
  31. package/dist/mixer.d.ts.map +1 -1
  32. package/dist/mixer.js +41 -1
  33. package/dist/mixer.js.map +1 -1
  34. package/dist/motion-scope.d.ts +2 -0
  35. package/dist/motion-scope.d.ts.map +1 -1
  36. package/dist/motion-scope.js +6 -0
  37. package/dist/motion-scope.js.map +1 -1
  38. package/dist/motion-value.d.ts +34 -2
  39. package/dist/motion-value.d.ts.map +1 -1
  40. package/dist/motion-value.js +277 -10
  41. package/dist/motion-value.js.map +1 -1
  42. package/dist/native/index.d.ts +1 -0
  43. package/dist/native/index.d.ts.map +1 -1
  44. package/dist/native/index.js +1 -0
  45. package/dist/native/index.js.map +1 -1
  46. package/dist/scene-camera.d.ts +5 -0
  47. package/dist/scene-camera.d.ts.map +1 -0
  48. package/dist/scene-camera.js +23 -0
  49. package/dist/scene-camera.js.map +1 -0
  50. package/dist/size-layout.d.ts +19 -0
  51. package/dist/size-layout.d.ts.map +1 -0
  52. package/dist/size-layout.js +216 -0
  53. package/dist/size-layout.js.map +1 -0
  54. package/dist/size.d.ts +19 -0
  55. package/dist/size.d.ts.map +1 -0
  56. package/dist/size.js +251 -0
  57. package/dist/size.js.map +1 -0
  58. package/dist/value-observers.d.ts +3 -1
  59. package/dist/value-observers.d.ts.map +1 -1
  60. package/dist/value-observers.js +10 -2
  61. package/dist/value-observers.js.map +1 -1
  62. package/package.json +5 -1
  63. package/platforms/android/include.gradle +9 -0
  64. package/platforms/android/java/org/rafaelaugusto/jolt/GestureSession.java +193 -0
  65. package/platforms/android/java/org/rafaelaugusto/jolt/GestureSurface.java +53 -0
  66. package/platforms/android/java/org/rafaelaugusto/jolt/LiveSpring.java +270 -0
  67. package/platforms/android/java/org/rafaelaugusto/jolt/SpringState.java +39 -0
  68. package/platforms/ios/src/JoltGestureSession.h +17 -0
  69. package/platforms/ios/src/JoltGestureSession.m +217 -0
  70. package/platforms/ios/src/JoltLiveSpring.h +28 -0
  71. package/platforms/ios/src/JoltLiveSpring.m +236 -0
  72. package/platforms/ios/src/JoltSpringState.h +35 -0
  73. package/platforms/ios/src/module.modulemap +2 -0
  74. package/src/android-plugin.d.ts +59 -0
  75. package/src/drag.ts +1 -1
  76. package/src/driver/android.ts +2 -0
  77. package/src/driver/ios-scroll-delegate.ts +23 -0
  78. package/src/driver/ios.ts +2 -0
  79. package/src/gesture-native.ts +124 -0
  80. package/src/gestures/index.ts +350 -0
  81. package/src/index.ts +5 -1
  82. package/src/ios-plugin.d.ts +61 -0
  83. package/src/live-spring-driver.ts +181 -0
  84. package/src/mixer.ts +43 -1
  85. package/src/motion-scope.ts +14 -0
  86. package/src/motion-value.ts +318 -11
  87. package/src/native/index.ts +1 -0
  88. package/src/scene-camera.ts +30 -0
  89. package/src/size-layout.ts +237 -0
  90. package/src/size.ts +302 -0
  91. package/src/value-observers.ts +12 -2
@@ -3,6 +3,7 @@ import { press } from './press.js'
3
3
  import { drag } from './drag.js'
4
4
  import { gradient } from './gradient.js'
5
5
  import { animateLayout, type LayoutOptions } from './layout.js'
6
+ import { animateSize, type SizeDimensions, type SizeOptions } from './size.js'
6
7
  import type { SpringName } from './transition.js'
7
8
  import { sequence, timeline } from './sequence.js'
8
9
  import {
@@ -35,6 +36,7 @@ export interface MotionScope {
35
36
  /** Owned handles/subscriptions, including suspended subscription factories. */
36
37
  readonly size: number
37
38
  animateLayout: typeof animateLayout
39
+ animateSize: typeof animateSize
38
40
  animate: typeof animate
39
41
  decay: typeof decay
40
42
  sequence: typeof sequence
@@ -195,6 +197,18 @@ export function motionScope(): MotionScope {
195
197
  },
196
198
  animate: ((...args: Parameters<typeof animate>) =>
197
199
  startOwned(() => animate(...args))) as typeof animate,
200
+ animateSize(
201
+ target: Target,
202
+ dimensions: SizeDimensions,
203
+ changeOrOptions?: (() => void) | SizeOptions | SpringName,
204
+ options?: SizeOptions | SpringName,
205
+ ) {
206
+ return startOwned(() =>
207
+ typeof changeOrOptions === 'function'
208
+ ? animateSize(target, dimensions, changeOrOptions, options)
209
+ : animateSize(target, dimensions, changeOrOptions),
210
+ )
211
+ },
198
212
  decay(...args) {
199
213
  return startOwned(() => decay(...args))
200
214
  },
@@ -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 {
@@ -1,6 +1,7 @@
1
1
  export { registerNativeProperty, checkNativeProperty } from '../native-property.js'
2
2
  export type { NativeProperty, NativePropertyDefinition } from '../native-property.js'
3
3
  export type { NativeSink } from '../native-property-driver.js'
4
+ export { sceneKitFieldOfView } from '../scene-camera.js'
4
5
  export {
5
6
  mapKitCoordinate,
6
7
  mapKitCamera,
@@ -0,0 +1,30 @@
1
+ /// <reference types="@nativescript/types-ios/lib/ios/objc-x86_64/objc!SceneKit" preserve="true" />
2
+ import { registerNativeProperty, type NativeProperty } from './native-property.js'
3
+
4
+ /** Perspective SCNCamera field of view in degrees (1..179), on its projectionDirection axis. */
5
+ export const sceneKitFieldOfView: (camera: SCNCamera) => NativeProperty<number> =
6
+ registerNativeProperty<number>({
7
+ name: 'jolt.scenekit/field-of-view',
8
+ units: ['degrees on the camera projectionDirection axis'],
9
+ platforms: ['ios'],
10
+ spatial: true,
11
+ encode: (value) => [value],
12
+ decode: (values) => values[0]!,
13
+ validate(value) {
14
+ if (!Number.isFinite(value) || value < 1 || value > 179)
15
+ throw new RangeError(
16
+ 'SceneKit field of view needs a finite angle in 1..179 degrees.',
17
+ )
18
+ },
19
+ interpolate: (a, b, t) => Math.max(1, Math.min(179, a + (b - a) * t)),
20
+ create(target) {
21
+ if (
22
+ typeof JoltReferenceProperties === 'undefined' ||
23
+ typeof JoltReferenceProperties.sceneFieldOfView !== 'function'
24
+ )
25
+ throw new Error(
26
+ 'SceneKit field of view requires an updated @rafaelaugusto/jolt-native plugin. Update both packages and rebuild the native app.',
27
+ )
28
+ return JoltReferenceProperties.sceneFieldOfView(target as NSObject)
29
+ },
30
+ })