@rafaelaugusto/jolt 0.1.0-beta.12 → 0.1.0-beta.14
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 +9 -6
- package/dist/animate.d.ts +6 -4
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +3 -1
- package/dist/animate.js.map +1 -1
- package/dist/drag.js +3 -3
- package/dist/drag.js.map +1 -1
- package/dist/driver/android-gradient.d.ts.map +1 -1
- package/dist/driver/android-gradient.js +11 -1
- package/dist/driver/android-gradient.js.map +1 -1
- package/dist/driver/android-particles.d.ts +6 -0
- package/dist/driver/android-particles.d.ts.map +1 -0
- package/dist/driver/android-particles.js +93 -0
- package/dist/driver/android-particles.js.map +1 -0
- 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-gradient.d.ts.map +1 -1
- package/dist/driver/ios-gradient.js +24 -2
- package/dist/driver/ios-gradient.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/engine.d.ts +2 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +32 -5
- package/dist/engine.js.map +1 -1
- package/dist/gradient-binding.d.ts +38 -0
- package/dist/gradient-binding.d.ts.map +1 -0
- package/dist/gradient-binding.js +128 -0
- package/dist/gradient-binding.js.map +1 -0
- package/dist/gradient.d.ts +19 -4
- package/dist/gradient.d.ts.map +1 -1
- package/dist/gradient.js +331 -70
- package/dist/gradient.js.map +1 -1
- package/dist/haptics.d.ts.map +1 -1
- package/dist/haptics.js +101 -28
- package/dist/haptics.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 +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/motion-scope.d.ts +3 -2
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +12 -2
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-value.d.ts +62 -8
- package/dist/motion-value.d.ts.map +1 -1
- package/dist/motion-value.js +327 -91
- package/dist/motion-value.js.map +1 -1
- package/dist/native/index.d.ts +7 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +4 -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/particles.d.ts +5 -2
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +16 -10
- package/dist/particles.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/scene-appearance.d.ts +14 -0
- package/dist/scene-appearance.d.ts.map +1 -0
- package/dist/scene-appearance.js +68 -0
- package/dist/scene-appearance.js.map +1 -0
- 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/types.d.ts +1 -1
- 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.map +1 -1
- package/dist/vue/index.js +28 -5
- package/dist/vue/index.js.map +1 -1
- package/package.json +6 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +30 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/ParticleEmitter.java +192 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ParticleSimulation.java +114 -0
- package/platforms/android/proguard-rules.pro +2 -0
- package/src/android-plugin.d.ts +93 -0
- package/src/animate.ts +13 -7
- package/src/drag.ts +3 -3
- package/src/driver/android-gradient.ts +8 -1
- package/src/driver/android-particles.ts +118 -0
- 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-gradient.ts +24 -2
- package/src/driver/ios.ts +2 -0
- package/src/engine.ts +33 -6
- package/src/gradient-binding.ts +158 -0
- package/src/gradient.ts +376 -91
- package/src/haptics.ts +106 -28
- package/src/image-effect.ts +10 -2
- package/src/index.ts +6 -0
- package/src/ios-plugin.d.ts +40 -0
- package/src/motion-scope.ts +14 -3
- package/src/motion-value.ts +374 -99
- package/src/native/index.ts +23 -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/particles.ts +18 -17
- package/src/playback-group.ts +17 -1
- package/src/playback.ts +41 -0
- package/src/scene-appearance.ts +92 -0
- package/src/sequence.ts +209 -20
- package/src/timed-animation.ts +192 -0
- package/src/types.ts +1 -1
- package/src/value-observers.ts +133 -0
- package/src/value-track.ts +71 -0
- package/src/vue/index.ts +29 -5
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
/** Bounded, allocation-free particle state. Units are DIPs, seconds and degrees.
|
|
4
|
+
* Android supplies elapsed wall time; the same engine runs in deterministic JVM tests. */
|
|
5
|
+
public final class ParticleSimulation {
|
|
6
|
+
public static final int MAX_PARTICLES = 10000;
|
|
7
|
+
public final int shape, source;
|
|
8
|
+
public final float size;
|
|
9
|
+
private final int[] palette;
|
|
10
|
+
private final double rate, lifetime, duration;
|
|
11
|
+
private final float velocity, velocityRange, angle, spread, gravity, spin, spinRange;
|
|
12
|
+
private final boolean fade;
|
|
13
|
+
private double[] births;
|
|
14
|
+
private float[] xs, ys, vxs, vys, rotations, spins;
|
|
15
|
+
private int[] colors;
|
|
16
|
+
private int live, peak;
|
|
17
|
+
private long scheduled, emitted, skipped;
|
|
18
|
+
private long random;
|
|
19
|
+
private double time;
|
|
20
|
+
|
|
21
|
+
public ParticleSimulation(int[] palette, int shape, int source, double rate,
|
|
22
|
+
double lifetimeMs, float size, float velocity, float velocityRange,
|
|
23
|
+
float angle, float spread, float gravity, float spin, float spinRange,
|
|
24
|
+
boolean fade, double durationMs, long seed) {
|
|
25
|
+
if (palette == null || palette.length == 0 || palette.length > 32 || shape < 0 || shape > 2 || source < 0 || source > 2)
|
|
26
|
+
throw new IllegalArgumentException("Invalid particle colors, shape or source");
|
|
27
|
+
if (!finite(rate) || rate <= 0 || rate > 50000 || !finite(lifetimeMs) || lifetimeMs < 1 || lifetimeMs > 60000
|
|
28
|
+
|| !finite(durationMs) || (durationMs != -1 && (durationMs < 1 || durationMs > Integer.MAX_VALUE - lifetimeMs - 50))
|
|
29
|
+
|| !finite(size) || size < 1 || size > 128 || !finite(velocity) || velocity < 0 || velocity > 10000
|
|
30
|
+
|| !finite(velocityRange) || velocityRange < 0 || velocityRange > 10000
|
|
31
|
+
|| !finite(angle) || Math.abs(angle) > 360 || !finite(spread) || spread < 0 || spread > 360
|
|
32
|
+
|| !finite(gravity) || Math.abs(gravity) > 10000 || !finite(spin) || Math.abs(spin) > 3600
|
|
33
|
+
|| !finite(spinRange) || spinRange < 0 || spinRange > 3600)
|
|
34
|
+
throw new IllegalArgumentException("Invalid particle settings");
|
|
35
|
+
this.palette = palette.clone();
|
|
36
|
+
this.shape = shape; this.source = source; this.rate = rate;
|
|
37
|
+
lifetime = lifetimeMs / 1000; duration = durationMs < 0 ? Double.POSITIVE_INFINITY : durationMs / 1000;
|
|
38
|
+
this.size = size; this.velocity = velocity; this.velocityRange = velocityRange;
|
|
39
|
+
this.angle = angle; this.spread = spread; this.gravity = gravity;
|
|
40
|
+
this.spin = spin; this.spinRange = spinRange; this.fade = fade;
|
|
41
|
+
random = seed == 0 ? 1 : seed;
|
|
42
|
+
double population = rate * Math.min(lifetime, duration);
|
|
43
|
+
if (population > MAX_PARTICLES + 0.000001) throw new IllegalArgumentException("Particle population exceeds 10000");
|
|
44
|
+
int capacity = Math.max(1, Math.min(MAX_PARTICLES, (int)Math.ceil(population)));
|
|
45
|
+
births = new double[capacity]; xs = new float[capacity]; ys = new float[capacity];
|
|
46
|
+
vxs = new float[capacity]; vys = new float[capacity]; rotations = new float[capacity];
|
|
47
|
+
spins = new float[capacity]; colors = new int[capacity];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private static boolean finite(double value) { return !Double.isNaN(value) && !Double.isInfinite(value); }
|
|
51
|
+
private float unit() {
|
|
52
|
+
// xorshift64*: deterministic internal seed, never part of the public JS contract.
|
|
53
|
+
random ^= random >>> 12; random ^= random << 25; random ^= random >>> 27;
|
|
54
|
+
return ((random * 2685821657736338717L) >>> 40) / 16777216f;
|
|
55
|
+
}
|
|
56
|
+
private float variation(float range) { return (unit() * 2 - 1) * range; }
|
|
57
|
+
|
|
58
|
+
/** Advance directly to the current time, skipping already-dead births after a stall.
|
|
59
|
+
* No backlog of expired particles and no integration steps proportional to missed frames. */
|
|
60
|
+
public void advance(double elapsedSeconds, float width, float height) {
|
|
61
|
+
if (births == null || !finite(elapsedSeconds)) return;
|
|
62
|
+
time = Math.max(time, Math.max(0, elapsedSeconds));
|
|
63
|
+
for (int i = live - 1; i >= 0; i--) {
|
|
64
|
+
if (time - births[i] >= lifetime - 0.000000001) {
|
|
65
|
+
int last = --live;
|
|
66
|
+
births[i] = births[last]; xs[i] = xs[last]; ys[i] = ys[last];
|
|
67
|
+
vxs[i] = vxs[last]; vys[i] = vys[last]; rotations[i] = rotations[last];
|
|
68
|
+
spins[i] = spins[last]; colors[i] = colors[last];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
long until = (long)Math.floor(Math.min(time, duration) * rate + 0.0000001);
|
|
72
|
+
long dead = Math.min(until, (long)Math.floor(Math.max(0, time - lifetime) * rate + 0.0000001));
|
|
73
|
+
if (dead > scheduled) { skipped += dead - scheduled; scheduled = dead; }
|
|
74
|
+
// scheduled is monotonic; at most capacity particles can be alive in this interval.
|
|
75
|
+
while (scheduled < until) {
|
|
76
|
+
long index = ++scheduled;
|
|
77
|
+
if (live >= births.length) { skipped++; continue; }
|
|
78
|
+
int i = live++;
|
|
79
|
+
births[i] = index / rate;
|
|
80
|
+
xs[i] = source == 0 ? width * .5f : unit() * width;
|
|
81
|
+
ys[i] = source == 1 ? 0 : source == 2 ? height : height * .5f;
|
|
82
|
+
double direction = Math.toRadians(angle + variation(spread * .5f));
|
|
83
|
+
float speed = velocity + variation(velocityRange);
|
|
84
|
+
vxs[i] = (float)Math.cos(direction) * speed;
|
|
85
|
+
vys[i] = (float)Math.sin(direction) * speed;
|
|
86
|
+
rotations[i] = shape == 1 ? unit() * 360 : 0;
|
|
87
|
+
spins[i] = spin + variation(spinRange);
|
|
88
|
+
colors[i] = palette[(int)((index - 1) % palette.length)];
|
|
89
|
+
emitted++;
|
|
90
|
+
}
|
|
91
|
+
peak = Math.max(peak, live);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public float x(int i) { return xs[i] + vxs[i] * (float)(time - births[i]); }
|
|
95
|
+
public float y(int i) { float age = (float)(time - births[i]); return ys[i] + vys[i] * age + gravity * age * age * .5f; }
|
|
96
|
+
public float rotation(int i) { return rotations[i] + spins[i] * (float)(time - births[i]); }
|
|
97
|
+
public int color(int i) {
|
|
98
|
+
int color = colors[i];
|
|
99
|
+
if (!fade) return color;
|
|
100
|
+
int alpha = Math.round((color >>> 24) * (float)Math.max(0, 1 - (time - births[i]) / lifetime));
|
|
101
|
+
return (color & 0x00ffffff) | (alpha << 24);
|
|
102
|
+
}
|
|
103
|
+
public boolean finished() { return time >= duration + lifetime || (time >= duration && live == 0); }
|
|
104
|
+
public int live() { return live; }
|
|
105
|
+
public int peak() { return peak; }
|
|
106
|
+
public int capacity() { return births == null ? 0 : births.length; }
|
|
107
|
+
public long emitted() { return emitted; }
|
|
108
|
+
public long skipped() { return skipped; }
|
|
109
|
+
public double time() { return time; }
|
|
110
|
+
public void clear() {
|
|
111
|
+
live = 0;
|
|
112
|
+
births = null; xs = ys = vxs = vys = rotations = spins = null; colors = null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -19,3 +19,5 @@
|
|
|
19
19
|
-keep class org.rafaelaugusto.jolt.PhotoZoom$* { *; }
|
|
20
20
|
-keep class org.rafaelaugusto.jolt.GradientLayer { *; }
|
|
21
21
|
-keep class org.rafaelaugusto.jolt.GradientLayer$* { *; }
|
|
22
|
+
-keep class org.rafaelaugusto.jolt.ParticleEmitter { *; }
|
|
23
|
+
-keep class org.rafaelaugusto.jolt.ParticleEmitter$* { *; }
|
package/src/android-plugin.d.ts
CHANGED
|
@@ -1,4 +1,52 @@
|
|
|
1
1
|
declare namespace org.rafaelaugusto.jolt {
|
|
2
|
+
class ParticleEmitter {
|
|
3
|
+
constructor(
|
|
4
|
+
target: android.view.View,
|
|
5
|
+
overlay: boolean,
|
|
6
|
+
colors: native.Array<number>,
|
|
7
|
+
shape: number,
|
|
8
|
+
source: number,
|
|
9
|
+
rate: number,
|
|
10
|
+
lifetimeMs: number,
|
|
11
|
+
size: number,
|
|
12
|
+
velocity: number,
|
|
13
|
+
velocityRange: number,
|
|
14
|
+
angle: number,
|
|
15
|
+
spread: number,
|
|
16
|
+
gravity: number,
|
|
17
|
+
spin: number,
|
|
18
|
+
spinRange: number,
|
|
19
|
+
fade: boolean,
|
|
20
|
+
durationMs: number,
|
|
21
|
+
listener: ParticleEmitter.Listener,
|
|
22
|
+
)
|
|
23
|
+
start(): void
|
|
24
|
+
refresh(cornerPixels: number): void
|
|
25
|
+
stop(): void
|
|
26
|
+
static getActiveCount(): number
|
|
27
|
+
isStopped(): boolean
|
|
28
|
+
getLiveCount(): number
|
|
29
|
+
getPeakCount(): number
|
|
30
|
+
getCapacity(): number
|
|
31
|
+
getEmittedCount(): number
|
|
32
|
+
getSkippedCount(): number
|
|
33
|
+
getFrameCount(): number
|
|
34
|
+
getDrawCount(): number
|
|
35
|
+
getWorkNanos(): number
|
|
36
|
+
getDrawNanos(): number
|
|
37
|
+
getDensity(): number
|
|
38
|
+
getTimeSeconds(): number
|
|
39
|
+
isObserving(): boolean
|
|
40
|
+
getOriginInWindow(): native.Array<number>
|
|
41
|
+
getParticle(index: number): native.Array<number>
|
|
42
|
+
getFrameIntervals(): native.Array<number>
|
|
43
|
+
getBounds(): android.graphics.Rect
|
|
44
|
+
}
|
|
45
|
+
namespace ParticleEmitter {
|
|
46
|
+
class Listener {
|
|
47
|
+
constructor(implementation: { onEnd(): void })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
2
50
|
class PhotoZoom {
|
|
3
51
|
constructor(
|
|
4
52
|
image: android.widget.ImageView,
|
|
@@ -116,6 +164,14 @@ declare namespace org.rafaelaugusto.jolt {
|
|
|
116
164
|
reverse: boolean,
|
|
117
165
|
listener: GradientLayer.Listener,
|
|
118
166
|
): void
|
|
167
|
+
control(elapsed: number, paused: boolean): void
|
|
168
|
+
isPlaying(): boolean
|
|
169
|
+
getGeometry(): native.Array<number>
|
|
170
|
+
getCorner(): number
|
|
171
|
+
getStops(): native.Array<number>
|
|
172
|
+
getColors(): native.Array<number>
|
|
173
|
+
getSampleCount(): number
|
|
174
|
+
dispose(): void
|
|
119
175
|
cancel(): void
|
|
120
176
|
}
|
|
121
177
|
namespace GradientLayer {
|
|
@@ -209,3 +265,40 @@ declare namespace org.rafaelaugusto.jolt {
|
|
|
209
265
|
}
|
|
210
266
|
}
|
|
211
267
|
}
|
|
268
|
+
|
|
269
|
+
declare namespace org.rafaelaugusto.jolt {
|
|
270
|
+
class PropertySink extends java.lang.Object {
|
|
271
|
+
static scalar(target: any, getter: string, setter: string): PropertySink
|
|
272
|
+
getDimensions(): number
|
|
273
|
+
isAlive(): boolean
|
|
274
|
+
isCompiled(): boolean
|
|
275
|
+
read(): native.Array<number>
|
|
276
|
+
write(values: native.Array<number>): void
|
|
277
|
+
dispose(): void
|
|
278
|
+
}
|
|
279
|
+
class MapLibrePropertySink extends PropertySink {
|
|
280
|
+
constructor(target: any, camera: boolean)
|
|
281
|
+
}
|
|
282
|
+
class PropertyPlayer extends java.lang.Object {
|
|
283
|
+
constructor(
|
|
284
|
+
sink: PropertySink,
|
|
285
|
+
times: native.Array<number>,
|
|
286
|
+
values: native.Array<number>,
|
|
287
|
+
finalValue: native.Array<number>,
|
|
288
|
+
duration: number,
|
|
289
|
+
delay: number,
|
|
290
|
+
repeat: number,
|
|
291
|
+
reverse: boolean,
|
|
292
|
+
listener: PropertyPlayer.Listener,
|
|
293
|
+
)
|
|
294
|
+
static activeCount(): number
|
|
295
|
+
getFrames(): number
|
|
296
|
+
control(time: number, paused: boolean): void
|
|
297
|
+
cancel(): void
|
|
298
|
+
}
|
|
299
|
+
namespace PropertyPlayer {
|
|
300
|
+
class Listener extends java.lang.Object {
|
|
301
|
+
constructor(implementation: { onEnd(completed: boolean): void })
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
package/src/animate.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
detachBindings,
|
|
30
30
|
isMotionValue,
|
|
31
31
|
type MotionValue,
|
|
32
|
+
type ValueAnimationOptions,
|
|
32
33
|
} from './motion-value.js'
|
|
33
34
|
import { resolveHaptic, type Haptic } from './haptics.js'
|
|
34
35
|
import { layerable } from './mixer.js'
|
|
@@ -77,8 +78,8 @@ interface Options {
|
|
|
77
78
|
export function animate(
|
|
78
79
|
value: MotionValue,
|
|
79
80
|
to: number | readonly (number | null)[],
|
|
80
|
-
transition?:
|
|
81
|
-
):
|
|
81
|
+
transition?: ValueAnimationOptions | SpringName,
|
|
82
|
+
): PlaybackAnimation
|
|
82
83
|
export function animate(
|
|
83
84
|
target: Target | readonly Target[],
|
|
84
85
|
animation: Keyframes | PresetName,
|
|
@@ -92,7 +93,7 @@ export function animate(
|
|
|
92
93
|
if (isMotionValue(target))
|
|
93
94
|
return asRoot(target).animate(
|
|
94
95
|
animation as number | readonly (number | null)[],
|
|
95
|
-
options as
|
|
96
|
+
options as ValueAnimationOptions | SpringName | undefined,
|
|
96
97
|
)
|
|
97
98
|
return prepareAnimation(
|
|
98
99
|
target as Target | readonly Target[],
|
|
@@ -237,7 +238,7 @@ export function decay(
|
|
|
237
238
|
* Stops all or selected properties at their current values. `scale` stops both axes.
|
|
238
239
|
* A motion value stops where it is, and so do the views bound to it.
|
|
239
240
|
*/
|
|
240
|
-
export function stop(value: MotionValue): void
|
|
241
|
+
export function stop(value: MotionValue, options?: { layer?: string }): void
|
|
241
242
|
export function stop(
|
|
242
243
|
target: Target | readonly Target[],
|
|
243
244
|
properties?: readonly KeyframeProperty[],
|
|
@@ -245,11 +246,16 @@ export function stop(
|
|
|
245
246
|
): void
|
|
246
247
|
export function stop(
|
|
247
248
|
target: Target | readonly Target[] | MotionValue,
|
|
248
|
-
properties?: readonly KeyframeProperty[],
|
|
249
|
+
properties?: readonly KeyframeProperty[] | { layer?: string },
|
|
249
250
|
options?: { layer?: string },
|
|
250
251
|
) {
|
|
251
|
-
if (isMotionValue(target))
|
|
252
|
-
|
|
252
|
+
if (isMotionValue(target))
|
|
253
|
+
return asRoot(target).stop((properties as { layer?: string } | undefined)?.layer)
|
|
254
|
+
if (properties && !Array.isArray(properties))
|
|
255
|
+
throw new TypeError('View properties must be an array.')
|
|
256
|
+
const expanded = (
|
|
257
|
+
properties as readonly KeyframeProperty[] | undefined
|
|
258
|
+
)?.flatMap<Property>((property) =>
|
|
253
259
|
property === 'scale' ? ['scaleX', 'scaleY'] : [property],
|
|
254
260
|
)
|
|
255
261
|
for (const view of toViews(target as Target | readonly Target[]))
|
package/src/drag.ts
CHANGED
|
@@ -247,7 +247,7 @@ class DragController implements Drag {
|
|
|
247
247
|
}
|
|
248
248
|
get position(): DragPoint {
|
|
249
249
|
const value = this.config.value
|
|
250
|
-
if (value) return { x: 0, y: 0, [this.config.axes[0]!]: value.
|
|
250
|
+
if (value) return { x: 0, y: 0, [this.config.axes[0]!]: value.base() }
|
|
251
251
|
return { x: currentValue(this.view, 'x'), y: currentValue(this.view, 'y') }
|
|
252
252
|
}
|
|
253
253
|
cancel = () => {
|
|
@@ -338,7 +338,7 @@ class DragController implements Drag {
|
|
|
338
338
|
this.blocked = false
|
|
339
339
|
const value = this.config.value
|
|
340
340
|
if (value) {
|
|
341
|
-
value.
|
|
341
|
+
value.stopBase()
|
|
342
342
|
this.origin = this.position
|
|
343
343
|
} else {
|
|
344
344
|
// Claim just the enabled axes, preserving the native presentation on takeover.
|
|
@@ -385,7 +385,7 @@ class DragController implements Drag {
|
|
|
385
385
|
: clampDrag(raw, curve.bounds)
|
|
386
386
|
if (axis === 'x') vx = input.velocityX * resistance
|
|
387
387
|
else vy = input.velocityY * resistance
|
|
388
|
-
if (this.config.value) this.config.value.
|
|
388
|
+
if (this.config.value) this.config.value.setBase(position[axis])
|
|
389
389
|
else {
|
|
390
390
|
stopBase(this.view, [axis])
|
|
391
391
|
setBase(this.view, axis, position[axis])
|
|
@@ -28,8 +28,10 @@ export function androidGradient(
|
|
|
28
28
|
}
|
|
29
29
|
let stack: android.graphics.drawable.LayerDrawable | undefined
|
|
30
30
|
let queued = false
|
|
31
|
+
let removed = false
|
|
31
32
|
|
|
32
33
|
const wrap = () => {
|
|
34
|
+
if (removed) return
|
|
33
35
|
const native = view.nativeViewProtected as android.view.View | undefined
|
|
34
36
|
if (!native) return
|
|
35
37
|
const corner = typeof view.borderRadius === 'number' ? view.borderRadius : 0
|
|
@@ -87,6 +89,9 @@ export function androidGradient(
|
|
|
87
89
|
}),
|
|
88
90
|
)
|
|
89
91
|
return {
|
|
92
|
+
control(elapsed, paused) {
|
|
93
|
+
if (active) layer.control(Math.round(elapsed), paused)
|
|
94
|
+
},
|
|
90
95
|
cancel() {
|
|
91
96
|
if (!active) return
|
|
92
97
|
active = false
|
|
@@ -96,10 +101,12 @@ export function androidGradient(
|
|
|
96
101
|
}
|
|
97
102
|
},
|
|
98
103
|
remove() {
|
|
104
|
+
if (removed) return
|
|
105
|
+
removed = true
|
|
99
106
|
view.off('loaded', wrap)
|
|
100
107
|
view.style.off('backgroundInternalChange', rewrap)
|
|
101
108
|
view.style.off('borderTopLeftRadiusChange', rewrap)
|
|
102
|
-
layer.
|
|
109
|
+
layer.dispose()
|
|
103
110
|
const native = view.nativeViewProtected as android.view.View | undefined
|
|
104
111
|
if (native && stack && native.getBackground() === stack)
|
|
105
112
|
native.setBackground(stack.getDrawable(0))
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Application, Color, Utils, type View } from '@nativescript/core'
|
|
2
|
+
import { onReducedMotion, reducedMotion } from '../config.js'
|
|
3
|
+
import type { ParticleSettings } from '../particles.js'
|
|
4
|
+
|
|
5
|
+
type NativeEmitter = org.rafaelaugusto.jolt.ParticleEmitter
|
|
6
|
+
const live = new WeakMap<View, { emitter?: NativeEmitter; burst?: NativeEmitter }>()
|
|
7
|
+
|
|
8
|
+
/** Internal diagnostic seam for native conformance, not a public animation API. */
|
|
9
|
+
export function currentAndroidParticles(view: View, overlay = false) {
|
|
10
|
+
return live.get(view)?.[overlay ? 'burst' : 'emitter']
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function androidParticles(
|
|
14
|
+
view: View,
|
|
15
|
+
settings: ParticleSettings,
|
|
16
|
+
overlay: boolean,
|
|
17
|
+
owners: WeakMap<View, () => void>,
|
|
18
|
+
): () => void {
|
|
19
|
+
let stopped = false,
|
|
20
|
+
queued = false
|
|
21
|
+
let emitter: NativeEmitter | undefined,
|
|
22
|
+
target: android.view.View | undefined,
|
|
23
|
+
unwatch: (() => void) | undefined
|
|
24
|
+
const key = overlay ? 'burst' : 'emitter'
|
|
25
|
+
const stop = () => {
|
|
26
|
+
if (stopped) return
|
|
27
|
+
stopped = true
|
|
28
|
+
view.off('loaded', schedule)
|
|
29
|
+
view.off('layoutChanged', schedule)
|
|
30
|
+
view.off('unloaded', stop)
|
|
31
|
+
view.style.off('borderTopLeftRadiusChange', schedule)
|
|
32
|
+
Application.off(Application.suspendEvent, stop)
|
|
33
|
+
unwatch?.()
|
|
34
|
+
unwatch = undefined
|
|
35
|
+
if (owners.get(view) === stop) owners.delete(view)
|
|
36
|
+
const active = live.get(view)
|
|
37
|
+
if (active && active[key] === emitter) {
|
|
38
|
+
delete active[key]
|
|
39
|
+
if (!active.emitter && !active.burst) live.delete(view)
|
|
40
|
+
}
|
|
41
|
+
const native = emitter
|
|
42
|
+
emitter = target = undefined
|
|
43
|
+
native?.stop()
|
|
44
|
+
}
|
|
45
|
+
const update = () => {
|
|
46
|
+
if (!view.isLoaded) return
|
|
47
|
+
if (reducedMotion()) return stop()
|
|
48
|
+
const native = view.nativeViewProtected as android.view.View | undefined
|
|
49
|
+
if (target && target !== native) return stop()
|
|
50
|
+
if (emitter) {
|
|
51
|
+
emitter.refresh(
|
|
52
|
+
(typeof view.borderRadius === 'number' ? view.borderRadius : 0) *
|
|
53
|
+
Utils.layout.getDisplayDensity(),
|
|
54
|
+
)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
if (!native?.getWindowToken() || native.getWidth() <= 0 || native.getHeight() <= 0)
|
|
58
|
+
return
|
|
59
|
+
const colors = Array.create('int', settings.colors.length)
|
|
60
|
+
settings.colors.forEach((color, i) => {
|
|
61
|
+
colors[i] = new Color(color).argb | 0
|
|
62
|
+
})
|
|
63
|
+
target = native
|
|
64
|
+
emitter = new org.rafaelaugusto.jolt.ParticleEmitter(
|
|
65
|
+
native,
|
|
66
|
+
overlay,
|
|
67
|
+
colors,
|
|
68
|
+
settings.shape === 'circle' ? 0 : settings.shape === 'rectangle' ? 1 : 2,
|
|
69
|
+
settings.source === 'center' ? 0 : settings.source === 'top' ? 1 : 2,
|
|
70
|
+
settings.rate,
|
|
71
|
+
settings.lifetime,
|
|
72
|
+
settings.size,
|
|
73
|
+
settings.velocity,
|
|
74
|
+
settings.velocityRange,
|
|
75
|
+
settings.angle,
|
|
76
|
+
settings.spread,
|
|
77
|
+
settings.gravity,
|
|
78
|
+
settings.spin,
|
|
79
|
+
settings.spinRange,
|
|
80
|
+
settings.fadeOut,
|
|
81
|
+
settings.duration ?? -1,
|
|
82
|
+
new org.rafaelaugusto.jolt.ParticleEmitter.Listener({ onEnd: stop }),
|
|
83
|
+
)
|
|
84
|
+
const active = live.get(view) ?? {}
|
|
85
|
+
active[key] = emitter
|
|
86
|
+
live.set(view, active)
|
|
87
|
+
emitter.refresh(
|
|
88
|
+
(typeof view.borderRadius === 'number' ? view.borderRadius : 0) *
|
|
89
|
+
Utils.layout.getDisplayDensity(),
|
|
90
|
+
)
|
|
91
|
+
emitter.start()
|
|
92
|
+
}
|
|
93
|
+
function schedule() {
|
|
94
|
+
if (stopped || queued) return
|
|
95
|
+
queued = true
|
|
96
|
+
void Promise.resolve().then(() => {
|
|
97
|
+
queued = false
|
|
98
|
+
if (stopped) return
|
|
99
|
+
try {
|
|
100
|
+
update()
|
|
101
|
+
} catch (error) {
|
|
102
|
+
stop()
|
|
103
|
+
throw error
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
owners.set(view, stop)
|
|
108
|
+
view.on('loaded', schedule)
|
|
109
|
+
view.on('layoutChanged', schedule)
|
|
110
|
+
view.on('unloaded', stop)
|
|
111
|
+
view.style.on('borderTopLeftRadiusChange', schedule)
|
|
112
|
+
Application.on(Application.suspendEvent, stop)
|
|
113
|
+
unwatch = onReducedMotion((reduced) => {
|
|
114
|
+
if (reduced) stop()
|
|
115
|
+
})
|
|
116
|
+
schedule()
|
|
117
|
+
return stop
|
|
118
|
+
}
|
package/src/driver/android.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { Application, Utils, type View } from '@nativescript/core'
|
|
13
13
|
import type { NumericProperty } from '../properties.js'
|
|
14
14
|
import type { Driver } from './index.js'
|
|
15
|
+
import { frame } from './frame.js'
|
|
15
16
|
import { isColorProperty } from '../color.js'
|
|
16
17
|
import {
|
|
17
18
|
playAndroidColor,
|
|
@@ -109,6 +110,7 @@ export function createAndroidDriver(): Driver {
|
|
|
109
110
|
|
|
110
111
|
return {
|
|
111
112
|
now: () => android.os.SystemClock.uptimeMillis(),
|
|
113
|
+
frame,
|
|
112
114
|
clock,
|
|
113
115
|
control(_view, handle, elapsed, paused, command) {
|
|
114
116
|
;(handle as Handle).control(elapsed, paused, command)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cancelAnimationFrame,
|
|
3
|
+
requestAnimationFrame,
|
|
4
|
+
} from '@nativescript/core/animation-frame'
|
|
5
|
+
|
|
6
|
+
/** NativeScript schedules subsequent frames with CADisplayLink / Choreographer. */
|
|
7
|
+
export function frame(callback: () => void): () => void {
|
|
8
|
+
const id = requestAnimationFrame(callback)
|
|
9
|
+
return () => cancelAnimationFrame(id)
|
|
10
|
+
}
|
package/src/driver/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface LayoutFrame {
|
|
|
15
15
|
export interface Driver {
|
|
16
16
|
/** Milliseconds, on the clock the platform's animations run on. */
|
|
17
17
|
now(): number
|
|
18
|
+
/** One native animation frame; returns an idempotent cancellation function. */
|
|
19
|
+
frame(callback: () => void): () => void
|
|
18
20
|
/** Starts the track at `startedAt + track.delay` and calls `onEnd` once, however it ends. */
|
|
19
21
|
play(
|
|
20
22
|
view: View,
|
|
@@ -47,7 +47,8 @@ export function iosGradient(view: View, type: GradientType): GradientPainter {
|
|
|
47
47
|
CATransaction.setDisableActions(true)
|
|
48
48
|
if (layer.superlayer !== host.layer) host.layer.insertSublayerAtIndex(layer, 0)
|
|
49
49
|
layer.frame = host.layer.bounds
|
|
50
|
-
layer.cornerRadius =
|
|
50
|
+
layer.cornerRadius =
|
|
51
|
+
typeof view.borderRadius === 'number' ? view.borderRadius : host.layer.cornerRadius
|
|
51
52
|
if (current) {
|
|
52
53
|
const model = values(current)
|
|
53
54
|
layer.colors = model.colors
|
|
@@ -59,9 +60,13 @@ export function iosGradient(view: View, type: GradientType): GradientPainter {
|
|
|
59
60
|
}
|
|
60
61
|
view.on('loaded', attach)
|
|
61
62
|
view.on('layoutChanged', attach)
|
|
63
|
+
view.style.on('borderTopLeftRadiusChange', attach)
|
|
62
64
|
|
|
63
65
|
return {
|
|
64
66
|
paint(frame) {
|
|
67
|
+
layer.speed = 1
|
|
68
|
+
layer.timeOffset = 0
|
|
69
|
+
layer.beginTime = 0
|
|
65
70
|
current = frame
|
|
66
71
|
generation++
|
|
67
72
|
for (const key of keys) layer.removeAnimationForKey(`jolt:gradient:${key}`)
|
|
@@ -73,7 +78,14 @@ export function iosGradient(view: View, type: GradientType): GradientPainter {
|
|
|
73
78
|
attach()
|
|
74
79
|
const sampled = frames.map(values)
|
|
75
80
|
const now = CACurrentMediaTime()
|
|
81
|
+
const parentTime = () =>
|
|
82
|
+
layer.superlayer?.convertTimeFromLayer(CACurrentMediaTime(), null) ??
|
|
83
|
+
CACurrentMediaTime()
|
|
76
84
|
CATransaction.begin()
|
|
85
|
+
CATransaction.setDisableActions(true)
|
|
86
|
+
layer.speed = 1
|
|
87
|
+
layer.timeOffset = 0
|
|
88
|
+
layer.beginTime = layer.superlayer?.convertTimeFromLayer(now, null) ?? now
|
|
77
89
|
if (timing.repeat !== Infinity)
|
|
78
90
|
CATransaction.setCompletionBlock(() => {
|
|
79
91
|
if (instance === generation) onEnd(true)
|
|
@@ -83,7 +95,7 @@ export function iosGradient(view: View, type: GradientType): GradientPainter {
|
|
|
83
95
|
animation.values = NSArray.arrayWithArray(sampled.map((value) => value[key]))
|
|
84
96
|
animation.keyTimes = NSArray.arrayWithArray(times as number[])
|
|
85
97
|
animation.duration = Math.max(timing.duration, 1) / 1000
|
|
86
|
-
animation.beginTime =
|
|
98
|
+
animation.beginTime = Math.max(0.000001, timing.delay / 1000)
|
|
87
99
|
animation.fillMode = kCAFillModeBackwards
|
|
88
100
|
if (timing.repeat > 0) {
|
|
89
101
|
animation.autoreverses = timing.reverse
|
|
@@ -95,6 +107,15 @@ export function iosGradient(view: View, type: GradientType): GradientPainter {
|
|
|
95
107
|
}
|
|
96
108
|
CATransaction.commit()
|
|
97
109
|
return {
|
|
110
|
+
control(elapsed, paused) {
|
|
111
|
+
if (instance !== generation) return
|
|
112
|
+
CATransaction.begin()
|
|
113
|
+
CATransaction.setDisableActions(true)
|
|
114
|
+
layer.speed = paused ? 0 : 1
|
|
115
|
+
layer.timeOffset = paused ? elapsed / 1000 : 0
|
|
116
|
+
layer.beginTime = paused ? 0 : parentTime() - elapsed / 1000
|
|
117
|
+
CATransaction.commit()
|
|
118
|
+
},
|
|
98
119
|
cancel() {
|
|
99
120
|
if (instance !== generation) return
|
|
100
121
|
generation++
|
|
@@ -107,6 +128,7 @@ export function iosGradient(view: View, type: GradientType): GradientPainter {
|
|
|
107
128
|
generation++
|
|
108
129
|
view.off('loaded', attach)
|
|
109
130
|
view.off('layoutChanged', attach)
|
|
131
|
+
view.style.off('borderTopLeftRadiusChange', attach)
|
|
110
132
|
layer.removeAllAnimations()
|
|
111
133
|
layer.removeFromSuperlayer()
|
|
112
134
|
},
|
package/src/driver/ios.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { playIOSColor, presentedIOSColor } from './ios-color.js'
|
|
|
13
13
|
import { after, type Driver } from './index.js'
|
|
14
14
|
import { hasInteractionSurface } from '../interaction-surface.js'
|
|
15
15
|
import { retainInteractiveLayout } from './ios-layout.js'
|
|
16
|
+
import { frame } from './frame.js'
|
|
16
17
|
|
|
17
18
|
interface Handle {
|
|
18
19
|
stop(): void
|
|
@@ -59,6 +60,7 @@ function ensurePerspective(view: View, layers: CALayer[]) {
|
|
|
59
60
|
export function createIOSDriver(): Driver {
|
|
60
61
|
return {
|
|
61
62
|
now: () => CACurrentMediaTime() * 1000,
|
|
63
|
+
frame,
|
|
62
64
|
|
|
63
65
|
play(view, track, startedAt, onEnd): Handle {
|
|
64
66
|
if (isDecorationProperty(track.property))
|
package/src/engine.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createClock,
|
|
3
|
+
registerPlayback,
|
|
3
4
|
validateProgress,
|
|
4
5
|
type PlaybackAnimation,
|
|
5
6
|
type PlaybackClock,
|
|
6
7
|
type PlaybackCommand,
|
|
7
8
|
} from './playback.js'
|
|
8
|
-
import type
|
|
9
|
+
import { Application, type View } from '@nativescript/core'
|
|
9
10
|
import { getDriver } from './driver/index.js'
|
|
10
11
|
import { playHaptics, type HapticOptions } from './haptics.js'
|
|
11
12
|
import { readValue, writeValue, type Property } from './properties.js'
|
|
@@ -109,7 +110,9 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
109
110
|
private readyListener: (() => void) | undefined
|
|
110
111
|
private readonly endless: boolean
|
|
111
112
|
private readonly timingListeners = new Set<() => void>()
|
|
113
|
+
private readonly playbackChanged: () => void
|
|
112
114
|
private timingChanged() {
|
|
115
|
+
this.playbackChanged()
|
|
113
116
|
for (const callback of [...this.timingListeners]) callback()
|
|
114
117
|
}
|
|
115
118
|
|
|
@@ -142,6 +145,7 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
142
145
|
if (this.settled) return
|
|
143
146
|
if (!this.started) {
|
|
144
147
|
this.pendingProgress = value
|
|
148
|
+
this.timingChanged()
|
|
145
149
|
return
|
|
146
150
|
}
|
|
147
151
|
this.seekTime(this.duration * value)
|
|
@@ -280,7 +284,12 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
280
284
|
getDriver().stop(this.view, handle)
|
|
281
285
|
this.playEntry(entry, elapsed)
|
|
282
286
|
}
|
|
283
|
-
private unloaded = () =>
|
|
287
|
+
private unloaded = () => {
|
|
288
|
+
// NativeScript unloads the root view when iOS enters the background.
|
|
289
|
+
// An explicitly paused owner can resume after that temporary unload.
|
|
290
|
+
if (this._paused && Application.suspended) return
|
|
291
|
+
this.stop()
|
|
292
|
+
}
|
|
284
293
|
|
|
285
294
|
constructor(
|
|
286
295
|
readonly view: View,
|
|
@@ -295,6 +304,18 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
295
304
|
!Number.isFinite(plan.transition.curve.duration)),
|
|
296
305
|
)
|
|
297
306
|
this.finished = new Promise((resolve) => (this.resolve = resolve))
|
|
307
|
+
this.playbackChanged = registerPlayback(this, () => ({
|
|
308
|
+
position: this.pendingProgress,
|
|
309
|
+
time:
|
|
310
|
+
this.pendingProgress === undefined
|
|
311
|
+
? this.time()
|
|
312
|
+
: this.pendingProgress * this.duration,
|
|
313
|
+
duration: this.duration,
|
|
314
|
+
paused: this._paused,
|
|
315
|
+
ready: this.ready,
|
|
316
|
+
settled: this.settled,
|
|
317
|
+
completed: this.settled && this.complete,
|
|
318
|
+
}))
|
|
298
319
|
setArrival(this, new Promise<void>((resolve) => (this.arrive = resolve)))
|
|
299
320
|
setArrivalTiming(this, {
|
|
300
321
|
read: () => ({
|
|
@@ -436,11 +457,17 @@ export class ViewAnimation implements PlaybackAnimation {
|
|
|
436
457
|
createClock(() => driver.now())
|
|
437
458
|
if (this._paused) this.clock.pause()
|
|
438
459
|
this.view.on('unloaded', this.unloaded)
|
|
439
|
-
this.haptics = playHaptics(this.view, tracks, this.haptic, driver, now, () =>
|
|
440
|
-
this.time(),
|
|
441
|
-
)
|
|
442
|
-
if (this._paused) this.haptics?.pause()
|
|
443
460
|
for (const entry of this.entries) this.playEntry(entry)
|
|
461
|
+
// Register discrete feedback after the visual animators. Android dispatches
|
|
462
|
+
// these callbacks in registration order within a frame: feedback should see
|
|
463
|
+
// that frame's updated pose, not the preceding frame's position.
|
|
464
|
+
// Scale-zero native playback may already have completed synchronously.
|
|
465
|
+
if (!this.settled) {
|
|
466
|
+
this.haptics = playHaptics(this.view, tracks, this.haptic, driver, now, () =>
|
|
467
|
+
this.time(),
|
|
468
|
+
)
|
|
469
|
+
if (this._paused) this.haptics?.pause()
|
|
470
|
+
}
|
|
444
471
|
if (this.pendingProgress !== undefined) {
|
|
445
472
|
const progress = this.pendingProgress
|
|
446
473
|
this.pendingProgress = undefined
|