@rafaelaugusto/jolt 0.1.0-beta.11 → 0.1.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +42 -8
  2. package/dist/animate.d.ts +15 -3
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +52 -34
  5. package/dist/animate.js.map +1 -1
  6. package/dist/drag.d.ts +17 -3
  7. package/dist/drag.d.ts.map +1 -1
  8. package/dist/drag.js +34 -11
  9. package/dist/drag.js.map +1 -1
  10. package/dist/driver/android.d.ts.map +1 -1
  11. package/dist/driver/android.js +2 -0
  12. package/dist/driver/android.js.map +1 -1
  13. package/dist/driver/frame.d.ts +3 -0
  14. package/dist/driver/frame.d.ts.map +1 -0
  15. package/dist/driver/frame.js +7 -0
  16. package/dist/driver/frame.js.map +1 -0
  17. package/dist/driver/index.d.ts +2 -0
  18. package/dist/driver/index.d.ts.map +1 -1
  19. package/dist/driver/index.js.map +1 -1
  20. package/dist/driver/ios.d.ts.map +1 -1
  21. package/dist/driver/ios.js +2 -0
  22. package/dist/driver/ios.js.map +1 -1
  23. package/dist/easing.d.ts +1 -1
  24. package/dist/easing.d.ts.map +1 -1
  25. package/dist/easing.js +11 -0
  26. package/dist/easing.js.map +1 -1
  27. package/dist/engine.d.ts +20 -2
  28. package/dist/engine.d.ts.map +1 -1
  29. package/dist/engine.js +166 -38
  30. package/dist/engine.js.map +1 -1
  31. package/dist/gradient.d.ts.map +1 -1
  32. package/dist/gradient.js +10 -4
  33. package/dist/gradient.js.map +1 -1
  34. package/dist/image-effect.d.ts +1 -0
  35. package/dist/image-effect.d.ts.map +1 -1
  36. package/dist/image-effect.js +10 -2
  37. package/dist/image-effect.js.map +1 -1
  38. package/dist/index.d.ts +8 -2
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/keyboard.d.ts.map +1 -1
  43. package/dist/keyboard.js +63 -10
  44. package/dist/keyboard.js.map +1 -1
  45. package/dist/mixer.d.ts +49 -0
  46. package/dist/mixer.d.ts.map +1 -0
  47. package/dist/mixer.js +238 -0
  48. package/dist/mixer.js.map +1 -0
  49. package/dist/motion-scope.d.ts +6 -2
  50. package/dist/motion-scope.d.ts.map +1 -1
  51. package/dist/motion-scope.js +12 -4
  52. package/dist/motion-scope.js.map +1 -1
  53. package/dist/motion-states.d.ts +10 -5
  54. package/dist/motion-states.d.ts.map +1 -1
  55. package/dist/motion-states.js +55 -22
  56. package/dist/motion-states.js.map +1 -1
  57. package/dist/motion-value.d.ts +152 -0
  58. package/dist/motion-value.d.ts.map +1 -0
  59. package/dist/motion-value.js +635 -0
  60. package/dist/motion-value.js.map +1 -0
  61. package/dist/native/index.d.ts +6 -0
  62. package/dist/native/index.d.ts.map +1 -0
  63. package/dist/native/index.js +3 -0
  64. package/dist/native/index.js.map +1 -0
  65. package/dist/native-properties.d.ts +28 -0
  66. package/dist/native-properties.d.ts.map +1 -0
  67. package/dist/native-properties.js +154 -0
  68. package/dist/native-properties.js.map +1 -0
  69. package/dist/native-property-driver.d.ts +19 -0
  70. package/dist/native-property-driver.d.ts.map +1 -0
  71. package/dist/native-property-driver.js +69 -0
  72. package/dist/native-property-driver.js.map +1 -0
  73. package/dist/native-property.d.ts +35 -0
  74. package/dist/native-property.d.ts.map +1 -0
  75. package/dist/native-property.js +277 -0
  76. package/dist/native-property.js.map +1 -0
  77. package/dist/path.d.ts.map +1 -1
  78. package/dist/path.js +2 -2
  79. package/dist/path.js.map +1 -1
  80. package/dist/playback-group.d.ts.map +1 -1
  81. package/dist/playback-group.js +17 -1
  82. package/dist/playback-group.js.map +1 -1
  83. package/dist/playback.d.ts +15 -0
  84. package/dist/playback.d.ts.map +1 -1
  85. package/dist/playback.js +23 -0
  86. package/dist/playback.js.map +1 -1
  87. package/dist/sequence.d.ts +24 -0
  88. package/dist/sequence.d.ts.map +1 -1
  89. package/dist/sequence.js +181 -19
  90. package/dist/sequence.js.map +1 -1
  91. package/dist/timed-animation.d.ts +38 -0
  92. package/dist/timed-animation.d.ts.map +1 -0
  93. package/dist/timed-animation.js +177 -0
  94. package/dist/timed-animation.js.map +1 -0
  95. package/dist/track.d.ts.map +1 -1
  96. package/dist/track.js +31 -12
  97. package/dist/track.js.map +1 -1
  98. package/dist/transition.d.ts +15 -4
  99. package/dist/transition.d.ts.map +1 -1
  100. package/dist/transition.js +46 -7
  101. package/dist/transition.js.map +1 -1
  102. package/dist/types.d.ts +5 -0
  103. package/dist/types.d.ts.map +1 -1
  104. package/dist/value-observers.d.ts +19 -0
  105. package/dist/value-observers.d.ts.map +1 -0
  106. package/dist/value-observers.js +111 -0
  107. package/dist/value-observers.js.map +1 -0
  108. package/dist/value-track.d.ts +13 -0
  109. package/dist/value-track.d.ts.map +1 -0
  110. package/dist/value-track.js +59 -0
  111. package/dist/value-track.js.map +1 -0
  112. package/dist/vue/index.d.ts +28 -2
  113. package/dist/vue/index.d.ts.map +1 -1
  114. package/dist/vue/index.js +78 -2
  115. package/dist/vue/index.js.map +1 -1
  116. package/package.json +6 -2
  117. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrame.java +53 -0
  118. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrameFollower.java +57 -0
  119. package/src/android-plugin.d.ts +64 -0
  120. package/src/animate.ts +106 -17
  121. package/src/drag.ts +67 -21
  122. package/src/driver/android.ts +2 -0
  123. package/src/driver/frame.ts +10 -0
  124. package/src/driver/index.ts +2 -0
  125. package/src/driver/ios.ts +2 -0
  126. package/src/easing.ts +14 -0
  127. package/src/engine.ts +181 -55
  128. package/src/gradient.ts +10 -4
  129. package/src/image-effect.ts +10 -2
  130. package/src/index.ts +23 -1
  131. package/src/ios-plugin.d.ts +33 -0
  132. package/src/keyboard.ts +83 -18
  133. package/src/mixer.ts +264 -0
  134. package/src/motion-scope.ts +17 -5
  135. package/src/motion-states.ts +79 -29
  136. package/src/motion-value.ts +795 -0
  137. package/src/native/index.ts +16 -0
  138. package/src/native-properties.ts +188 -0
  139. package/src/native-property-driver.ts +123 -0
  140. package/src/native-property.ts +353 -0
  141. package/src/path.ts +2 -1
  142. package/src/playback-group.ts +17 -1
  143. package/src/playback.ts +41 -0
  144. package/src/sequence.ts +209 -20
  145. package/src/timed-animation.ts +192 -0
  146. package/src/track.ts +37 -12
  147. package/src/transition.ts +69 -10
  148. package/src/types.ts +5 -0
  149. package/src/value-observers.ts +133 -0
  150. package/src/value-track.ts +71 -0
  151. package/src/vue/index.ts +98 -1
