@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.
- package/README.md +42 -8
- package/dist/animate.d.ts +15 -3
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +52 -34
- package/dist/animate.js.map +1 -1
- package/dist/drag.d.ts +17 -3
- package/dist/drag.d.ts.map +1 -1
- package/dist/drag.js +34 -11
- package/dist/drag.js.map +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +2 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/frame.d.ts +3 -0
- package/dist/driver/frame.d.ts.map +1 -0
- package/dist/driver/frame.js +7 -0
- package/dist/driver/frame.js.map +1 -0
- package/dist/driver/index.d.ts +2 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +2 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/easing.d.ts +1 -1
- package/dist/easing.d.ts.map +1 -1
- package/dist/easing.js +11 -0
- package/dist/easing.js.map +1 -1
- package/dist/engine.d.ts +20 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +166 -38
- package/dist/engine.js.map +1 -1
- package/dist/gradient.d.ts.map +1 -1
- package/dist/gradient.js +10 -4
- package/dist/gradient.js.map +1 -1
- package/dist/image-effect.d.ts +1 -0
- package/dist/image-effect.d.ts.map +1 -1
- package/dist/image-effect.js +10 -2
- package/dist/image-effect.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +63 -10
- package/dist/keyboard.js.map +1 -1
- package/dist/mixer.d.ts +49 -0
- package/dist/mixer.d.ts.map +1 -0
- package/dist/mixer.js +238 -0
- package/dist/mixer.js.map +1 -0
- package/dist/motion-scope.d.ts +6 -2
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +12 -4
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-states.d.ts +10 -5
- package/dist/motion-states.d.ts.map +1 -1
- package/dist/motion-states.js +55 -22
- package/dist/motion-states.js.map +1 -1
- package/dist/motion-value.d.ts +152 -0
- package/dist/motion-value.d.ts.map +1 -0
- package/dist/motion-value.js +635 -0
- package/dist/motion-value.js.map +1 -0
- package/dist/native/index.d.ts +6 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +3 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native-properties.d.ts +28 -0
- package/dist/native-properties.d.ts.map +1 -0
- package/dist/native-properties.js +154 -0
- package/dist/native-properties.js.map +1 -0
- package/dist/native-property-driver.d.ts +19 -0
- package/dist/native-property-driver.d.ts.map +1 -0
- package/dist/native-property-driver.js +69 -0
- package/dist/native-property-driver.js.map +1 -0
- package/dist/native-property.d.ts +35 -0
- package/dist/native-property.d.ts.map +1 -0
- package/dist/native-property.js +277 -0
- package/dist/native-property.js.map +1 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +2 -2
- package/dist/path.js.map +1 -1
- package/dist/playback-group.d.ts.map +1 -1
- package/dist/playback-group.js +17 -1
- package/dist/playback-group.js.map +1 -1
- package/dist/playback.d.ts +15 -0
- package/dist/playback.d.ts.map +1 -1
- package/dist/playback.js +23 -0
- package/dist/playback.js.map +1 -1
- package/dist/sequence.d.ts +24 -0
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +181 -19
- package/dist/sequence.js.map +1 -1
- package/dist/timed-animation.d.ts +38 -0
- package/dist/timed-animation.d.ts.map +1 -0
- package/dist/timed-animation.js +177 -0
- package/dist/timed-animation.js.map +1 -0
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +31 -12
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +15 -4
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +46 -7
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/value-observers.d.ts +19 -0
- package/dist/value-observers.d.ts.map +1 -0
- package/dist/value-observers.js +111 -0
- package/dist/value-observers.js.map +1 -0
- package/dist/value-track.d.ts +13 -0
- package/dist/value-track.d.ts.map +1 -0
- package/dist/value-track.js +59 -0
- package/dist/value-track.js.map +1 -0
- package/dist/vue/index.d.ts +28 -2
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +78 -2
- package/dist/vue/index.js.map +1 -1
- package/package.json +6 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrame.java +53 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFrameFollower.java +57 -0
- package/src/android-plugin.d.ts +64 -0
- package/src/animate.ts +106 -17
- package/src/drag.ts +67 -21
- package/src/driver/android.ts +2 -0
- package/src/driver/frame.ts +10 -0
- package/src/driver/index.ts +2 -0
- package/src/driver/ios.ts +2 -0
- package/src/easing.ts +14 -0
- package/src/engine.ts +181 -55
- package/src/gradient.ts +10 -4
- package/src/image-effect.ts +10 -2
- package/src/index.ts +23 -1
- package/src/ios-plugin.d.ts +33 -0
- package/src/keyboard.ts +83 -18
- package/src/mixer.ts +264 -0
- package/src/motion-scope.ts +17 -5
- package/src/motion-states.ts +79 -29
- package/src/motion-value.ts +795 -0
- package/src/native/index.ts +16 -0
- package/src/native-properties.ts +188 -0
- package/src/native-property-driver.ts +123 -0
- package/src/native-property.ts +353 -0
- package/src/path.ts +2 -1
- package/src/playback-group.ts +17 -1
- package/src/playback.ts +41 -0
- package/src/sequence.ts +209 -20
- package/src/timed-animation.ts +192 -0
- package/src/track.ts +37 -12
- package/src/transition.ts +69 -10
- package/src/types.ts +5 -0
- package/src/value-observers.ts +133 -0
- package/src/value-track.ts +71 -0
- 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
|
)
|