@rafaelaugusto/jolt-native 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +12 -2
- package/index.js +2 -0
- package/package.json +32 -4
- package/platforms/android/include.gradle +6 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/MapLibrePropertySink.java +60 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/PropertyPlayer.java +85 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/PropertySink.java +48 -0
- package/platforms/android/proguard-rules.pro +13 -0
- package/platforms/ios/build.xcconfig +1 -0
- package/platforms/ios/src/JoltPropertyPlayer.h +28 -0
- package/platforms/ios/src/JoltPropertyPlayer.m +130 -0
- package/platforms/ios/src/JoltReferenceProperties.h +8 -0
- package/platforms/ios/src/JoltReferenceProperties.m +63 -0
- package/platforms/ios/src/module.modulemap +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rafael Augusto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Jolt native property runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Optional NativeScript plugin for `@rafaelaugusto/jolt/native`. Install this package alongside Jolt and rebuild the app. Core Jolt does not depend on this plugin.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @rafaelaugusto/jolt @rafaelaugusto/jolt-native
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Includes compiled scalar/compound playback, MapKit and SceneKit sinks on iOS, and a MapLibre sink on Android. It does not install MapLibre; the app chooses its SDK version. Reference demo: MapLibre Android 11.8.0, NativeScript 9.1, iOS 26 / Android 16. Minimum NativeScript 8.9; native helper APIs require iOS 13+ / Android API 23+ (the app/runtime or SDK may require newer).
|
|
10
|
+
|
|
11
|
+
See the [native properties guide](https://jolt.rafaelaugusto.dev/guide/native-properties) for capabilities, authoring, ownership and execution limits.
|
|
12
|
+
|
|
13
|
+
Release maintainers: publish this optional package together with compatible core releases (`npm publish` from this directory). Build and pack the core at the repository root. The demo uses local file dependencies and never needs the package published for validation.
|
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rafaelaugusto/jolt-native",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-beta.13",
|
|
4
|
+
"description": "Optional compiled property adapters for Jolt (MapKit, MapLibre and SceneKit).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"platforms/ios",
|
|
11
|
+
"platforms/android/java",
|
|
12
|
+
"platforms/android/include.gradle",
|
|
13
|
+
"platforms/android/proguard-rules.pro"
|
|
14
|
+
],
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@rafaelaugusto/jolt": ">=0.1.0-beta.13",
|
|
18
|
+
"@nativescript/core": ">=8.9.0"
|
|
19
|
+
},
|
|
20
|
+
"nativescript": {
|
|
21
|
+
"platforms": {
|
|
22
|
+
"ios": "8.9.0",
|
|
23
|
+
"android": "8.9.0"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/rafaelaugustos/jolt.git",
|
|
32
|
+
"directory": "packages/native"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import java.lang.reflect.Constructor;
|
|
4
|
+
import java.lang.reflect.Field;
|
|
5
|
+
import java.lang.reflect.Method;
|
|
6
|
+
|
|
7
|
+
/** Optional MapLibre integration: cached reflection avoids a core dependency on the SDK. */
|
|
8
|
+
public final class MapLibrePropertySink extends PropertySink {
|
|
9
|
+
private final boolean camera;
|
|
10
|
+
private final Constructor<?> coordinate, builder;
|
|
11
|
+
private final Method read, write, build, targetMethod, zoom, tilt, bearing, cameraUpdate;
|
|
12
|
+
private final Method latitude, longitude;
|
|
13
|
+
private final Field cameraTarget, cameraZoom, cameraTilt, cameraBearing;
|
|
14
|
+
public MapLibrePropertySink(Object target, boolean camera) {
|
|
15
|
+
super(target,camera?5:2); this.camera=camera;
|
|
16
|
+
try {
|
|
17
|
+
Class<?> latLng=Class.forName("org.maplibre.android.geometry.LatLng");
|
|
18
|
+
coordinate=latLng.getConstructor(double.class,double.class);
|
|
19
|
+
latitude=latLng.getMethod("getLatitude"); longitude=latLng.getMethod("getLongitude");
|
|
20
|
+
if(camera) {
|
|
21
|
+
Class<?> position=Class.forName("org.maplibre.android.camera.CameraPosition");
|
|
22
|
+
Class<?> b=Class.forName("org.maplibre.android.camera.CameraPosition$Builder");
|
|
23
|
+
Class<?> update=Class.forName("org.maplibre.android.camera.CameraUpdate");
|
|
24
|
+
builder=b.getConstructor(); build=b.getMethod("build");
|
|
25
|
+
targetMethod=b.getMethod("target",latLng); zoom=b.getMethod("zoom",double.class);
|
|
26
|
+
tilt=b.getMethod("tilt",double.class); bearing=b.getMethod("bearing",double.class);
|
|
27
|
+
cameraUpdate=Class.forName("org.maplibre.android.camera.CameraUpdateFactory").getMethod("newCameraPosition",position);
|
|
28
|
+
read=target.getClass().getMethod("getCameraPosition"); write=target.getClass().getMethod("moveCamera",update);
|
|
29
|
+
cameraTarget=position.getField("target"); cameraZoom=position.getField("zoom");
|
|
30
|
+
cameraTilt=position.getField("tilt"); cameraBearing=position.getField("bearing");
|
|
31
|
+
} else {
|
|
32
|
+
read=target.getClass().getMethod("getPosition"); write=target.getClass().getMethod("setPosition",latLng);
|
|
33
|
+
builder=null; build=targetMethod=zoom=tilt=bearing=cameraUpdate=null;
|
|
34
|
+
cameraTarget=cameraZoom=cameraTilt=cameraBearing=null;
|
|
35
|
+
}
|
|
36
|
+
} catch(ReflectiveOperationException e) { throw new IllegalArgumentException("MapLibre adapter needs a compatible MapLibreMap or Marker",e); }
|
|
37
|
+
}
|
|
38
|
+
public double[] read() {
|
|
39
|
+
Object object=target.get(); if(object==null) return new double[getDimensions()];
|
|
40
|
+
try {
|
|
41
|
+
Object value=read.invoke(object), p=camera?cameraTarget.get(value):value;
|
|
42
|
+
double lat=((Number)latitude.invoke(p)).doubleValue(), lon=((Number)longitude.invoke(p)).doubleValue();
|
|
43
|
+
return camera?new double[]{lat,lon,cameraZoom.getDouble(value),cameraTilt.getDouble(value),cameraBearing.getDouble(value)}:new double[]{lat,lon};
|
|
44
|
+
} catch(ReflectiveOperationException e) { throw new IllegalStateException(e); }
|
|
45
|
+
}
|
|
46
|
+
public void write(double[] values) {
|
|
47
|
+
Object object=target.get(); if(object==null) return;
|
|
48
|
+
try {
|
|
49
|
+
double lat=Math.max(-85.05112878,Math.min(85.05112878,values[0]));
|
|
50
|
+
double lon=((values[1]+180)%360+360)%360-180;
|
|
51
|
+
Object p=coordinate.newInstance(lat,lon);
|
|
52
|
+
if(!camera) write.invoke(object,p);
|
|
53
|
+
else {
|
|
54
|
+
Object b=builder.newInstance(); targetMethod.invoke(b,p);
|
|
55
|
+
zoom.invoke(b,Math.max(0,Math.min(24,values[2]))); tilt.invoke(b,Math.max(0,Math.min(60,values[3])));
|
|
56
|
+
bearing.invoke(b,values[4]); write.invoke(object,cameraUpdate.invoke(null,build.invoke(b)));
|
|
57
|
+
}
|
|
58
|
+
} catch(ReflectiveOperationException e) { throw new IllegalStateException(e); }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import android.animation.Animator;
|
|
4
|
+
import android.animation.AnimatorListenerAdapter;
|
|
5
|
+
import android.animation.ValueAnimator;
|
|
6
|
+
import android.view.animation.LinearInterpolator;
|
|
7
|
+
|
|
8
|
+
/** Native sampled compound-property playback. No JS animation-update listener. */
|
|
9
|
+
public final class PropertyPlayer {
|
|
10
|
+
public interface Listener { void onEnd(boolean completed); }
|
|
11
|
+
private PropertySink sink;
|
|
12
|
+
private Listener listener;
|
|
13
|
+
private final double[] times, values, finalValue;
|
|
14
|
+
private final double duration, delay;
|
|
15
|
+
private final int repeats, dimensions;
|
|
16
|
+
private final boolean reverse;
|
|
17
|
+
private ValueAnimator animator;
|
|
18
|
+
private boolean suppressed, stopped, counted;
|
|
19
|
+
private long frames;
|
|
20
|
+
private static int active;
|
|
21
|
+
|
|
22
|
+
public PropertyPlayer(PropertySink sink, double[] times, double[] values, double[] finalValue,
|
|
23
|
+
double duration, double delay, double repeat, boolean reverse, Listener listener) {
|
|
24
|
+
if (!sink.isCompiled() || times.length<2 || values.length!=times.length*sink.getDimensions())
|
|
25
|
+
throw new IllegalArgumentException("Expected compiled sink and matching keyframes");
|
|
26
|
+
this.sink=sink; this.times=times.clone(); this.values=values.clone(); this.finalValue=finalValue.clone();
|
|
27
|
+
this.duration=Math.max(1,duration); this.delay=delay; this.repeats=repeat<0?-1:(int)repeat;
|
|
28
|
+
this.reverse=reverse; this.listener=listener; this.dimensions=sink.getDimensions();
|
|
29
|
+
control(0,false);
|
|
30
|
+
}
|
|
31
|
+
public static int activeCount() { return active; }
|
|
32
|
+
public long getFrames() { return frames; }
|
|
33
|
+
private void count(boolean running) {
|
|
34
|
+
if(counted==running) return;
|
|
35
|
+
counted=running; active+=running?1:-1;
|
|
36
|
+
}
|
|
37
|
+
private void apply(double progress) {
|
|
38
|
+
if(stopped) return;
|
|
39
|
+
if(!sink.isAlive()) { end(false); return; }
|
|
40
|
+
int low=0, high=times.length-1;
|
|
41
|
+
while(low+1<high) { int mid=(low+high)/2; if(times[mid]<=progress) low=mid; else high=mid; }
|
|
42
|
+
double span=times[high]-times[low], t=span>0?Math.max(0,Math.min(1,(progress-times[low])/span)):1;
|
|
43
|
+
double[] result=new double[dimensions];
|
|
44
|
+
for(int i=0;i<dimensions;i++) result[i]=values[low*dimensions+i]+(values[high*dimensions+i]-values[low*dimensions+i])*t;
|
|
45
|
+
write(result);
|
|
46
|
+
}
|
|
47
|
+
private void write(double[] value) {
|
|
48
|
+
try { if(sink!=null && sink.isAlive()) { sink.write(value); frames++; } else end(false); }
|
|
49
|
+
catch(RuntimeException e) { android.util.Log.e("Jolt","Native property write failed",e); end(false); }
|
|
50
|
+
}
|
|
51
|
+
private void end(boolean completed) {
|
|
52
|
+
count(false);
|
|
53
|
+
if(listener!=null && !stopped) listener.onEnd(completed);
|
|
54
|
+
if(!completed) cancel();
|
|
55
|
+
}
|
|
56
|
+
public void control(double time, boolean paused) {
|
|
57
|
+
if(stopped) return;
|
|
58
|
+
suppressed=true;
|
|
59
|
+
if(animator!=null) { animator.removeAllUpdateListeners(); animator.removeAllListeners(); animator.cancel(); }
|
|
60
|
+
animator=ValueAnimator.ofFloat(0,1);
|
|
61
|
+
animator.setInterpolator(new LinearInterpolator()); animator.setDuration((long)duration);
|
|
62
|
+
animator.setStartDelay((long)Math.max(0,delay-time)); animator.setRepeatCount(repeats);
|
|
63
|
+
animator.setRepeatMode(reverse?ValueAnimator.REVERSE:ValueAnimator.RESTART);
|
|
64
|
+
animator.addUpdateListener(value -> { if(!suppressed) apply(((Number)value.getAnimatedValue()).doubleValue()); });
|
|
65
|
+
animator.addListener(new AnimatorListenerAdapter() {
|
|
66
|
+
@Override public void onAnimationEnd(Animator animation) {
|
|
67
|
+
if(!suppressed && !stopped) { write(finalValue); end(true); }
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
animator.start();
|
|
71
|
+
if(paused) animator.pause();
|
|
72
|
+
if(time>=delay) animator.setCurrentPlayTime((long)Math.max(0,time-delay));
|
|
73
|
+
suppressed=false;
|
|
74
|
+
double local=Math.max(0,time-delay), iteration=Math.floor(local/duration), progress=(local%duration)/duration;
|
|
75
|
+
if(reverse && iteration%2>=1) progress=1-progress;
|
|
76
|
+
if(repeats>=0 && time>=delay+duration*(repeats+1)) write(finalValue); else apply(progress);
|
|
77
|
+
count(!paused && !stopped && (repeats<0 || time<delay+duration*(repeats+1)));
|
|
78
|
+
}
|
|
79
|
+
public void cancel() {
|
|
80
|
+
if(stopped) return;
|
|
81
|
+
stopped=true; listener=null; count(false);
|
|
82
|
+
if(animator!=null) { animator.removeAllUpdateListeners(); animator.removeAllListeners(); animator.cancel(); animator=null; }
|
|
83
|
+
sink=null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import java.lang.ref.WeakReference;
|
|
4
|
+
import java.lang.reflect.Method;
|
|
5
|
+
|
|
6
|
+
/** Implement in compiled Java/Kotlin. Property writes and reads run on Android's UI thread. */
|
|
7
|
+
public abstract class PropertySink {
|
|
8
|
+
protected final WeakReference<Object> target;
|
|
9
|
+
private final int dimensions;
|
|
10
|
+
protected PropertySink(Object target, int dimensions) { this.target=new WeakReference<>(target); this.dimensions=dimensions; }
|
|
11
|
+
public final int getDimensions() { return dimensions; }
|
|
12
|
+
public boolean isAlive() { return target.get()!=null; }
|
|
13
|
+
public final boolean isCompiled() {
|
|
14
|
+
return !java.lang.reflect.Proxy.isProxyClass(getClass()) && !getClass().getName().startsWith("com.tns.gen.");
|
|
15
|
+
}
|
|
16
|
+
public abstract double[] read();
|
|
17
|
+
public abstract void write(double[] values);
|
|
18
|
+
public void dispose() { target.clear(); }
|
|
19
|
+
|
|
20
|
+
/** Numeric JavaBean property, with cached reflection and no JS setter. */
|
|
21
|
+
public static PropertySink scalar(Object target, String getter, String setter) {
|
|
22
|
+
return new Scalar(target, getter, setter);
|
|
23
|
+
}
|
|
24
|
+
private static final class Scalar extends PropertySink {
|
|
25
|
+
private final Method getter, setter;
|
|
26
|
+
private final boolean useFloat;
|
|
27
|
+
Scalar(Object target, String get, String set) {
|
|
28
|
+
super(target,1);
|
|
29
|
+
try {
|
|
30
|
+
getter=target.getClass().getMethod(get);
|
|
31
|
+
Class<?> type=getter.getReturnType();
|
|
32
|
+
if (type!=float.class && type!=double.class) throw new IllegalArgumentException("Scalar properties must return float or double");
|
|
33
|
+
setter=target.getClass().getMethod(set,type); useFloat=type==float.class;
|
|
34
|
+
} catch (ReflectiveOperationException e) { throw new IllegalArgumentException("Unsupported numeric native property",e); }
|
|
35
|
+
}
|
|
36
|
+
public double[] read() {
|
|
37
|
+
Object object=target.get(); if(object==null) return new double[]{0};
|
|
38
|
+
try { return new double[]{((Number)getter.invoke(object)).doubleValue()}; }
|
|
39
|
+
catch (ReflectiveOperationException e) { throw new IllegalStateException(e); }
|
|
40
|
+
}
|
|
41
|
+
public void write(double[] values) {
|
|
42
|
+
Object object=target.get(); if(object==null) return;
|
|
43
|
+
try {
|
|
44
|
+
if(useFloat) setter.invoke(object, (float)values[0]); else setter.invoke(object,values[0]);
|
|
45
|
+
} catch (ReflectiveOperationException e) { throw new IllegalStateException(e); }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-keep class org.rafaelaugusto.jolt.PropertySink { *; }
|
|
2
|
+
-keep class org.rafaelaugusto.jolt.PropertySink$* { *; }
|
|
3
|
+
-keep class org.rafaelaugusto.jolt.PropertyPlayer { *; }
|
|
4
|
+
-keep class org.rafaelaugusto.jolt.PropertyPlayer$* { *; }
|
|
5
|
+
-keep class org.rafaelaugusto.jolt.MapLibrePropertySink { *; }
|
|
6
|
+
# Optional MapLibre adapters cache these SDK methods/fields by name.
|
|
7
|
+
-keep class org.maplibre.android.geometry.LatLng { *; }
|
|
8
|
+
-keep class org.maplibre.android.camera.CameraPosition { *; }
|
|
9
|
+
-keep class org.maplibre.android.camera.CameraPosition$Builder { *; }
|
|
10
|
+
-keep class org.maplibre.android.camera.CameraUpdate { *; }
|
|
11
|
+
-keep class org.maplibre.android.camera.CameraUpdateFactory { *; }
|
|
12
|
+
-keepclassmembers class org.maplibre.android.maps.MapLibreMap { public *** getCameraPosition(...); public void moveCamera(...); }
|
|
13
|
+
-keepclassmembers class org.maplibre.android.annotations.Marker { public *** getPosition(...); public void setPosition(...); }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
OTHER_LDFLAGS = $(inherited) -framework MapKit -framework SceneKit
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
3
|
+
|
|
4
|
+
/** Subclass in compiled Objective-C/Swift. Targets are weak; writes run on the main thread. */
|
|
5
|
+
@interface JoltPropertySink : NSObject
|
|
6
|
+
- (instancetype)initWithTarget:(NSObject *)target dimensions:(NSUInteger)dimensions;
|
|
7
|
+
+ (instancetype)scalarTarget:(NSObject *)target keyPath:(NSString *)keyPath;
|
|
8
|
+
@property(nonatomic, weak, readonly, nullable) NSObject *target;
|
|
9
|
+
@property(nonatomic, readonly) NSUInteger dimensions;
|
|
10
|
+
@property(nonatomic, readonly) BOOL alive;
|
|
11
|
+
@property(nonatomic, readonly) BOOL compiled;
|
|
12
|
+
- (NSArray<NSNumber *> *)read;
|
|
13
|
+
- (void)write:(NSArray<NSNumber *> *)values;
|
|
14
|
+
- (void)dispose;
|
|
15
|
+
@end
|
|
16
|
+
|
|
17
|
+
@interface JoltPropertyPlayer : NSObject
|
|
18
|
+
+ (instancetype)startSink:(JoltPropertySink *)sink times:(NSArray<NSNumber *> *)times
|
|
19
|
+
values:(NSArray<NSNumber *> *)values finalValue:(NSArray<NSNumber *> *)finalValue
|
|
20
|
+
duration:(double)duration delay:(double)delay repeat:(double)repeat reverse:(BOOL)reverse
|
|
21
|
+
completion:(void (^)(BOOL))completion;
|
|
22
|
+
@property(nonatomic, readonly) double elapsed;
|
|
23
|
+
@property(nonatomic, readonly) NSUInteger frames;
|
|
24
|
+
+ (NSUInteger)activeCount;
|
|
25
|
+
- (void)control:(double)elapsed paused:(BOOL)paused;
|
|
26
|
+
- (void)cancel;
|
|
27
|
+
@end
|
|
28
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#import "JoltPropertyPlayer.h"
|
|
2
|
+
#import <QuartzCore/QuartzCore.h>
|
|
3
|
+
#import <UIKit/UIKit.h>
|
|
4
|
+
#import <objc/runtime.h>
|
|
5
|
+
|
|
6
|
+
@interface JoltPropertySink ()
|
|
7
|
+
@property(nonatomic, weak, readwrite) NSObject *target;
|
|
8
|
+
@property(nonatomic, readwrite) NSUInteger dimensions;
|
|
9
|
+
@end
|
|
10
|
+
@interface JoltScalarSink : JoltPropertySink
|
|
11
|
+
@property(nonatomic, copy) NSString *keyPath;
|
|
12
|
+
+ (instancetype)target:(NSObject *)target keyPath:(NSString *)keyPath;
|
|
13
|
+
@end
|
|
14
|
+
@implementation JoltPropertySink
|
|
15
|
+
- (instancetype)initWithTarget:(NSObject *)target dimensions:(NSUInteger)dimensions {
|
|
16
|
+
if ((self = [super init])) { _target = target; _dimensions = dimensions; }
|
|
17
|
+
return self;
|
|
18
|
+
}
|
|
19
|
+
+ (instancetype)scalarTarget:(NSObject *)target keyPath:(NSString *)keyPath { return [JoltScalarSink target:target keyPath:keyPath]; }
|
|
20
|
+
- (BOOL)alive { return self.target != nil; }
|
|
21
|
+
- (BOOL)compiled { return class_getImageName(self.class) != NULL; }
|
|
22
|
+
- (NSArray<NSNumber *> *)read { [self doesNotRecognizeSelector:_cmd]; return @[]; }
|
|
23
|
+
- (void)write:(NSArray<NSNumber *> *)values { [self doesNotRecognizeSelector:_cmd]; }
|
|
24
|
+
- (void)dispose { self.target = nil; }
|
|
25
|
+
@end
|
|
26
|
+
|
|
27
|
+
@implementation JoltScalarSink
|
|
28
|
+
+ (instancetype)target:(NSObject *)target keyPath:(NSString *)keyPath {
|
|
29
|
+
JoltScalarSink *sink = [[self alloc] initWithTarget:target dimensions:1]; sink.keyPath = keyPath;
|
|
30
|
+
NSAssert([[target valueForKeyPath:keyPath] isKindOfClass:NSNumber.class], @"Expected a numeric native property");
|
|
31
|
+
return sink;
|
|
32
|
+
}
|
|
33
|
+
- (NSArray<NSNumber *> *)read { return @[[self.target valueForKeyPath:self.keyPath] ?: @0]; }
|
|
34
|
+
- (void)write:(NSArray<NSNumber *> *)values {
|
|
35
|
+
[CATransaction begin]; [CATransaction setDisableActions:YES];
|
|
36
|
+
[self.target setValue:values[0] forKeyPath:self.keyPath];
|
|
37
|
+
[CATransaction commit];
|
|
38
|
+
}
|
|
39
|
+
@end
|
|
40
|
+
|
|
41
|
+
@interface JoltPropertyPlayer ()
|
|
42
|
+
@property(nonatomic, strong, nullable) JoltPropertySink *sink;
|
|
43
|
+
@property(nonatomic, copy) NSArray<NSNumber *> *times, *values, *finalValue;
|
|
44
|
+
@property(nonatomic) double duration, delay, repeats, anchor, offset;
|
|
45
|
+
@property(nonatomic) BOOL reverse, paused, stopped, ended;
|
|
46
|
+
@property(nonatomic, readwrite) NSUInteger frames;
|
|
47
|
+
@property(nonatomic, copy, nullable) void (^completion)(BOOL);
|
|
48
|
+
@end
|
|
49
|
+
@implementation JoltPropertyPlayer
|
|
50
|
+
static NSMutableSet<JoltPropertyPlayer *> *playing;
|
|
51
|
+
static CADisplayLink *displayLink;
|
|
52
|
+
+ (void)initialize { if (self == JoltPropertyPlayer.class) playing = [NSMutableSet new]; }
|
|
53
|
+
+ (void)schedule {
|
|
54
|
+
if (playing.count && !displayLink) {
|
|
55
|
+
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
|
|
56
|
+
if (@available(iOS 15.0, *)) {
|
|
57
|
+
float rate = MIN(120, UIScreen.mainScreen.maximumFramesPerSecond);
|
|
58
|
+
displayLink.preferredFrameRateRange = CAFrameRateRangeMake(MIN(80, rate), rate, rate);
|
|
59
|
+
}
|
|
60
|
+
[displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
|
|
61
|
+
} else if (!playing.count) { [displayLink invalidate]; displayLink = nil; }
|
|
62
|
+
}
|
|
63
|
+
+ (void)tick:(CADisplayLink *)frame {
|
|
64
|
+
for (JoltPropertyPlayer *player in playing.allObjects) {
|
|
65
|
+
if (!player.sink.alive) { [player end:NO]; continue; }
|
|
66
|
+
[player apply:player.elapsed];
|
|
67
|
+
if (isfinite(player.repeats) && player.elapsed >= player.delay + player.duration * (player.repeats + 1)) [player end:YES];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
+ (instancetype)startSink:(JoltPropertySink *)sink times:(NSArray<NSNumber *> *)times
|
|
71
|
+
values:(NSArray<NSNumber *> *)values finalValue:(NSArray<NSNumber *> *)finalValue
|
|
72
|
+
duration:(double)duration delay:(double)delay repeat:(double)repeat reverse:(BOOL)reverse
|
|
73
|
+
completion:(void (^)(BOOL))completion {
|
|
74
|
+
NSParameterAssert(NSThread.isMainThread);
|
|
75
|
+
NSParameterAssert(sink.compiled && sink.dimensions && values.count == times.count * sink.dimensions && finalValue.count == sink.dimensions);
|
|
76
|
+
JoltPropertyPlayer *player = [self new];
|
|
77
|
+
player.sink = sink; player.times = times; player.values = values; player.finalValue = finalValue;
|
|
78
|
+
player.duration = MAX(0.001, duration); player.delay = delay; player.repeats = repeat; player.reverse = reverse;
|
|
79
|
+
player.anchor = CACurrentMediaTime() * 1000; player.completion = completion;
|
|
80
|
+
[player apply:0];
|
|
81
|
+
if (!player.ended) { [playing addObject:player]; [self schedule]; }
|
|
82
|
+
return player;
|
|
83
|
+
}
|
|
84
|
+
- (double)elapsed { return self.offset + (self.paused ? 0 : CACurrentMediaTime() * 1000 - self.anchor); }
|
|
85
|
+
- (void)apply:(double)elapsed {
|
|
86
|
+
if (self.stopped || !self.sink.alive) return;
|
|
87
|
+
@try {
|
|
88
|
+
if (isfinite(self.repeats) && elapsed >= self.delay + self.duration * (self.repeats + 1)) {
|
|
89
|
+
[self.sink write:self.finalValue]; self.frames++; return;
|
|
90
|
+
}
|
|
91
|
+
double time = MAX(0, elapsed - self.delay), iteration = floor(time / self.duration);
|
|
92
|
+
double progress = fmod(time, self.duration) / self.duration;
|
|
93
|
+
if (self.reverse && fmod(iteration, 2) >= 1) progress = 1 - progress;
|
|
94
|
+
NSUInteger low = 0, high = self.times.count - 1;
|
|
95
|
+
while (low + 1 < high) {
|
|
96
|
+
NSUInteger mid = (low + high) / 2;
|
|
97
|
+
if (self.times[mid].doubleValue <= progress) low = mid; else high = mid;
|
|
98
|
+
}
|
|
99
|
+
double start = self.times[low].doubleValue, finish = self.times[high].doubleValue;
|
|
100
|
+
double t = finish > start ? MIN(1, MAX(0, (progress-start)/(finish-start))) : 1;
|
|
101
|
+
NSUInteger n = self.sink.dimensions;
|
|
102
|
+
NSMutableArray *value = [NSMutableArray arrayWithCapacity:n];
|
|
103
|
+
for (NSUInteger i=0; i<n; i++) {
|
|
104
|
+
double a=self.values[low*n+i].doubleValue, b=self.values[high*n+i].doubleValue;
|
|
105
|
+
[value addObject:@(a+(b-a)*t)];
|
|
106
|
+
}
|
|
107
|
+
[self.sink write:value]; self.frames++;
|
|
108
|
+
} @catch (NSException *exception) { NSLog(@"Jolt native property failed: %@", exception.reason); [self end:NO]; }
|
|
109
|
+
}
|
|
110
|
+
- (void)control:(double)elapsed paused:(BOOL)paused {
|
|
111
|
+
if (self.stopped) return;
|
|
112
|
+
self.offset=elapsed; self.anchor=CACurrentMediaTime()*1000; self.paused=paused; self.ended=NO;
|
|
113
|
+
[self apply:elapsed];
|
|
114
|
+
if (paused || self.ended) [playing removeObject:self]; else [playing addObject:self];
|
|
115
|
+
[JoltPropertyPlayer schedule];
|
|
116
|
+
}
|
|
117
|
+
- (void)end:(BOOL)completed {
|
|
118
|
+
if (self.ended || self.stopped) return;
|
|
119
|
+
self.ended=YES;
|
|
120
|
+
[playing removeObject:self]; [JoltPropertyPlayer schedule];
|
|
121
|
+
if (self.completion) self.completion(completed);
|
|
122
|
+
}
|
|
123
|
+
- (void)cancel {
|
|
124
|
+
if (self.stopped) return;
|
|
125
|
+
self.stopped=YES; self.completion=nil;
|
|
126
|
+
[playing removeObject:self]; self.sink=nil;
|
|
127
|
+
[JoltPropertyPlayer schedule];
|
|
128
|
+
}
|
|
129
|
+
+ (NSUInteger)activeCount { return playing.count; }
|
|
130
|
+
@end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#import "JoltPropertyPlayer.h"
|
|
2
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
3
|
+
@interface JoltReferenceProperties : NSObject
|
|
4
|
+
+ (JoltPropertySink *)mapCoordinate:(NSObject *)annotation;
|
|
5
|
+
+ (JoltPropertySink *)mapCamera:(NSObject *)mapView;
|
|
6
|
+
+ (JoltPropertySink *)scenePose:(NSObject *)node;
|
|
7
|
+
@end
|
|
8
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#import "JoltReferenceProperties.h"
|
|
2
|
+
#import <MapKit/MapKit.h>
|
|
3
|
+
#import <SceneKit/SceneKit.h>
|
|
4
|
+
|
|
5
|
+
static double longitude(double value) { return fmod(fmod(value+180,360)+360,360)-180; }
|
|
6
|
+
@interface JoltMapCoordinateSink : JoltPropertySink @end
|
|
7
|
+
@implementation JoltMapCoordinateSink
|
|
8
|
+
- (NSArray<NSNumber *> *)read {
|
|
9
|
+
CLLocationCoordinate2D p=[(id<MKAnnotation>)self.target coordinate];
|
|
10
|
+
return @[@(p.latitude),@(p.longitude)];
|
|
11
|
+
}
|
|
12
|
+
- (void)write:(NSArray<NSNumber *> *)values {
|
|
13
|
+
id<MKAnnotation> target=(id)self.target;
|
|
14
|
+
if(target) target.coordinate=CLLocationCoordinate2DMake(MAX(-90,MIN(90,values[0].doubleValue)),longitude(values[1].doubleValue));
|
|
15
|
+
}
|
|
16
|
+
@end
|
|
17
|
+
@interface JoltMapCameraSink : JoltPropertySink @end
|
|
18
|
+
@implementation JoltMapCameraSink
|
|
19
|
+
- (NSArray<NSNumber *> *)read {
|
|
20
|
+
MKMapCamera *camera=[(MKMapView *)self.target camera];
|
|
21
|
+
return @[@(camera.centerCoordinate.latitude),@(camera.centerCoordinate.longitude),@(camera.altitude),@(camera.pitch),@(camera.heading)];
|
|
22
|
+
}
|
|
23
|
+
- (void)write:(NSArray<NSNumber *> *)values {
|
|
24
|
+
MKMapView *map=(id)self.target;
|
|
25
|
+
if(!map) return;
|
|
26
|
+
MKMapCamera *camera=[map.camera copy];
|
|
27
|
+
camera.centerCoordinate=CLLocationCoordinate2DMake(MAX(-90,MIN(90,values[0].doubleValue)),longitude(values[1].doubleValue));
|
|
28
|
+
camera.altitude=MAX(1,values[2].doubleValue); camera.pitch=MAX(0,MIN(80,values[3].doubleValue)); camera.heading=values[4].doubleValue;
|
|
29
|
+
[map setCamera:camera animated:NO];
|
|
30
|
+
}
|
|
31
|
+
@end
|
|
32
|
+
@interface JoltScenePoseSink : JoltPropertySink @end
|
|
33
|
+
@implementation JoltScenePoseSink
|
|
34
|
+
- (NSArray<NSNumber *> *)read {
|
|
35
|
+
SCNNode *node=(id)self.target;
|
|
36
|
+
SCNNode *presented=node.presentationNode ?: node;
|
|
37
|
+
SCNVector3 p=presented.position; SCNQuaternion q=presented.orientation;
|
|
38
|
+
return @[@(p.x),@(p.y),@(p.z),@(q.x),@(q.y),@(q.z),@(q.w)];
|
|
39
|
+
}
|
|
40
|
+
- (void)write:(NSArray<NSNumber *> *)values {
|
|
41
|
+
SCNNode *node=(id)self.target; if(!node) return;
|
|
42
|
+
[SCNTransaction begin]; [SCNTransaction setDisableActions:YES];
|
|
43
|
+
node.position=SCNVector3Make(values[0].floatValue,values[1].floatValue,values[2].floatValue);
|
|
44
|
+
double x=values[3].doubleValue,y=values[4].doubleValue,z=values[5].doubleValue,w=values[6].doubleValue;
|
|
45
|
+
double norm=sqrt(x*x+y*y+z*z+w*w);
|
|
46
|
+
node.orientation=norm>0?SCNVector4Make(x/norm,y/norm,z/norm,w/norm):SCNVector4Make(0,0,0,1);
|
|
47
|
+
[SCNTransaction commit];
|
|
48
|
+
}
|
|
49
|
+
@end
|
|
50
|
+
@implementation JoltReferenceProperties
|
|
51
|
+
+ (JoltPropertySink *)mapCoordinate:(NSObject *)annotation {
|
|
52
|
+
NSParameterAssert([annotation conformsToProtocol:@protocol(MKAnnotation)] && [annotation respondsToSelector:@selector(setCoordinate:)]);
|
|
53
|
+
return [[JoltMapCoordinateSink alloc] initWithTarget:annotation dimensions:2];
|
|
54
|
+
}
|
|
55
|
+
+ (JoltPropertySink *)mapCamera:(NSObject *)mapView {
|
|
56
|
+
NSParameterAssert([mapView isKindOfClass:MKMapView.class]);
|
|
57
|
+
return [[JoltMapCameraSink alloc] initWithTarget:mapView dimensions:5];
|
|
58
|
+
}
|
|
59
|
+
+ (JoltPropertySink *)scenePose:(NSObject *)node {
|
|
60
|
+
NSParameterAssert([node isKindOfClass:SCNNode.class]);
|
|
61
|
+
return [[JoltScenePoseSink alloc] initWithTarget:node dimensions:7];
|
|
62
|
+
}
|
|
63
|
+
@end
|