@@ -0,0 +1,16 @@
1
+ export { registerNativeProperty, checkNativeProperty } from '../native-property.js'
2
+ export type { NativeProperty, NativePropertyDefinition } from '../native-property.js'
3
+ export type { NativeSink } from '../native-property-driver.js'
4
+ export {
5
+ mapKitCoordinate,
6
+ mapKitCamera,
7
+ mapLibreCoordinate,
8
+ mapLibreCamera,
9
+ sceneKitPose,
10
+ } from '../native-properties.js'
11
+ export type {
12
+ Coordinate,
13
+ MapKitCamera,
14
+ MapLibreCamera,
15
+ ScenePose,
16
+ } from '../native-properties.js'
@@ -0,0 +1,188 @@
1
+ import { registerNativeProperty } from './native-property.js'
2
+
3
+ export interface Coordinate {
4
+ latitude: number
5
+ longitude: number
6
+ }
7
+ export interface MapKitCamera extends Coordinate {
8
+ altitude: number
9
+ pitch: number
10
+ heading: number
11
+ }
12
+ export interface MapLibreCamera extends Coordinate {
13
+ zoom: number
14
+ tilt: number
15
+ bearing: number
16
+ }
17
+ export interface ScenePose {
18
+ position: readonly [number, number, number]
19
+ orientation: readonly [number, number, number, number]
20
+ }
21
+
22
+ /** Shortest longitude/heading interpolation. Coordinates use a local linear path, not a geodesic. */
23
+ export function shortestAngle(from: number, to: number, fraction: number) {
24
+ const delta = ((((to - from + 180) % 360) + 360) % 360) - 180
25
+ return from + delta * fraction
26
+ }
27
+ function validateCoordinate(value: Coordinate) {
28
+ if (
29
+ !Number.isFinite(value.latitude) ||
30
+ Math.abs(value.latitude) > 90 ||
31
+ !Number.isFinite(value.longitude)
32
+ )
33
+ throw new RangeError('Coordinates need latitude in -90..90 and a finite longitude.')
34
+ }
35
+ function coordinate(from: Coordinate, to: Coordinate, t: number): Coordinate {
36
+ return {
37
+ latitude: Math.max(
38
+ -90,
39
+ Math.min(90, from.latitude + (to.latitude - from.latitude) * t),
40
+ ),
41
+ longitude: shortestAngle(from.longitude, to.longitude, t),
42
+ }
43
+ }
44
+
45
+ export const mapKitCoordinate = registerNativeProperty<Coordinate>({
46
+ name: 'jolt.mapkit/coordinate',
47
+ units: ['latitude degrees', 'longitude degrees'],
48
+ platforms: ['ios'],
49
+ spatial: true,
50
+ encode: (v) => [v.latitude, v.longitude],
51
+ decode: (v) => ({ latitude: v[0]!, longitude: v[1]! }),
52
+ validate: validateCoordinate,
53
+ interpolate: coordinate,
54
+ create: (target) => JoltReferenceProperties.mapCoordinate(target as NSObject),
55
+ })
56
+ export const mapKitCamera = registerNativeProperty<MapKitCamera>({
57
+ name: 'jolt.mapkit/camera',
58
+ units: [
59
+ 'latitude degrees',
60
+ 'longitude degrees',
61
+ 'altitude meters',
62
+ 'pitch degrees',
63
+ 'heading degrees',
64
+ ],
65
+ platforms: ['ios'],
66
+ spatial: true,
67
+ encode: (v) => [v.latitude, v.longitude, v.altitude, v.pitch, v.heading],
68
+ decode: (v) => ({
69
+ latitude: v[0]!,
70
+ longitude: v[1]!,
71
+ altitude: v[2]!,
72
+ pitch: v[3]!,
73
+ heading: v[4]!,
74
+ }),
75
+ validate(v) {
76
+ validateCoordinate(v)
77
+ if (v.altitude < 1 || v.pitch < 0 || v.pitch > 80)
78
+ throw new RangeError('MapKit camera needs altitude >= 1m and pitch 0..80 degrees.')
79
+ },
80
+ interpolate: (a, b, t) => ({
81
+ ...coordinate(a, b, t),
82
+ altitude: Math.max(1, a.altitude + (b.altitude - a.altitude) * t),
83
+ pitch: Math.max(0, Math.min(80, a.pitch + (b.pitch - a.pitch) * t)),
84
+ heading: shortestAngle(a.heading, b.heading, t),
85
+ }),
86
+ create: (target) => JoltReferenceProperties.mapCamera(target as NSObject),
87
+ })
88
+ export const mapLibreCoordinate = registerNativeProperty<Coordinate>({
89
+ name: 'jolt.maplibre/coordinate',
90
+ units: ['latitude degrees', 'longitude degrees'],
91
+ platforms: ['android'],
92
+ spatial: true,
93
+ encode: (v) => [v.latitude, v.longitude],
94
+ decode: (v) => ({ latitude: v[0]!, longitude: v[1]! }),
95
+ validate: validateCoordinate,
96
+ interpolate: coordinate,
97
+ create: (target) => new org.rafaelaugusto.jolt.MapLibrePropertySink(target, false),
98
+ })
99
+ export const mapLibreCamera = registerNativeProperty<MapLibreCamera>({
100
+ name: 'jolt.maplibre/camera',
101
+ units: [
102
+ 'latitude degrees',
103
+ 'longitude degrees',
104
+ 'zoom level',
105
+ 'tilt degrees',
106
+ 'bearing degrees',
107
+ ],
108
+ platforms: ['android'],
109
+ spatial: true,
110
+ encode: (v) => [v.latitude, v.longitude, v.zoom, v.tilt, v.bearing],
111
+ decode: (v) => ({
112
+ latitude: v[0]!,
113
+ longitude: v[1]!,
114
+ zoom: v[2]!,
115
+ tilt: v[3]!,
116
+ bearing: v[4]!,
117
+ }),
118
+ validate(v) {
119
+ validateCoordinate(v)
120
+ if (v.zoom < 0 || v.zoom > 24 || v.tilt < 0 || v.tilt > 60)
121
+ throw new RangeError('MapLibre camera needs zoom 0..24 and tilt 0..60 degrees.')
122
+ },
123
+ interpolate: (a, b, t) => ({
124
+ ...coordinate(a, b, t),
125
+ zoom: Math.max(0, Math.min(24, a.zoom + (b.zoom - a.zoom) * t)),
126
+ tilt: Math.max(0, Math.min(60, a.tilt + (b.tilt - a.tilt) * t)),
127
+ bearing: shortestAngle(a.bearing, b.bearing, t),
128
+ }),
129
+ create: (target) => new org.rafaelaugusto.jolt.MapLibrePropertySink(target, true),
130
+ })
131
+
132
+ /** Unit quaternion slerp; q and -q describe the same orientation. */
133
+ export function interpolatePose(a: ScenePose, b: ScenePose, t: number): ScenePose {
134
+ const normalize = (q: readonly number[]) => {
135
+ const length = Math.hypot(...q)
136
+ if (!Number.isFinite(length) || length < 1e-12)
137
+ throw new RangeError('Orientation must be a nonzero finite quaternion.')
138
+ return q.map((v) => v / length)
139
+ }
140
+ const from = normalize(a.orientation),
141
+ to = normalize(b.orientation)
142
+ let dot = from.reduce((sum, v, i) => sum + v * to[i]!, 0)
143
+ if (dot < 0) {
144
+ dot = -dot
145
+ for (let i = 0; i < 4; i++) to[i] = -to[i]!
146
+ }
147
+ const theta = Math.acos(Math.min(1, dot)),
148
+ sine = Math.sin(theta)
149
+ const x = sine < 1e-6 ? 1 - t : Math.sin((1 - t) * theta) / sine,
150
+ y = sine < 1e-6 ? t : Math.sin(t * theta) / sine
151
+ return {
152
+ position: a.position.map(
153
+ (v, i) => v + (b.position[i]! - v) * t,
154
+ ) as unknown as ScenePose['position'],
155
+ orientation: normalize(
156
+ from.map((v, i) => v * x + to[i]! * y),
157
+ ) as unknown as ScenePose['orientation'],
158
+ }
159
+ }
160
+ export const sceneKitPose = registerNativeProperty<ScenePose>({
161
+ name: 'jolt.scenekit/pose',
162
+ units: [
163
+ 'x scene units',
164
+ 'y scene units',
165
+ 'z scene units',
166
+ 'quaternion x',
167
+ 'quaternion y',
168
+ 'quaternion z',
169
+ 'quaternion w',
170
+ ],
171
+ platforms: ['ios'],
172
+ spatial: true,
173
+ encode: (v) => [...v.position, ...v.orientation],
174
+ decode: (v) => ({
175
+ position: [v[0]!, v[1]!, v[2]!],
176
+ orientation: [v[3]!, v[4]!, v[5]!, v[6]!],
177
+ }),
178
+ validate(v) {
179
+ if (
180
+ v.position.length !== 3 ||
181
+ v.orientation.length !== 4 ||
182
+ Math.hypot(...v.orientation) < 1e-12
183
+ )
184
+ throw new RangeError('Scene pose needs a 3D position and a nonzero quaternion.')
185
+ },
186
+ interpolate: interpolatePose,
187
+ create: (target) => JoltReferenceProperties.scenePose(target as NSObject),
188
+ })
@@ -0,0 +1,123 @@
1
+ import { isAndroid } from '@nativescript/core'
2
+ import type { Track } from './track.js'
3
+
4
+ /** Opaque compiled JoltPropertySink / PropertySink instance. */
5
+ export type NativeSink = object
6
+ export interface NativePlayer {
7
+ control(time: number, paused: boolean): void
8
+ cancel(): void
9
+ }
10
+ export interface NativePropertyDriver {
11
+ validate(sink: NativeSink, dimensions: number): void
12
+ alive(sink: NativeSink): boolean
13
+ read(sink: NativeSink): number[]
14
+ write(sink: NativeSink, values: readonly number[]): void
15
+ dispose(sink: NativeSink): void
16
+ play(
17
+ sink: NativeSink,
18
+ track: Track,
19
+ samples: readonly (readonly number[])[],
20
+ final: readonly number[],
21
+ end: (completed: boolean) => void,
22
+ ): NativePlayer
23
+ }
24
+ let override: NativePropertyDriver | undefined
25
+ /** Test injection; not part of the public package entry. */
26
+ export function setNativePropertyDriver(driver: NativePropertyDriver | undefined) {
27
+ override = driver
28
+ }
29
+ export function nativePropertyDriver(): NativePropertyDriver {
30
+ if (override) return override
31
+ const available = isAndroid
32
+ ? typeof org !== 'undefined' &&
33
+ typeof org.rafaelaugusto.jolt.PropertySink?.scalar === 'function'
34
+ : typeof JoltPropertySink !== 'undefined'
35
+ if (!available)
36
+ throw new Error(
37
+ 'Native property adapters require the optional @rafaelaugusto/jolt-native plugin. Install it and rebuild the native app.',
38
+ )
39
+ return isAndroid ? androidDriver : iosDriver
40
+ }
41
+
42
+ function array(values: readonly number[]) {
43
+ const result = Array.create('double', values.length)
44
+ values.forEach((value, i) => {
45
+ result[i] = value
46
+ })
47
+ return result
48
+ }
49
+ const androidDriver: NativePropertyDriver = {
50
+ validate(sink, dimensions) {
51
+ if (
52
+ !(sink instanceof org.rafaelaugusto.jolt.PropertySink) ||
53
+ sink.getDimensions() !== dimensions ||
54
+ !sink.isCompiled()
55
+ )
56
+ throw new TypeError(
57
+ 'Native adapters require a compiled PropertySink with matching dimensions, not a JavaScript setter.',
58
+ )
59
+ },
60
+ alive: (sink) => (sink as org.rafaelaugusto.jolt.PropertySink).isAlive(),
61
+ read(sink) {
62
+ const values = (sink as org.rafaelaugusto.jolt.PropertySink).read()
63
+ return Array.from({ length: values.length }, (_, i) => values[i]!)
64
+ },
65
+ write: (sink, values) =>
66
+ (sink as org.rafaelaugusto.jolt.PropertySink).write(array(values)),
67
+ dispose: (sink) => (sink as org.rafaelaugusto.jolt.PropertySink).dispose(),
68
+ play(sink, track, samples, final, end) {
69
+ const player = new org.rafaelaugusto.jolt.PropertyPlayer(
70
+ sink as org.rafaelaugusto.jolt.PropertySink,
71
+ array(track.times),
72
+ array(samples.flat()),
73
+ array(final),
74
+ track.duration,
75
+ track.delay,
76
+ Number.isFinite(track.repeat) ? track.repeat : -1,
77
+ track.reverse,
78
+ new org.rafaelaugusto.jolt.PropertyPlayer.Listener({ onEnd: end }),
79
+ )
80
+ return {
81
+ control: (time, paused) => player.control(time, paused),
82
+ cancel: () => player.cancel(),
83
+ }
84
+ },
85
+ }
86
+ const iosDriver: NativePropertyDriver = {
87
+ validate(sink, dimensions) {
88
+ if (
89
+ !(sink instanceof JoltPropertySink) ||
90
+ sink.dimensions !== dimensions ||
91
+ !sink.compiled
92
+ )
93
+ throw new TypeError(
94
+ 'Native adapters require a compiled JoltPropertySink with matching dimensions, not a JavaScript setter.',
95
+ )
96
+ },
97
+ alive: (sink) => (sink as JoltPropertySink).alive,
98
+ read(sink) {
99
+ const values = (sink as JoltPropertySink).read()
100
+ return Array.from({ length: values.count }, (_, i) => values.objectAtIndex(i))
101
+ },
102
+ write: (sink, values) =>
103
+ (sink as JoltPropertySink).write(NSArray.arrayWithArray([...values])),
104
+ dispose: (sink) => (sink as JoltPropertySink).dispose(),
105
+ play(sink, track, samples, final, end) {
106
+ const player =
107
+ JoltPropertyPlayer.startSinkTimesValuesFinalValueDurationDelayRepeatReverseCompletion(
108
+ sink as JoltPropertySink,
109
+ NSArray.arrayWithArray([...track.times]),
110
+ NSArray.arrayWithArray(samples.flat()),
111
+ NSArray.arrayWithArray([...final]),
112
+ track.duration,
113
+ track.delay,
114
+ track.repeat,
115
+ track.reverse,
116
+ end,
117
+ )
118
+ return {
119
+ control: (time, paused) => player.controlPaused(time, paused),
120
+ cancel: () => player.cancel(),
121
+ }
122
+ },
123
+ }
@@ -0,0 +1,353 @@
1
+ import { isAndroid, Trace } from '@nativescript/core'
2
+ import { reducedMotion } from './config.js'
3
+ import type { DerivedValue } from './motion-value.js'
4
+ import { valuePlayback } from './motion-value.js'
5
+ import type { PlaybackAnimation } from './playback.js'
6
+ import { TimedAnimation } from './timed-animation.js'
7
+ import { createTrack, instantTrack, type Track } from './track.js'
8
+ import {
9
+ resolveTransition,
10
+ springs,
11
+ type Transition,
12
+ type SpringName,
13
+ } from './transition.js'
14
+ import {
15
+ nativePropertyDriver,
16
+ type NativeSink,
17
+ type NativePlayer,
18
+ } from './native-property-driver.js'
19
+
20
+ export interface NativePropertyDefinition<T> {
21
+ /** Namespaced identity, e.g. 'acme.chart/reveal'. One registration per name. */
22
+ readonly name: string
23
+ readonly units: readonly string[]
24
+ readonly platforms: readonly ('ios' | 'android')[]
25
+ /** Spatial properties jump to their destination under reduced motion. */
26
+ readonly spatial?: boolean
27
+ encode(value: T): readonly number[]
28
+ decode(value: readonly number[]): T
29
+ /** Called once per sampled keyframe in JS, never per native frame. */
30
+ interpolate?(from: T, to: T, fraction: number): T
31
+ validate?(value: T): void
32
+ /** Must return a compiled native sink, not a JavaScript implementation of its methods. */
33
+ create(target: object): NativeSink
34
+ }
35
+
36
+ export interface NativeProperty<T> {
37
+ readonly name: string
38
+ get(): T
39
+ set(value: T): void
40
+ animate(to: T, options?: Transition | SpringName): PlaybackAnimation
41
+ /** Sample a pure mapping at setup/boundaries, then write the property entirely in native code. */
42
+ bind(value: DerivedValue, map: (value: number) => T): () => void
43
+ stop(): void
44
+ /** Release this owner's native resources. Call before destroying the SDK target. */
45
+ dispose(): void
46
+ }
47
+
48
+ interface Owner {
49
+ stop(): void
50
+ velocity(): readonly number[]
51
+ }
52
+ const definitions = new Set<string>()
53
+ const owners = new WeakMap<object, Map<string, Owner>>()
54
+
55
+ /** Optional plugin registry. No SDK is loaded until its adapter creates a target. */
56
+ export function registerNativeProperty<T>(
57
+ definition: NativePropertyDefinition<T>,
58
+ ): (target: object) => NativeProperty<T> {
59
+ if (!/^[\w.-]+\/[\w.-]+$/.test(definition.name))
60
+ throw new TypeError('Native property names must be namespaced: plugin/property.')
61
+ if (definitions.has(definition.name))
62
+ throw new Error(`Native property "${definition.name}" is already registered.`)
63
+ if (
64
+ !definition.units.length ||
65
+ definition.units.some((unit) => typeof unit !== 'string' || !unit)
66
+ )
67
+ throw new TypeError('Describe the units of every native property component.')
68
+ if (
69
+ !definition.platforms.length ||
70
+ definition.platforms.some((p) => p !== 'ios' && p !== 'android')
71
+ )
72
+ throw new TypeError('Declare supported native platforms.')
73
+ if (
74
+ typeof definition.create !== 'function' ||
75
+ typeof definition.encode !== 'function' ||
76
+ typeof definition.decode !== 'function'
77
+ )
78
+ throw new TypeError('A native property needs create(), encode() and decode().')
79
+ definitions.add(definition.name)
80
+ return (target) => new PropertyTarget(target, definition)
81
+ }
82
+
83
+ class PropertyTarget<T> implements NativeProperty<T> {
84
+ private readonly sink: NativeSink
85
+ private readonly owned = new Set<Owner>()
86
+ private disposed = false
87
+ readonly name: string
88
+ constructor(
89
+ private target: object | undefined,
90
+ private readonly definition: NativePropertyDefinition<T>,
91
+ ) {
92
+ this.name = definition.name
93
+ if (!target || (typeof target !== 'object' && typeof target !== 'function'))
94
+ throw new TypeError('A native property needs a native target.')
95
+ const platform = isAndroid ? 'android' : 'ios'
96
+ if (!definition.platforms.includes(platform))
97
+ throw new Error(`${this.name} is unavailable on ${platform}.`)
98
+ const driver = nativePropertyDriver()
99
+ this.sink = definition.create(target)
100
+ try {
101
+ driver.validate(this.sink, definition.units.length)
102
+ } catch (error) {
103
+ try {
104
+ driver.dispose(this.sink)
105
+ } catch {}
106
+ throw error
107
+ }
108
+ }
109
+ private alive() {
110
+ if (this.disposed) throw new Error(`${this.name} has been disposed.`)
111
+ if (!nativePropertyDriver().alive(this.sink))
112
+ throw new Error(`${this.name} has lost its native target.`)
113
+ }
114
+ private vector(value: T): readonly number[] {
115
+ this.definition.validate?.(value)
116
+ const numbers = [...this.definition.encode(value)]
117
+ if (
118
+ numbers.length !== this.definition.units.length ||
119
+ numbers.some((n) => !Number.isFinite(n))
120
+ )
121
+ throw new TypeError(
122
+ `${this.name} expects ${this.definition.units.length} finite components.`,
123
+ )
124
+ return numbers
125
+ }
126
+ get(): T {
127
+ this.alive()
128
+ return this.definition.decode(nativePropertyDriver().read(this.sink))
129
+ }
130
+ set(value: T) {
131
+ this.alive()
132
+ const vector = this.vector(value)
133
+ this.owner()?.stop()
134
+ nativePropertyDriver().write(this.sink, vector)
135
+ }
136
+ private owner() {
137
+ return this.target ? owners.get(this.target)?.get(this.name) : undefined
138
+ }
139
+ private claim(owner: Owner) {
140
+ this.owner()?.stop()
141
+ let map = owners.get(this.target!)
142
+ if (!map) owners.set(this.target!, (map = new Map()))
143
+ map.set(this.name, owner)
144
+ this.owned.add(owner)
145
+ }
146
+ private release(owner: Owner) {
147
+ if (this.target && this.owner() === owner) owners.get(this.target)?.delete(this.name)
148
+ this.owned.delete(owner)
149
+ }
150
+ animate(to: T, options: Transition | SpringName = {}): PlaybackAnimation {
151
+ this.alive()
152
+ const from = this.get(),
153
+ first = this.vector(from),
154
+ last = this.vector(to)
155
+ if (typeof options === 'object' && 'layer' in options)
156
+ throw new TypeError(
157
+ 'Native property adapters do not support layers; bind a layered scalar value instead.',
158
+ )
159
+ if (typeof options === 'string' && !Object.hasOwn(springs, options))
160
+ throw new TypeError(`Unknown spring "${options}".`)
161
+ const transition = resolveTransition(
162
+ typeof options === 'string' ? springs[options] : options,
163
+ 'opacity',
164
+ 2,
165
+ )
166
+ const velocity = this.owner()?.velocity()
167
+ const delta = last.map((v, i) => v - first[i]!)
168
+ const length = delta.reduce((s, v) => s + v * v, 0)
169
+ const speed =
170
+ length && velocity
171
+ ? delta.reduce((s, v, i) => s + v * (velocity[i] ?? 0), 0) / length
172
+ : 0
173
+ const track = dense(
174
+ this.definition.spatial && reducedMotion()
175
+ ? instantTrack('opacity', 1)
176
+ : createTrack('opacity', [0, 1], transition, speed),
177
+ )
178
+ const at = (fraction: number) =>
179
+ this.vector(
180
+ this.definition.interpolate
181
+ ? this.definition.interpolate(from, to, fraction)
182
+ : this.definition.decode(first.map((v, i) => v + delta[i]! * fraction)),
183
+ )
184
+ const samples = track.values.map(at)
185
+ const final = at(track.to)
186
+ let player: NativePlayer | undefined
187
+ const animation = new TimedAnimation(
188
+ track.end,
189
+ track.arrival,
190
+ (time, paused) => {
191
+ player?.control(time, paused)
192
+ },
193
+ (completed) => {
194
+ try {
195
+ if (completed && nativePropertyDriver().alive(this.sink))
196
+ nativePropertyDriver().write(this.sink, final)
197
+ } finally {
198
+ player?.cancel()
199
+ player = undefined
200
+ this.release(owner)
201
+ }
202
+ },
203
+ )
204
+ const owner: Owner = {
205
+ stop: () => animation.stop(),
206
+ velocity: () =>
207
+ animation.paused
208
+ ? first.map(() => 0)
209
+ : at(track.valueAt(animation.time + 0.5)).map(
210
+ (v, i) => (v - at(track.valueAt(animation.time - 0.5))[i]!) * 1000,
211
+ ),
212
+ }
213
+ this.claim(owner)
214
+ try {
215
+ if (track.end > 0) {
216
+ player = nativePropertyDriver().play(
217
+ this.sink,
218
+ track,
219
+ samples,
220
+ final,
221
+ (completed) => {
222
+ if (!completed) animation.stop()
223
+ },
224
+ )
225
+ if (animation.done) {
226
+ player.cancel()
227
+ player = undefined
228
+ } else player.control(animation.time, false)
229
+ }
230
+ animation.start()
231
+ } catch (error) {
232
+ animation.stop()
233
+ throw error
234
+ }
235
+ return animation
236
+ }
237
+ bind(value: DerivedValue, map: (value: number) => T): () => void {
238
+ this.alive()
239
+ if (typeof map !== 'function')
240
+ throw new TypeError('A native binding needs a pure value mapping.')
241
+ const source = valuePlayback(value)
242
+ let player: NativePlayer | undefined
243
+ let off = () => {}
244
+ let stopped = false
245
+ const prepare = () => {
246
+ const state = source.snapshot()
247
+ state.track = dense(state.track)
248
+ const spatial = this.definition.spatial && reducedMotion()
249
+ const samples = state.track.values.map((v) => this.vector(map(v)))
250
+ const current = this.vector(map(spatial ? state.track.to : state.value))
251
+ const final = this.vector(map(state.track.to))
252
+ return { ...state, samples, current, final, running: state.running && !spatial }
253
+ }
254
+ const initial = prepare() // Validate all samples before interrupting another owner.
255
+ const owner: Owner = {
256
+ stop: () => {
257
+ if (stopped) return
258
+ stopped = true
259
+ off()
260
+ player?.cancel()
261
+ player = undefined
262
+ this.release(owner)
263
+ },
264
+ velocity: () => this.definition.units.map(() => 0),
265
+ }
266
+ const apply = (state: ReturnType<typeof prepare>) => {
267
+ player?.cancel()
268
+ player = undefined
269
+ if (!state.running) nativePropertyDriver().write(this.sink, state.current)
270
+ else {
271
+ player = nativePropertyDriver().play(
272
+ this.sink,
273
+ state.track,
274
+ state.samples,
275
+ state.final,
276
+ (completed) => {
277
+ if (!completed) owner.stop()
278
+ },
279
+ )
280
+ if (stopped) {
281
+ player.cancel()
282
+ player = undefined
283
+ } else player.control(source.snapshot().elapsed, state.paused)
284
+ }
285
+ }
286
+ this.claim(owner)
287
+ try {
288
+ off = source.subscribe(() => {
289
+ if (stopped) return
290
+ try {
291
+ this.alive()
292
+ apply(prepare())
293
+ } catch (error) {
294
+ owner.stop()
295
+ Trace.write(
296
+ `Jolt native binding failed: ${String(error)}`,
297
+ Trace.categories.Error,
298
+ Trace.messageType.error,
299
+ )
300
+ }
301
+ })
302
+ apply(initial)
303
+ } catch (error) {
304
+ owner.stop()
305
+ throw error
306
+ }
307
+ return owner.stop
308
+ }
309
+ stop() {
310
+ for (const owner of [...this.owned]) owner.stop()
311
+ }
312
+ dispose() {
313
+ if (this.disposed) return
314
+ this.stop()
315
+ this.disposed = true
316
+ nativePropertyDriver().dispose(this.sink)
317
+ this.target = undefined
318
+ }
319
+ }
320
+
321
+ /** Reusable conformance checks for setup/command behavior; frame/lifecycle checks require a native app. */
322
+ export function checkNativeProperty<T>(property: NativeProperty<T>, value: T) {
323
+ const previous = property.get()
324
+ try {
325
+ property.set(value)
326
+ return property.get()
327
+ } finally {
328
+ property.set(previous)
329
+ }
330
+ }
331
+
332
+ /** Uniform samples also capture nonlinear compound interpolation and pure binding maps. */
333
+ function dense(track: Track): Track {
334
+ if (track.duration === 0) return track
335
+ const count = Math.max(1, Math.min(6000, Math.ceil((track.duration * 60) / 1000)))
336
+ const times = [
337
+ ...new Set([
338
+ ...track.times,
339
+ ...Array.from({ length: count + 1 }, (_, i) => i / count),
340
+ ]),
341
+ ].sort((a, b) => a - b)
342
+ return {
343
+ ...track,
344
+ times,
345
+ values: times.map((time) =>
346
+ time === 1
347
+ ? track.values.at(-1)!
348
+ : time === 0
349
+ ? track.values[0]!
350
+ : track.valueAt(track.delay + time * track.duration),
351
+ ),
352
+ }
353
+ }
package/src/path.ts CHANGED
@@ -2,6 +2,7 @@ import { parsePath, type MotionPath } from './svg-path.js'
2
2
  import { reducedMotion, timeScale } from './config.js'
3
3
  import { createTrack, instantTrack, type Track } from './track.js'
4
4
  import {
5
+ fitTransition,
5
6
  mergeTransitions,
6
7
  resolveTransition,
7
8
  type Transition,
@@ -37,7 +38,7 @@ export function preparePath(
37
38
  const geometry = parsePath(keyframes.path)
38
39
  const orient = keyframes.orient === true
39
40
  const transition = resolveTransition(
40
- overrides.path ? mergeTransitions(base, overrides.path) : base,
41
+ fitTransition(overrides.path ? mergeTransitions(base, overrides.path) : base, 2),
41
42
  'opacity',
42
43
  2,
43
44
  )