@rafaelaugusto/jolt 0.1.0-beta.3 → 0.1.0-beta.5
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 +78 -13
- package/dist/animate.d.ts +3 -2
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +35 -5
- package/dist/animate.js.map +1 -1
- package/dist/color.d.ts +11 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +73 -0
- package/dist/color.js.map +1 -0
- package/dist/decoration.d.ts +18 -0
- package/dist/decoration.d.ts.map +1 -0
- package/dist/decoration.js +155 -0
- package/dist/decoration.js.map +1 -0
- package/dist/driver/android-color.d.ts +20 -0
- package/dist/driver/android-color.d.ts.map +1 -0
- package/dist/driver/android-color.js +136 -0
- package/dist/driver/android-color.js.map +1 -0
- package/dist/driver/android-decoration.d.ts +22 -0
- package/dist/driver/android-decoration.d.ts.map +1 -0
- package/dist/driver/android-decoration.js +139 -0
- package/dist/driver/android-decoration.js.map +1 -0
- package/dist/driver/android-playback.d.ts +13 -0
- package/dist/driver/android-playback.d.ts.map +1 -0
- package/dist/driver/android-playback.js +141 -0
- package/dist/driver/android-playback.js.map +1 -0
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +28 -35
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +8 -1
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.d.ts +16 -0
- package/dist/driver/ios-color.d.ts.map +1 -0
- package/dist/driver/ios-color.js +392 -0
- package/dist/driver/ios-color.js.map +1 -0
- package/dist/driver/ios-decoration.d.ts +17 -0
- package/dist/driver/ios-decoration.d.ts.map +1 -0
- package/dist/driver/ios-decoration.js +271 -0
- package/dist/driver/ios-decoration.js.map +1 -0
- package/dist/driver/ios-playback.d.ts +10 -0
- package/dist/driver/ios-playback.d.ts.map +1 -0
- package/dist/driver/ios-playback.js +33 -0
- package/dist/driver/ios-playback.js.map +1 -0
- package/dist/driver/ios-shadow.d.ts +6 -0
- package/dist/driver/ios-shadow.d.ts.map +1 -0
- package/dist/driver/ios-shadow.js +52 -0
- package/dist/driver/ios-shadow.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +78 -35
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +48 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +217 -21
- package/dist/engine.js.map +1 -1
- package/dist/haptics.d.ts +4 -1
- package/dist/haptics.d.ts.map +1 -1
- package/dist/haptics.js +27 -3
- package/dist/haptics.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/liquid-grid.d.ts +3 -0
- package/dist/liquid-grid.d.ts.map +1 -0
- package/dist/liquid-grid.js +24 -0
- package/dist/liquid-grid.js.map +1 -0
- package/dist/liquid.d.ts +18 -0
- package/dist/liquid.d.ts.map +1 -0
- package/dist/liquid.js +229 -0
- package/dist/liquid.js.map +1 -0
- package/dist/playback-group.d.ts +6 -0
- package/dist/playback-group.d.ts.map +1 -0
- package/dist/playback-group.js +113 -0
- package/dist/playback-group.js.map +1 -0
- package/dist/playback.d.ts +25 -0
- package/dist/playback.d.ts.map +1 -0
- package/dist/playback.js +35 -0
- package/dist/playback.js.map +1 -0
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +6 -1
- package/dist/press.js.map +1 -1
- package/dist/properties.d.ts +5 -2
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +25 -1
- package/dist/properties.js.map +1 -1
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +6 -0
- package/dist/sequence.js.map +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +3 -0
- package/dist/text.js.map +1 -1
- package/dist/track.d.ts +1 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +48 -3
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +11 -2
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -2
- package/platforms/android/include.gradle +7 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
- package/platforms/android/proguard-rules.pro +7 -0
- package/src/animate.ts +48 -9
- package/src/color.ts +101 -0
- package/src/decoration.ts +180 -0
- package/src/driver/android-color.ts +170 -0
- package/src/driver/android-decoration.ts +182 -0
- package/src/driver/android-playback.ts +157 -0
- package/src/driver/android.ts +50 -46
- package/src/driver/index.ts +19 -1
- package/src/driver/ios-color.ts +441 -0
- package/src/driver/ios-decoration.ts +332 -0
- package/src/driver/ios-playback.ts +37 -0
- package/src/driver/ios-shadow.ts +50 -0
- package/src/driver/ios.ts +91 -42
- package/src/engine.ts +227 -23
- package/src/haptics.ts +25 -2
- package/src/index.ts +4 -0
- package/src/liquid-grid.ts +28 -0
- package/src/liquid.ts +282 -0
- package/src/playback-group.ts +110 -0
- package/src/playback.ts +62 -0
- package/src/press.ts +8 -2
- package/src/properties.ts +31 -5
- package/src/sequence.ts +5 -0
- package/src/text.ts +2 -0
- package/src/track.ts +72 -3
- package/src/transition.ts +13 -2
- package/src/types.ts +20 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import android.animation.StateListAnimator;
|
|
4
|
+
import android.graphics.drawable.Drawable;
|
|
5
|
+
import android.graphics.drawable.ColorDrawable;
|
|
6
|
+
import android.view.View;
|
|
7
|
+
import java.lang.ref.WeakReference;
|
|
8
|
+
import java.lang.reflect.Constructor;
|
|
9
|
+
import java.lang.reflect.Field;
|
|
10
|
+
import java.util.WeakHashMap;
|
|
11
|
+
|
|
12
|
+
/** ObjectAnimator's float target. No JavaScript enters the native frame loop. */
|
|
13
|
+
public final class DecorationTarget {
|
|
14
|
+
private static final String BORDER = "org.nativescript.widgets.BorderDrawable";
|
|
15
|
+
private static final Field[][] FIELDS = fields();
|
|
16
|
+
private static final Constructor<?> CONSTRUCTOR = constructor();
|
|
17
|
+
private static final WeakHashMap<View, BackgroundLease> BACKGROUNDS = new WeakHashMap<>();
|
|
18
|
+
private static final WeakHashMap<View, Lease> LEASES = new WeakHashMap<>();
|
|
19
|
+
private final WeakReference<View> view;
|
|
20
|
+
private final int property;
|
|
21
|
+
private final int borderColor;
|
|
22
|
+
private float value;
|
|
23
|
+
private boolean acquired;
|
|
24
|
+
|
|
25
|
+
// 0 = uniform radius, 1 = uniform width, 2 = elevation, 3 = translationZ.
|
|
26
|
+
public DecorationTarget(View view, int property, float value, int borderColor) {
|
|
27
|
+
this.view = new WeakReference<>(view);
|
|
28
|
+
this.property = property;
|
|
29
|
+
this.value = value;
|
|
30
|
+
this.borderColor = borderColor;
|
|
31
|
+
}
|
|
32
|
+
private static Field[][] fields() {
|
|
33
|
+
try {
|
|
34
|
+
Class<?> type = Class.forName(BORDER);
|
|
35
|
+
String[][] names = {
|
|
36
|
+
{"borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", "borderBottomLeftRadius"},
|
|
37
|
+
{"borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth"}
|
|
38
|
+
};
|
|
39
|
+
Field[][] result = new Field[2][4];
|
|
40
|
+
for (int i = 0; i < 2; i++) for (int j = 0; j < 4; j++) {
|
|
41
|
+
result[i][j] = type.getDeclaredField(names[i][j]);
|
|
42
|
+
result[i][j].setAccessible(true);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
} catch (ReflectiveOperationException | SecurityException exception) { return null; }
|
|
46
|
+
}
|
|
47
|
+
private static Constructor<?> constructor() {
|
|
48
|
+
try { return Class.forName(BORDER).getConstructor(float.class); }
|
|
49
|
+
catch (ReflectiveOperationException | SecurityException exception) { return null; }
|
|
50
|
+
}
|
|
51
|
+
public boolean isSupported() {
|
|
52
|
+
View nativeView = view.get();
|
|
53
|
+
if (nativeView == null) return false;
|
|
54
|
+
if (property >= 2) return true;
|
|
55
|
+
Drawable drawable = ColorTarget.background(nativeView);
|
|
56
|
+
return FIELDS != null && ((drawable != null && drawable.getClass().getName().equals(BORDER)) ||
|
|
57
|
+
((drawable == null || drawable.getClass() == ColorDrawable.class) && CONSTRUCTOR != null && ColorTarget.FIELDS != null));
|
|
58
|
+
}
|
|
59
|
+
public float getValue() { return value; }
|
|
60
|
+
public void setValue(float next) {
|
|
61
|
+
View nativeView = view.get();
|
|
62
|
+
if (nativeView == null) return;
|
|
63
|
+
value = next;
|
|
64
|
+
if (property >= 2) suspendStateAnimator(nativeView);
|
|
65
|
+
if (property == 2) { nativeView.setElevation(next); return; }
|
|
66
|
+
if (property == 3) { nativeView.setTranslationZ(next); return; }
|
|
67
|
+
ensureBackground(nativeView);
|
|
68
|
+
Drawable drawable = ColorTarget.background(nativeView);
|
|
69
|
+
if (drawable == null || !drawable.getClass().getName().equals(BORDER) || FIELDS == null) return;
|
|
70
|
+
try {
|
|
71
|
+
// Core rebuilds the paths from these fields in draw() and getOutline().
|
|
72
|
+
// Keep its bitmap, gradient, color and all other state on the same drawable.
|
|
73
|
+
for (Field field : FIELDS[property]) field.setFloat(drawable, next);
|
|
74
|
+
drawable.invalidateSelf();
|
|
75
|
+
nativeView.invalidateOutline();
|
|
76
|
+
} catch (IllegalAccessException exception) { /* Keep Core's final model. */ }
|
|
77
|
+
}
|
|
78
|
+
public void acquire() {
|
|
79
|
+
View nativeView = view.get();
|
|
80
|
+
if (nativeView == null || acquired) return;
|
|
81
|
+
acquired = true;
|
|
82
|
+
if (property < 2) {
|
|
83
|
+
BackgroundLease lease = BACKGROUNDS.get(nativeView);
|
|
84
|
+
if (lease == null) { lease = new BackgroundLease(); BACKGROUNDS.put(nativeView, lease); }
|
|
85
|
+
lease.count++;
|
|
86
|
+
ensureBackground(nativeView);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
Lease lease = LEASES.get(nativeView);
|
|
90
|
+
if (lease == null) {
|
|
91
|
+
lease = new Lease(nativeView.getStateListAnimator());
|
|
92
|
+
LEASES.put(nativeView, lease);
|
|
93
|
+
nativeView.setStateListAnimator(null);
|
|
94
|
+
}
|
|
95
|
+
lease.count++;
|
|
96
|
+
suspendStateAnimator(nativeView);
|
|
97
|
+
}
|
|
98
|
+
public void release() {
|
|
99
|
+
View nativeView = view.get();
|
|
100
|
+
if (!acquired || nativeView == null) return;
|
|
101
|
+
acquired = false;
|
|
102
|
+
if (property < 2) {
|
|
103
|
+
BackgroundLease lease = BACKGROUNDS.get(nativeView);
|
|
104
|
+
if (lease != null && --lease.count == 0) {
|
|
105
|
+
BACKGROUNDS.remove(nativeView);
|
|
106
|
+
if (lease.drawable != null && nativeView.getBackground() == lease.drawable) nativeView.setBackground(lease.original);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
suspendStateAnimator(nativeView);
|
|
111
|
+
Lease lease = LEASES.get(nativeView);
|
|
112
|
+
if (lease != null && --lease.count == 0) {
|
|
113
|
+
LEASES.remove(nativeView);
|
|
114
|
+
if (nativeView.getStateListAnimator() == null) {
|
|
115
|
+
float elevation = nativeView.getElevation(), z = nativeView.getTranslationZ();
|
|
116
|
+
nativeView.setStateListAnimator(lease.animator);
|
|
117
|
+
if (lease.animator != null) lease.animator.jumpToCurrentState();
|
|
118
|
+
nativeView.setElevation(elevation);
|
|
119
|
+
nativeView.setTranslationZ(z);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
private void ensureBackground(View nativeView) {
|
|
124
|
+
BackgroundLease lease = BACKGROUNDS.get(nativeView);
|
|
125
|
+
Drawable original = nativeView.getBackground();
|
|
126
|
+
if (lease == null || (original != null && original.getClass() != ColorDrawable.class) || CONSTRUCTOR == null || ColorTarget.FIELDS == null) return;
|
|
127
|
+
try {
|
|
128
|
+
Drawable drawable = (Drawable) CONSTRUCTOR.newInstance(nativeView.getResources().getDisplayMetrics().density);
|
|
129
|
+
ColorTarget.FIELDS[0].setInt(drawable, original == null ? 0 : ((ColorDrawable) original).getColor());
|
|
130
|
+
for (int i = 1; i < ColorTarget.FIELDS.length; i++) ColorTarget.FIELDS[i].setInt(drawable, borderColor);
|
|
131
|
+
lease.original = original;
|
|
132
|
+
lease.drawable = drawable;
|
|
133
|
+
nativeView.setBackground(drawable);
|
|
134
|
+
} catch (ReflectiveOperationException exception) { /* Unsupported Core. */ }
|
|
135
|
+
}
|
|
136
|
+
private static final class BackgroundLease {
|
|
137
|
+
Drawable original, drawable;
|
|
138
|
+
int count;
|
|
139
|
+
}
|
|
140
|
+
private static void suspendStateAnimator(View view) {
|
|
141
|
+
Lease lease = LEASES.get(view);
|
|
142
|
+
StateListAnimator current = view.getStateListAnimator();
|
|
143
|
+
if (lease != null && current != null) {
|
|
144
|
+
// A Core model write may have installed an updated state animator.
|
|
145
|
+
lease.animator = current;
|
|
146
|
+
view.setStateListAnimator(null);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
private static final class Lease {
|
|
150
|
+
StateListAnimator animator;
|
|
151
|
+
int count;
|
|
152
|
+
Lease(StateListAnimator animator) { this.animator = animator; }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-keep class org.rafaelaugusto.jolt.ColorTarget { *; }
|
|
2
|
+
-keep class org.nativescript.widgets.BorderDrawable { *; }
|
|
3
|
+
-keepnames class org.nativescript.widgets.BoxShadowDrawable
|
|
4
|
+
-keepclassmembers class org.nativescript.widgets.BoxShadowDrawable {
|
|
5
|
+
public android.graphics.drawable.Drawable getWrappedDrawable();
|
|
6
|
+
}
|
|
7
|
+
-keep class org.rafaelaugusto.jolt.DecorationTarget { *; }
|
package/src/animate.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isDecorationProperty,
|
|
3
|
+
validateDecorationTarget,
|
|
4
|
+
validateDecorationFrames,
|
|
5
|
+
} from './decoration.js'
|
|
6
|
+
import { playbackGroup } from './playback-group.js'
|
|
7
|
+
import type { PlaybackAnimation } from './playback.js'
|
|
1
8
|
import { View } from '@nativescript/core'
|
|
9
|
+
import {
|
|
10
|
+
isColorProperty,
|
|
11
|
+
parseColor,
|
|
12
|
+
validateColorTarget,
|
|
13
|
+
type ColorKeyframes,
|
|
14
|
+
} from './color.js'
|
|
2
15
|
import { DEFAULT_FRICTION, solveDecay } from './decay.js'
|
|
3
16
|
import {
|
|
4
17
|
arrivalOf,
|
|
@@ -51,7 +64,7 @@ export function animate(
|
|
|
51
64
|
target: Target | readonly Target[],
|
|
52
65
|
animation: Keyframes | PresetName,
|
|
53
66
|
options?: AnimateOptions | SpringName,
|
|
54
|
-
):
|
|
67
|
+
): PlaybackAnimation {
|
|
55
68
|
const { keyframes, base, overrides, stagger, relative, haptic } = prepare(
|
|
56
69
|
animation,
|
|
57
70
|
options,
|
|
@@ -59,15 +72,23 @@ export function animate(
|
|
|
59
72
|
const feedback = resolveHaptic(haptic)
|
|
60
73
|
|
|
61
74
|
if (!isList(target)) {
|
|
75
|
+
const view = toView(target)
|
|
76
|
+
for (const property of Object.keys(keyframes))
|
|
77
|
+
if (isColorProperty(property)) validateColorTarget(view, property)
|
|
78
|
+
else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
|
|
62
79
|
return new ViewAnimation(
|
|
63
|
-
|
|
80
|
+
view,
|
|
64
81
|
plan(keyframes, base, overrides, 0, relative),
|
|
65
82
|
feedback,
|
|
66
83
|
)
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
const views = target.map(toView)
|
|
70
|
-
|
|
87
|
+
for (const view of views)
|
|
88
|
+
for (const property of Object.keys(keyframes))
|
|
89
|
+
if (isColorProperty(property)) validateColorTarget(view, property)
|
|
90
|
+
else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
|
|
91
|
+
return playbackGroup(
|
|
71
92
|
views.map(
|
|
72
93
|
(view, index) =>
|
|
73
94
|
new ViewAnimation(
|
|
@@ -80,6 +101,7 @@ export function animate(
|
|
|
80
101
|
relative,
|
|
81
102
|
),
|
|
82
103
|
feedback,
|
|
104
|
+
true,
|
|
83
105
|
),
|
|
84
106
|
),
|
|
85
107
|
)
|
|
@@ -90,7 +112,7 @@ export function decay(
|
|
|
90
112
|
target: Target | readonly Target[],
|
|
91
113
|
velocity: DecayVelocity,
|
|
92
114
|
options: DecayOptions = {},
|
|
93
|
-
):
|
|
115
|
+
): PlaybackAnimation {
|
|
94
116
|
const friction = options.friction ?? DEFAULT_FRICTION
|
|
95
117
|
if (!Number.isFinite(friction) || friction <= 0 || friction >= 1) {
|
|
96
118
|
throw new RangeError('Decay friction must be between 0 and 1 exclusively.')
|
|
@@ -152,8 +174,10 @@ export function decay(
|
|
|
152
174
|
]
|
|
153
175
|
})
|
|
154
176
|
const views = toViews(target)
|
|
155
|
-
if (plans.length === 0) return
|
|
156
|
-
return
|
|
177
|
+
if (plans.length === 0) return playbackGroup([])
|
|
178
|
+
return playbackGroup(
|
|
179
|
+
views.map((view) => new ViewAnimation(view, plans, undefined, true)),
|
|
180
|
+
)
|
|
157
181
|
}
|
|
158
182
|
|
|
159
183
|
/** Stops all or selected properties at their current values. `scale` stops both axes. */
|
|
@@ -282,7 +306,10 @@ export function startingValues(
|
|
|
282
306
|
const { keyframes, relative } = prepare(animation, options)
|
|
283
307
|
if (!keyframes) throw new Error(`Unknown preset "${animation}".`)
|
|
284
308
|
return new Map(
|
|
285
|
-
expand(keyframes).map(([property, [first]]) => [
|
|
309
|
+
expand(keyframes).map(([property, [first]]) => [
|
|
310
|
+
property,
|
|
311
|
+
relative && !isColorProperty(property) ? null : first!,
|
|
312
|
+
]),
|
|
286
313
|
)
|
|
287
314
|
}
|
|
288
315
|
|
|
@@ -290,7 +317,7 @@ function expand(keyframes: Keyframes): [Property, (number | null)[]][] {
|
|
|
290
317
|
const expanded: [Property, (number | null)[]][] = []
|
|
291
318
|
for (const [key, value] of Object.entries(keyframes) as [
|
|
292
319
|
keyof Keyframes,
|
|
293
|
-
KeyframeValue | undefined,
|
|
320
|
+
KeyframeValue | ColorKeyframes | undefined,
|
|
294
321
|
][]) {
|
|
295
322
|
if (value === undefined) continue
|
|
296
323
|
if (key !== 'scale' && !isProperty(key)) throw new Error(`Can't animate "${key}".`)
|
|
@@ -304,7 +331,18 @@ function expand(keyframes: Keyframes): [Property, (number | null)[]][] {
|
|
|
304
331
|
return expanded
|
|
305
332
|
}
|
|
306
333
|
|
|
307
|
-
function toKeyframes(
|
|
334
|
+
function toKeyframes(
|
|
335
|
+
property: Property,
|
|
336
|
+
value: KeyframeValue | ColorKeyframes,
|
|
337
|
+
): (number | null)[] {
|
|
338
|
+
if (isColorProperty(property)) {
|
|
339
|
+
const frames = Array.isArray(value) ? [...value] : [null, value]
|
|
340
|
+
if (!frames.length) throw new TypeError(`Empty color keyframes for "${property}".`)
|
|
341
|
+
if (frames.length === 1) frames.unshift(null)
|
|
342
|
+
return frames.map((frame) => (frame === null ? null : parseColor(frame)))
|
|
343
|
+
}
|
|
344
|
+
if (typeof value !== 'number' && !Array.isArray(value))
|
|
345
|
+
throw new TypeError(`Invalid numeric keyframes for "${property}".`)
|
|
308
346
|
const frames = typeof value === 'number' ? [null, value] : [...value]
|
|
309
347
|
if (frames.length === 1) frames.unshift(null)
|
|
310
348
|
if (
|
|
@@ -313,6 +351,7 @@ function toKeyframes(property: Property, value: KeyframeValue): (number | null)[
|
|
|
313
351
|
) {
|
|
314
352
|
throw new Error(`Invalid keyframes for "${property}": ${JSON.stringify(value)}.`)
|
|
315
353
|
}
|
|
354
|
+
if (isDecorationProperty(property)) validateDecorationFrames(property, frames)
|
|
316
355
|
return frames
|
|
317
356
|
}
|
|
318
357
|
|
package/src/color.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Button, Color, Label, TextBase, isIOS, type View } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
export type ColorProperty = 'backgroundColor' | 'borderColor' | 'color'
|
|
4
|
+
export type ColorValue = string | Color
|
|
5
|
+
export type ColorKeyframes = ColorValue | readonly (ColorValue | null)[]
|
|
6
|
+
export const isColorProperty = (property: string): property is ColorProperty =>
|
|
7
|
+
property === 'backgroundColor' || property === 'borderColor' || property === 'color'
|
|
8
|
+
|
|
9
|
+
export function parseColor(value: unknown): number {
|
|
10
|
+
if (value instanceof Color) return value.argb >>> 0
|
|
11
|
+
if (typeof value !== 'string' || !Color.isValid(value))
|
|
12
|
+
throw new TypeError(`Invalid animation color: ${String(value)}.`)
|
|
13
|
+
return new Color(value).argb >>> 0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readColor(view: View, property: ColorProperty): number {
|
|
17
|
+
const value = property === 'borderColor' ? view.borderTopColor : view[property]
|
|
18
|
+
return value instanceof Color
|
|
19
|
+
? value.argb >>> 0
|
|
20
|
+
: property === 'backgroundColor'
|
|
21
|
+
? 0
|
|
22
|
+
: 0xff000000
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateColorTarget(view: View, property: ColorProperty) {
|
|
26
|
+
if (
|
|
27
|
+
property === 'color' &&
|
|
28
|
+
!(isIOS ? view instanceof Label || view instanceof Button : view instanceof TextBase)
|
|
29
|
+
)
|
|
30
|
+
throw new TypeError(
|
|
31
|
+
'Text color animation requires a Label or Button on iOS, or a TextBase on Android.',
|
|
32
|
+
)
|
|
33
|
+
if (property === 'borderColor') {
|
|
34
|
+
const colors = [
|
|
35
|
+
view.borderTopColor,
|
|
36
|
+
view.borderRightColor,
|
|
37
|
+
view.borderBottomColor,
|
|
38
|
+
view.borderLeftColor,
|
|
39
|
+
]
|
|
40
|
+
if (
|
|
41
|
+
colors.some(
|
|
42
|
+
(color) => (color?.argb ?? 0xff000000) !== (colors[0]?.argb ?? 0xff000000),
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
throw new TypeError(
|
|
46
|
+
'borderColor animation requires the same starting color on every edge.',
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const clamp = (value: number) => Math.max(0, Math.min(1, value))
|
|
52
|
+
const linear = (channel: number) =>
|
|
53
|
+
channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4
|
|
54
|
+
const encoded = (channel: number) =>
|
|
55
|
+
channel <= 0.0031308 ? 12.92 * channel : 1.055 * channel ** (1 / 2.4) - 0.055
|
|
56
|
+
|
|
57
|
+
function lab(value: number) {
|
|
58
|
+
const r = linear(((value >>> 16) & 255) / 255)
|
|
59
|
+
const g = linear(((value >>> 8) & 255) / 255)
|
|
60
|
+
const b = linear((value & 255) / 255)
|
|
61
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b)
|
|
62
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b)
|
|
63
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b)
|
|
64
|
+
return [
|
|
65
|
+
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
66
|
+
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
67
|
+
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
68
|
+
] as const
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Premultiplied OKLab interpolation, clipped to the sRGB output gamut. */
|
|
72
|
+
export function colorMixer(from: number, to: number): (progress: number) => number {
|
|
73
|
+
const a = lab(from),
|
|
74
|
+
b = lab(to)
|
|
75
|
+
const alphaA = (from >>> 24) / 255,
|
|
76
|
+
alphaB = (to >>> 24) / 255
|
|
77
|
+
return (progress) => {
|
|
78
|
+
const p = clamp(progress)
|
|
79
|
+
if (p === 0) return from
|
|
80
|
+
if (p === 1) return to
|
|
81
|
+
const alpha = alphaA + (alphaB - alphaA) * p
|
|
82
|
+
if (alpha <= 0) return 0
|
|
83
|
+
const channel = (i: number) => (a[i]! * alphaA * (1 - p) + b[i]! * alphaB * p) / alpha
|
|
84
|
+
const L = channel(0),
|
|
85
|
+
A = channel(1),
|
|
86
|
+
B = channel(2)
|
|
87
|
+
const l = (L + 0.3963377774 * A + 0.2158037573 * B) ** 3
|
|
88
|
+
const m = (L - 0.1055613458 * A - 0.0638541728 * B) ** 3
|
|
89
|
+
const s = (L - 0.0894841775 * A - 1.291485548 * B) ** 3
|
|
90
|
+
const r = Math.round(
|
|
91
|
+
clamp(encoded(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s)) * 255,
|
|
92
|
+
)
|
|
93
|
+
const g = Math.round(
|
|
94
|
+
clamp(encoded(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s)) * 255,
|
|
95
|
+
)
|
|
96
|
+
const blue = Math.round(
|
|
97
|
+
clamp(encoded(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)) * 255,
|
|
98
|
+
)
|
|
99
|
+
return ((Math.round(alpha * 255) << 24) | (r << 16) | (g << 8) | blue) >>> 0
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { preserveZeroShadow } from './driver/ios-shadow.js'
|
|
2
|
+
import { Color, Utils, isAndroid, type View } from '@nativescript/core'
|
|
3
|
+
|
|
4
|
+
type Shadow = Exclude<View['boxShadow'], string>[number]
|
|
5
|
+
export type DecorationProperty =
|
|
6
|
+
| 'borderRadius'
|
|
7
|
+
| 'borderWidth'
|
|
8
|
+
| 'shadowOpacity'
|
|
9
|
+
| 'shadowRadius'
|
|
10
|
+
| 'shadowOffsetX'
|
|
11
|
+
| 'shadowOffsetY'
|
|
12
|
+
| 'elevation'
|
|
13
|
+
| 'translationZ'
|
|
14
|
+
|
|
15
|
+
export const decorationProperties: readonly DecorationProperty[] = [
|
|
16
|
+
'borderRadius',
|
|
17
|
+
'borderWidth',
|
|
18
|
+
'shadowOpacity',
|
|
19
|
+
'shadowRadius',
|
|
20
|
+
'shadowOffsetX',
|
|
21
|
+
'shadowOffsetY',
|
|
22
|
+
'elevation',
|
|
23
|
+
'translationZ',
|
|
24
|
+
]
|
|
25
|
+
export const radiusKeys = [
|
|
26
|
+
'borderTopLeftRadius',
|
|
27
|
+
'borderTopRightRadius',
|
|
28
|
+
'borderBottomRightRadius',
|
|
29
|
+
'borderBottomLeftRadius',
|
|
30
|
+
] as const
|
|
31
|
+
export const widthKeys = [
|
|
32
|
+
'borderTopWidth',
|
|
33
|
+
'borderRightWidth',
|
|
34
|
+
'borderBottomWidth',
|
|
35
|
+
'borderLeftWidth',
|
|
36
|
+
] as const
|
|
37
|
+
const writing = new WeakSet<View>()
|
|
38
|
+
export const isWritingDecoration = (view: View) => writing.has(view)
|
|
39
|
+
export const isDecorationProperty = (key: string): key is DecorationProperty =>
|
|
40
|
+
decorationProperties.includes(key as DecorationProperty)
|
|
41
|
+
|
|
42
|
+
/** Core lengths may be numbers, px strings or parsed { value, unit } lengths. */
|
|
43
|
+
export function dip(value: unknown): number {
|
|
44
|
+
if (value === undefined || value === null) return 0
|
|
45
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
const match = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(dip|px)?$/.exec(value.trim())
|
|
48
|
+
if (match)
|
|
49
|
+
return Number(match[1]) / (match[2] === 'px' ? Utils.layout.getDisplayDensity() : 1)
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'object' && 'value' in value && 'unit' in value) {
|
|
52
|
+
const length = value as { value: number; unit: string }
|
|
53
|
+
if (Number.isFinite(length.value) && (length.unit === 'dip' || length.unit === 'px'))
|
|
54
|
+
return length.value / (length.unit === 'px' ? Utils.layout.getDisplayDensity() : 1)
|
|
55
|
+
}
|
|
56
|
+
throw new TypeError('Decoration animations require finite DIP or px lengths.')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function singleShadow(view: View): Shadow {
|
|
60
|
+
const shadows = view.boxShadow
|
|
61
|
+
if (!Array.isArray(shadows) || shadows.length !== 1 || shadows[0]!.inset)
|
|
62
|
+
throw new TypeError('Shadow animation requires one existing outer boxShadow.')
|
|
63
|
+
return shadows[0]!
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function validateDecorationTarget(view: View, property: DecorationProperty) {
|
|
67
|
+
const shadow = property.startsWith('shadow')
|
|
68
|
+
const z = property === 'elevation' || property === 'translationZ'
|
|
69
|
+
if ((shadow && isAndroid) || (z && !isAndroid))
|
|
70
|
+
throw new TypeError(`"${property}" is only supported on ${z ? 'Android' : 'iOS'}.`)
|
|
71
|
+
if (z) return
|
|
72
|
+
if (view.style.clipPath || view.style.cornerShape === 'squircle')
|
|
73
|
+
throw new TypeError(
|
|
74
|
+
'Decoration animations require circular corners without a clipPath.',
|
|
75
|
+
)
|
|
76
|
+
for (const keys of [radiusKeys, widthKeys]) {
|
|
77
|
+
const values = keys.map((key) => dip(view[key]))
|
|
78
|
+
if (values.some((value) => value !== values[0]))
|
|
79
|
+
throw new TypeError(
|
|
80
|
+
'Decoration animations require uniform border widths and radii.',
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
const colors = [
|
|
84
|
+
view.borderTopColor,
|
|
85
|
+
view.borderRightColor,
|
|
86
|
+
view.borderBottomColor,
|
|
87
|
+
view.borderLeftColor,
|
|
88
|
+
].map((color) => color?.argb ?? 0)
|
|
89
|
+
if (colors.some((color) => color !== colors[0]))
|
|
90
|
+
throw new TypeError('Decoration animations require uniform border colors.')
|
|
91
|
+
if (shadow) {
|
|
92
|
+
const value = singleShadow(view)
|
|
93
|
+
for (const length of [
|
|
94
|
+
value.offsetX,
|
|
95
|
+
value.offsetY,
|
|
96
|
+
value.blurRadius,
|
|
97
|
+
value.spreadRadius,
|
|
98
|
+
])
|
|
99
|
+
dip(length)
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
property === 'borderRadius' &&
|
|
103
|
+
isAndroid &&
|
|
104
|
+
view.boxShadow &&
|
|
105
|
+
(!Array.isArray(view.boxShadow) || view.boxShadow.length)
|
|
106
|
+
)
|
|
107
|
+
throw new TypeError(
|
|
108
|
+
'Animate Android radius with elevation shadows; CSS boxShadow caches its shape.',
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function validateDecorationFrames(
|
|
113
|
+
property: DecorationProperty,
|
|
114
|
+
frames: readonly (number | null)[],
|
|
115
|
+
) {
|
|
116
|
+
if (
|
|
117
|
+
frames.some((value) => value !== null && clampDecoration(property, value) !== value)
|
|
118
|
+
)
|
|
119
|
+
throw new RangeError(`Invalid keyframes for "${property}".`)
|
|
120
|
+
}
|
|
121
|
+
export function clampDecoration(property: DecorationProperty, value: number): number {
|
|
122
|
+
if (property === 'shadowOpacity') return Math.max(0, Math.min(1, value))
|
|
123
|
+
return property === 'borderRadius' ||
|
|
124
|
+
property === 'borderWidth' ||
|
|
125
|
+
property === 'shadowRadius' ||
|
|
126
|
+
property === 'elevation'
|
|
127
|
+
? Math.max(0, value)
|
|
128
|
+
: value
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function readDecoration(view: View, property: DecorationProperty): number {
|
|
132
|
+
if (property === 'borderRadius') {
|
|
133
|
+
const radius = !isAndroid ? view.nativeViewProtected?.layer?.cornerRadius : undefined
|
|
134
|
+
return typeof radius === 'number' ? radius : dip(view.borderTopLeftRadius)
|
|
135
|
+
}
|
|
136
|
+
if (property === 'borderWidth') return dip(view.borderTopWidth)
|
|
137
|
+
if (property === 'elevation')
|
|
138
|
+
return view.nativeViewProtected?.getElevation
|
|
139
|
+
? view.nativeViewProtected.getElevation() / Utils.layout.getDisplayDensity()
|
|
140
|
+
: view.androidElevation || 0
|
|
141
|
+
if (property === 'translationZ')
|
|
142
|
+
return (
|
|
143
|
+
view.nativeViewProtected?.getTranslationZ?.() / Utils.layout.getDisplayDensity() ||
|
|
144
|
+
0
|
|
145
|
+
)
|
|
146
|
+
const shadow = singleShadow(view)
|
|
147
|
+
if (property === 'shadowOpacity')
|
|
148
|
+
return ((shadow.color?.argb ?? 0xff000000) >>> 24) / 255
|
|
149
|
+
return property === 'shadowRadius'
|
|
150
|
+
? dip(shadow.blurRadius) / 2
|
|
151
|
+
: dip(property === 'shadowOffsetX' ? shadow.offsetX : shadow.offsetY)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function writeDecoration(view: View, property: DecorationProperty, raw: number) {
|
|
155
|
+
const value = clampDecoration(property, raw)
|
|
156
|
+
writing.add(view)
|
|
157
|
+
try {
|
|
158
|
+
if (property === 'borderRadius' || property === 'borderWidth') view[property] = value
|
|
159
|
+
else if (property === 'elevation') view.androidElevation = value
|
|
160
|
+
else if (property === 'translationZ')
|
|
161
|
+
view.nativeViewProtected?.setTranslationZ?.(
|
|
162
|
+
value * Utils.layout.getDisplayDensity(),
|
|
163
|
+
)
|
|
164
|
+
else {
|
|
165
|
+
const shadow = { ...singleShadow(view) }
|
|
166
|
+
if (property === 'shadowOpacity')
|
|
167
|
+
shadow.color = new Color(
|
|
168
|
+
((Math.round(value * 255) << 24) | ((shadow.color?.argb ?? 0) & 0xffffff)) >>>
|
|
169
|
+
0,
|
|
170
|
+
)
|
|
171
|
+
else if (property === 'shadowRadius') shadow.blurRadius = value * 2
|
|
172
|
+
else if (property === 'shadowOffsetX') shadow.offsetX = value
|
|
173
|
+
else shadow.offsetY = value
|
|
174
|
+
view.boxShadow = [shadow]
|
|
175
|
+
if (!isAndroid) preserveZeroShadow(view)
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
writing.delete(view)
|
|
179
|
+
}
|
|
180
|
+
}
|