@rafaelaugusto/jolt 0.1.0-beta.2 → 0.1.0-beta.4
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 +112 -16
- package/dist/animate.d.ts +3 -1
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +89 -3
- 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/decay.d.ts +11 -0
- package/dist/decay.d.ts.map +1 -0
- package/dist/decay.js +22 -0
- package/dist/decay.js.map +1 -0
- package/dist/driver/android-color.d.ts +17 -0
- package/dist/driver/android-color.d.ts.map +1 -0
- package/dist/driver/android-color.js +134 -0
- package/dist/driver/android-color.js.map +1 -0
- package/dist/driver/android.d.ts +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +101 -5
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +4 -2
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.d.ts +14 -0
- package/dist/driver/ios-color.d.ts.map +1 -0
- package/dist/driver/ios-color.js +381 -0
- package/dist/driver/ios-color.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +9 -1
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +15 -8
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- 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/loader.d.ts +19 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +172 -0
- package/dist/loader.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 -3
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +13 -2
- package/dist/properties.js.map +1 -1
- package/dist/reveal.d.ts +25 -0
- package/dist/reveal.d.ts.map +1 -0
- package/dist/reveal.js +293 -0
- package/dist/reveal.js.map +1 -0
- package/dist/text-layout.d.ts +22 -0
- package/dist/text-layout.d.ts.map +1 -0
- package/dist/text-layout.js +179 -0
- package/dist/text-layout.js.map +1 -0
- package/dist/text.d.ts +17 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +267 -0
- package/dist/text.js.map +1 -0
- package/dist/track.d.ts +1 -1
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +63 -5
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts +5 -0
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +8 -2
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +19 -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/proguard-rules.pro +6 -0
- package/src/animate.ts +105 -5
- package/src/color.ts +101 -0
- package/src/decay.ts +45 -0
- package/src/driver/android-color.ts +168 -0
- package/src/driver/android.ts +120 -10
- package/src/driver/index.ts +9 -2
- package/src/driver/ios-color.ts +432 -0
- package/src/driver/ios.ts +13 -3
- package/src/engine.ts +17 -9
- package/src/index.ts +10 -1
- package/src/liquid-grid.ts +28 -0
- package/src/liquid.ts +282 -0
- package/src/loader.ts +201 -0
- package/src/press.ts +8 -2
- package/src/properties.ts +16 -6
- package/src/reveal.ts +374 -0
- package/src/text-layout.ts +216 -0
- package/src/text.ts +298 -0
- package/src/track.ts +105 -5
- package/src/transition.ts +16 -2
- package/src/types.ts +18 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
package org.rafaelaugusto.jolt;
|
|
2
|
+
|
|
3
|
+
import android.graphics.PorterDuff;
|
|
4
|
+
import android.graphics.drawable.ColorDrawable;
|
|
5
|
+
import android.graphics.drawable.Drawable;
|
|
6
|
+
import android.view.View;
|
|
7
|
+
import android.widget.TextView;
|
|
8
|
+
import java.lang.ref.WeakReference;
|
|
9
|
+
import java.lang.reflect.Field;
|
|
10
|
+
import java.lang.reflect.Method;
|
|
11
|
+
|
|
12
|
+
/** ObjectAnimator target. All frame updates stay in native code. */
|
|
13
|
+
public final class ColorTarget {
|
|
14
|
+
private static final String BORDER = "org.nativescript.widgets.BorderDrawable";
|
|
15
|
+
private static final Field[] FIELDS = fields();
|
|
16
|
+
private static final String SHADOW = "org.nativescript.widgets.BoxShadowDrawable";
|
|
17
|
+
private static final Method UNWRAP = unwrapMethod();
|
|
18
|
+
private final WeakReference<View> view;
|
|
19
|
+
private final int property;
|
|
20
|
+
private int color;
|
|
21
|
+
|
|
22
|
+
// 0 = background, 1 = uniform border, 2 = text.
|
|
23
|
+
public ColorTarget(View view, int property, int initialColor) {
|
|
24
|
+
this.view = new WeakReference<>(view);
|
|
25
|
+
this.property = property;
|
|
26
|
+
this.color = initialColor;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private static Field[] fields() {
|
|
30
|
+
try {
|
|
31
|
+
Class<?> type = Class.forName(BORDER);
|
|
32
|
+
String[] names = {"backgroundColor", "borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor"};
|
|
33
|
+
Field[] result = new Field[names.length];
|
|
34
|
+
for (int i = 0; i < names.length; i++) {
|
|
35
|
+
result[i] = type.getDeclaredField(names[i]);
|
|
36
|
+
result[i].setAccessible(true);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
} catch (ReflectiveOperationException | SecurityException exception) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private static Method unwrapMethod() {
|
|
45
|
+
try {
|
|
46
|
+
return Class.forName(SHADOW).getMethod("getWrappedDrawable");
|
|
47
|
+
} catch (ReflectiveOperationException exception) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private Drawable background(View nativeView) {
|
|
53
|
+
Drawable drawable = nativeView.getBackground();
|
|
54
|
+
// Avoid reflective method lookups and exceptions in the frame loop.
|
|
55
|
+
if (drawable != null && drawable.getClass().getName().equals(SHADOW) && UNWRAP != null) {
|
|
56
|
+
try {
|
|
57
|
+
return (Drawable) UNWRAP.invoke(drawable);
|
|
58
|
+
} catch (ReflectiveOperationException exception) {
|
|
59
|
+
return drawable;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return drawable;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public boolean isSupported() {
|
|
66
|
+
View nativeView = view.get();
|
|
67
|
+
if (nativeView == null) return false;
|
|
68
|
+
if (property == 2) return nativeView instanceof TextView;
|
|
69
|
+
Drawable drawable = background(nativeView);
|
|
70
|
+
if (drawable == null) return true;
|
|
71
|
+
if (drawable.getClass().getName().equals(SHADOW)) return false;
|
|
72
|
+
return !drawable.getClass().getName().equals(BORDER) || FIELDS != null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public int getColor() { return color; }
|
|
76
|
+
|
|
77
|
+
public void setColor(int value) {
|
|
78
|
+
View nativeView = view.get();
|
|
79
|
+
if (nativeView == null) return;
|
|
80
|
+
color = value;
|
|
81
|
+
if (property == 2) {
|
|
82
|
+
((TextView) nativeView).setTextColor(value);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Resolve on every frame: a Core style update may replace the drawable.
|
|
86
|
+
Drawable drawable = background(nativeView);
|
|
87
|
+
if (drawable == null || drawable.getClass().getName().equals(SHADOW)) return;
|
|
88
|
+
if (drawable.getClass().getName().equals(BORDER)) {
|
|
89
|
+
if (FIELDS == null) return;
|
|
90
|
+
try {
|
|
91
|
+
if (property == 0) FIELDS[0].setInt(drawable, value);
|
|
92
|
+
else for (int i = 1; i < FIELDS.length; i++) FIELDS[i].setInt(drawable, value);
|
|
93
|
+
drawable.invalidateSelf();
|
|
94
|
+
} catch (IllegalAccessException exception) {
|
|
95
|
+
// Unsupported Core versions keep the final model color.
|
|
96
|
+
}
|
|
97
|
+
} else if (property == 0) {
|
|
98
|
+
if (drawable instanceof ColorDrawable) ((ColorDrawable) drawable).setColor(value);
|
|
99
|
+
else drawable.setColorFilter(value, PorterDuff.Mode.SRC_IN);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
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
|
+
}
|
package/src/animate.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { View } from '@nativescript/core'
|
|
2
|
+
import {
|
|
3
|
+
isColorProperty,
|
|
4
|
+
parseColor,
|
|
5
|
+
validateColorTarget,
|
|
6
|
+
type ColorKeyframes,
|
|
7
|
+
} from './color.js'
|
|
8
|
+
import { DEFAULT_FRICTION, solveDecay } from './decay.js'
|
|
2
9
|
import {
|
|
3
10
|
arrivalOf,
|
|
4
11
|
setArrival,
|
|
@@ -9,7 +16,7 @@ import {
|
|
|
9
16
|
} from './engine.js'
|
|
10
17
|
import { getPreset, type PresetName } from './presets.js'
|
|
11
18
|
import { resolveHaptic, type Haptic } from './haptics.js'
|
|
12
|
-
import { isProperty, type Property } from './properties.js'
|
|
19
|
+
import { isProperty, precision, type Property } from './properties.js'
|
|
13
20
|
import {
|
|
14
21
|
hasCurve,
|
|
15
22
|
mergeTransitions,
|
|
@@ -20,6 +27,8 @@ import {
|
|
|
20
27
|
} from './transition.js'
|
|
21
28
|
import type {
|
|
22
29
|
AnimateOptions,
|
|
30
|
+
DecayOptions,
|
|
31
|
+
DecayVelocity,
|
|
23
32
|
Keyframes,
|
|
24
33
|
KeyframeValue,
|
|
25
34
|
Stagger,
|
|
@@ -56,14 +65,20 @@ export function animate(
|
|
|
56
65
|
const feedback = resolveHaptic(haptic)
|
|
57
66
|
|
|
58
67
|
if (!isList(target)) {
|
|
68
|
+
const view = toView(target)
|
|
69
|
+
for (const property of Object.keys(keyframes))
|
|
70
|
+
if (isColorProperty(property)) validateColorTarget(view, property)
|
|
59
71
|
return new ViewAnimation(
|
|
60
|
-
|
|
72
|
+
view,
|
|
61
73
|
plan(keyframes, base, overrides, 0, relative),
|
|
62
74
|
feedback,
|
|
63
75
|
)
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
const views = target.map(toView)
|
|
79
|
+
for (const view of views)
|
|
80
|
+
for (const property of Object.keys(keyframes))
|
|
81
|
+
if (isColorProperty(property)) validateColorTarget(view, property)
|
|
67
82
|
return group(
|
|
68
83
|
views.map(
|
|
69
84
|
(view, index) =>
|
|
@@ -82,6 +97,77 @@ export function animate(
|
|
|
82
97
|
)
|
|
83
98
|
}
|
|
84
99
|
|
|
100
|
+
/** Starts at the current translation and slows down from a velocity in DIPs per second. */
|
|
101
|
+
export function decay(
|
|
102
|
+
target: Target | readonly Target[],
|
|
103
|
+
velocity: DecayVelocity,
|
|
104
|
+
options: DecayOptions = {},
|
|
105
|
+
): Animation {
|
|
106
|
+
const friction = options.friction ?? DEFAULT_FRICTION
|
|
107
|
+
if (!Number.isFinite(friction) || friction <= 0 || friction >= 1) {
|
|
108
|
+
throw new RangeError('Decay friction must be between 0 and 1 exclusively.')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const stops = (values: readonly number[] | undefined) => {
|
|
112
|
+
if (values === undefined) return undefined
|
|
113
|
+
if (
|
|
114
|
+
!Array.isArray(values) ||
|
|
115
|
+
values.length === 0 ||
|
|
116
|
+
![...values].every(Number.isFinite)
|
|
117
|
+
) {
|
|
118
|
+
throw new RangeError('Decay snap needs a nonempty array of finite positions.')
|
|
119
|
+
}
|
|
120
|
+
return [...values]
|
|
121
|
+
}
|
|
122
|
+
const snap = options.snap
|
|
123
|
+
if (
|
|
124
|
+
snap !== undefined &&
|
|
125
|
+
!Array.isArray(snap) &&
|
|
126
|
+
(snap === null ||
|
|
127
|
+
typeof snap !== 'object' ||
|
|
128
|
+
Object.keys(snap).some((key) => key !== 'x' && key !== 'y'))
|
|
129
|
+
) {
|
|
130
|
+
throw new TypeError('Decay snap must be an array or an object with x and y stops.')
|
|
131
|
+
}
|
|
132
|
+
const shared = Array.isArray(snap)
|
|
133
|
+
const perAxis = snap as { x?: readonly number[]; y?: readonly number[] } | undefined
|
|
134
|
+
const snapped = shared ? stops(snap) : undefined
|
|
135
|
+
const snapX = shared ? snapped : stops(perAxis?.x)
|
|
136
|
+
const snapY = shared ? snapped : stops(perAxis?.y)
|
|
137
|
+
|
|
138
|
+
const plans = Object.entries(velocity).flatMap<Plan>(([property, value]) => {
|
|
139
|
+
if (property !== 'x' && property !== 'y') {
|
|
140
|
+
throw new Error(`Can't decay "${property}". Use x or y.`)
|
|
141
|
+
}
|
|
142
|
+
if (value === undefined) return []
|
|
143
|
+
if (!Number.isFinite(value)) {
|
|
144
|
+
throw new RangeError(`Invalid decay velocity for "${property}".`)
|
|
145
|
+
}
|
|
146
|
+
// Validate every axis before handing any plans to the engine.
|
|
147
|
+
solveDecay(0, value, friction, precision[property])
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
property,
|
|
151
|
+
keyframes: [null, null],
|
|
152
|
+
transition: {
|
|
153
|
+
curve: {
|
|
154
|
+
type: 'decay',
|
|
155
|
+
friction,
|
|
156
|
+
velocity: value,
|
|
157
|
+
snap: property === 'x' ? snapX : snapY,
|
|
158
|
+
},
|
|
159
|
+
delay: 0,
|
|
160
|
+
repeat: 0,
|
|
161
|
+
reverse: false,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
]
|
|
165
|
+
})
|
|
166
|
+
const views = toViews(target)
|
|
167
|
+
if (plans.length === 0) return group([])
|
|
168
|
+
return group(views.map((view) => new ViewAnimation(view, plans)))
|
|
169
|
+
}
|
|
170
|
+
|
|
85
171
|
/** Stops all or selected properties at their current values. `scale` stops both axes. */
|
|
86
172
|
export function stop(
|
|
87
173
|
target: Target | readonly Target[],
|
|
@@ -208,7 +294,10 @@ export function startingValues(
|
|
|
208
294
|
const { keyframes, relative } = prepare(animation, options)
|
|
209
295
|
if (!keyframes) throw new Error(`Unknown preset "${animation}".`)
|
|
210
296
|
return new Map(
|
|
211
|
-
expand(keyframes).map(([property, [first]]) => [
|
|
297
|
+
expand(keyframes).map(([property, [first]]) => [
|
|
298
|
+
property,
|
|
299
|
+
relative && !isColorProperty(property) ? null : first!,
|
|
300
|
+
]),
|
|
212
301
|
)
|
|
213
302
|
}
|
|
214
303
|
|
|
@@ -216,7 +305,7 @@ function expand(keyframes: Keyframes): [Property, (number | null)[]][] {
|
|
|
216
305
|
const expanded: [Property, (number | null)[]][] = []
|
|
217
306
|
for (const [key, value] of Object.entries(keyframes) as [
|
|
218
307
|
keyof Keyframes,
|
|
219
|
-
KeyframeValue | undefined,
|
|
308
|
+
KeyframeValue | ColorKeyframes | undefined,
|
|
220
309
|
][]) {
|
|
221
310
|
if (value === undefined) continue
|
|
222
311
|
if (key !== 'scale' && !isProperty(key)) throw new Error(`Can't animate "${key}".`)
|
|
@@ -230,7 +319,18 @@ function expand(keyframes: Keyframes): [Property, (number | null)[]][] {
|
|
|
230
319
|
return expanded
|
|
231
320
|
}
|
|
232
321
|
|
|
233
|
-
function toKeyframes(
|
|
322
|
+
function toKeyframes(
|
|
323
|
+
property: Property,
|
|
324
|
+
value: KeyframeValue | ColorKeyframes,
|
|
325
|
+
): (number | null)[] {
|
|
326
|
+
if (isColorProperty(property)) {
|
|
327
|
+
const frames = Array.isArray(value) ? [...value] : [null, value]
|
|
328
|
+
if (!frames.length) throw new TypeError(`Empty color keyframes for "${property}".`)
|
|
329
|
+
if (frames.length === 1) frames.unshift(null)
|
|
330
|
+
return frames.map((frame) => (frame === null ? null : parseColor(frame)))
|
|
331
|
+
}
|
|
332
|
+
if (typeof value !== 'number' && !Array.isArray(value))
|
|
333
|
+
throw new TypeError(`Invalid numeric keyframes for "${property}".`)
|
|
234
334
|
const frames = typeof value === 'number' ? [null, value] : [...value]
|
|
235
335
|
if (frames.length === 1) frames.unshift(null)
|
|
236
336
|
if (
|
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
|
+
}
|
package/src/decay.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface DecayMotion {
|
|
2
|
+
duration: number
|
|
3
|
+
to: number
|
|
4
|
+
position(time: number): number
|
|
5
|
+
velocity(time: number): number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Velocity retains this fraction every millisecond, independently of the display's refresh rate. */
|
|
9
|
+
export const DEFAULT_FRICTION = 0.998
|
|
10
|
+
|
|
11
|
+
/** Exponential motion, with velocity in units per second and time in milliseconds. */
|
|
12
|
+
export function solveDecay(
|
|
13
|
+
from: number,
|
|
14
|
+
velocity: number,
|
|
15
|
+
friction: number,
|
|
16
|
+
precision: number,
|
|
17
|
+
): DecayMotion {
|
|
18
|
+
const rate = -Math.log(friction)
|
|
19
|
+
const distance = velocity / (1000 * rate)
|
|
20
|
+
const to = from + distance
|
|
21
|
+
// Stop only when both the remaining distance and speed are imperceptible.
|
|
22
|
+
const duration =
|
|
23
|
+
velocity === 0
|
|
24
|
+
? 0
|
|
25
|
+
: Math.ceil(
|
|
26
|
+
Math.max(
|
|
27
|
+
0,
|
|
28
|
+
Math.log(Math.abs(distance) / precision) / rate,
|
|
29
|
+
Math.log(Math.abs(velocity) / (precision * 10)) / rate,
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if (!Number.isFinite(to) || !Number.isFinite(duration) || duration > 2_147_483_647) {
|
|
34
|
+
throw new RangeError('Decay velocity and friction produce an unplayable motion.')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
duration,
|
|
39
|
+
to,
|
|
40
|
+
position: (time) =>
|
|
41
|
+
time >= duration ? to : from - distance * Math.expm1(-rate * Math.max(0, time)),
|
|
42
|
+
velocity: (time) =>
|
|
43
|
+
time >= duration ? 0 : velocity * Math.exp(-rate * Math.max(0, time)),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { type View } from '@nativescript/core'
|
|
2
|
+
import { readColor, type ColorProperty } from '../color.js'
|
|
3
|
+
import type { Track } from '../track.js'
|
|
4
|
+
|
|
5
|
+
interface NativeTarget {
|
|
6
|
+
getColor(): number
|
|
7
|
+
setColor(value: number): void
|
|
8
|
+
isSupported(): boolean
|
|
9
|
+
}
|
|
10
|
+
interface ColorHandle {
|
|
11
|
+
cancel(): void
|
|
12
|
+
elapsed(): number
|
|
13
|
+
target: NativeTarget
|
|
14
|
+
}
|
|
15
|
+
const playing = new WeakMap<View, Map<ColorProperty, ColorHandle>>()
|
|
16
|
+
|
|
17
|
+
export function presentedAndroidColor(view: View, property: ColorProperty): number {
|
|
18
|
+
const active = playing.get(view)?.get(property)
|
|
19
|
+
if (active) return active.target.getColor() >>> 0
|
|
20
|
+
if (property === 'color' && view.nativeViewProtected instanceof android.widget.TextView)
|
|
21
|
+
return view.nativeViewProtected.getCurrentTextColor() >>> 0
|
|
22
|
+
return readColor(view, property)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function playAndroidColor(
|
|
26
|
+
view: View,
|
|
27
|
+
track: Track,
|
|
28
|
+
onEnd: (finished?: boolean) => void,
|
|
29
|
+
): ColorHandle {
|
|
30
|
+
const property = track.property as ColorProperty
|
|
31
|
+
const changes =
|
|
32
|
+
property === 'borderColor'
|
|
33
|
+
? [
|
|
34
|
+
'borderTopColorChange',
|
|
35
|
+
'borderRightColorChange',
|
|
36
|
+
'borderBottomColorChange',
|
|
37
|
+
'borderLeftColorChange',
|
|
38
|
+
]
|
|
39
|
+
: [`${property}Change`]
|
|
40
|
+
const bridge = globalThis as unknown as {
|
|
41
|
+
org: {
|
|
42
|
+
rafaelaugusto: {
|
|
43
|
+
jolt: {
|
|
44
|
+
ColorTarget: new (
|
|
45
|
+
view: android.view.View,
|
|
46
|
+
property: number,
|
|
47
|
+
color: number,
|
|
48
|
+
) => NativeTarget
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
let target: NativeTarget
|
|
54
|
+
try {
|
|
55
|
+
target = new bridge.org.rafaelaugusto.jolt.ColorTarget(
|
|
56
|
+
view.nativeViewProtected,
|
|
57
|
+
property === 'backgroundColor' ? 0 : property === 'borderColor' ? 1 : 2,
|
|
58
|
+
track.values[0]! | 0,
|
|
59
|
+
)
|
|
60
|
+
} catch {
|
|
61
|
+
// A JS-only update cannot install the native plugin. Keep the final model.
|
|
62
|
+
onEnd(false)
|
|
63
|
+
return {
|
|
64
|
+
cancel() {},
|
|
65
|
+
elapsed: () => 0,
|
|
66
|
+
target: {
|
|
67
|
+
getColor: () => readColor(view, property),
|
|
68
|
+
setColor() {},
|
|
69
|
+
isSupported: () => false,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const frames = Array.create(android.animation.Keyframe, track.values.length)
|
|
74
|
+
track.values.forEach((value, index) => {
|
|
75
|
+
frames[index] = android.animation.Keyframe.ofInt(track.times[index]!, value | 0)
|
|
76
|
+
})
|
|
77
|
+
const holder = android.animation.PropertyValuesHolder.ofKeyframe('color', frames)
|
|
78
|
+
holder.setEvaluator(new android.animation.ArgbEvaluator())
|
|
79
|
+
const holders = Array.create(android.animation.PropertyValuesHolder, 1)
|
|
80
|
+
holders[0] = holder
|
|
81
|
+
const animator = android.animation.ObjectAnimator.ofPropertyValuesHolder(
|
|
82
|
+
target as unknown as java.lang.Object,
|
|
83
|
+
holders,
|
|
84
|
+
)
|
|
85
|
+
animator.setDuration(Math.max(track.duration, 1))
|
|
86
|
+
animator.setStartDelay(track.delay)
|
|
87
|
+
animator.setInterpolator(new android.view.animation.LinearInterpolator())
|
|
88
|
+
if (track.repeat > 0) {
|
|
89
|
+
animator.setRepeatCount(
|
|
90
|
+
Number.isFinite(track.repeat)
|
|
91
|
+
? track.repeat
|
|
92
|
+
: android.animation.ValueAnimator.INFINITE,
|
|
93
|
+
)
|
|
94
|
+
animator.setRepeatMode(
|
|
95
|
+
track.reverse
|
|
96
|
+
? android.animation.ValueAnimator.REVERSE
|
|
97
|
+
: android.animation.ValueAnimator.RESTART,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
let stopped = false,
|
|
101
|
+
started = false,
|
|
102
|
+
queued = false
|
|
103
|
+
const cleanup = () => {
|
|
104
|
+
if (stopped) return
|
|
105
|
+
stopped = true
|
|
106
|
+
view.off('unloaded', cancel)
|
|
107
|
+
if (property === 'color') {
|
|
108
|
+
view.off('textChange', invalidate)
|
|
109
|
+
view.off('formattedTextChange', invalidate)
|
|
110
|
+
}
|
|
111
|
+
for (const name of changes) view.style.off(name, invalidate)
|
|
112
|
+
if (playing.get(view)?.get(property) === handle) playing.get(view)?.delete(property)
|
|
113
|
+
}
|
|
114
|
+
const cancel = () => {
|
|
115
|
+
if (stopped) return
|
|
116
|
+
cleanup()
|
|
117
|
+
animator.cancel()
|
|
118
|
+
onEnd(false)
|
|
119
|
+
}
|
|
120
|
+
const invalidate = () => {
|
|
121
|
+
if (stopped || queued) return
|
|
122
|
+
queued = true
|
|
123
|
+
void Promise.resolve().then(() => {
|
|
124
|
+
queued = false
|
|
125
|
+
if (!stopped) cancel()
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
const handle: ColorHandle = {
|
|
129
|
+
target,
|
|
130
|
+
elapsed: () => (started ? track.delay + animator.getCurrentPlayTime() : 0),
|
|
131
|
+
cancel() {
|
|
132
|
+
cleanup()
|
|
133
|
+
animator.cancel()
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
let entries = playing.get(view)
|
|
137
|
+
if (!entries) playing.set(view, (entries = new Map()))
|
|
138
|
+
entries.set(property, handle)
|
|
139
|
+
animator.addListener(
|
|
140
|
+
new android.animation.Animator.AnimatorListener({
|
|
141
|
+
onAnimationStart() {
|
|
142
|
+
started = true
|
|
143
|
+
},
|
|
144
|
+
onAnimationRepeat() {},
|
|
145
|
+
onAnimationCancel() {},
|
|
146
|
+
onAnimationEnd() {
|
|
147
|
+
if (!stopped) {
|
|
148
|
+
cleanup()
|
|
149
|
+
onEnd()
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
)
|
|
154
|
+
view.on('unloaded', cancel)
|
|
155
|
+
if (property === 'color') {
|
|
156
|
+
view.on('textChange', invalidate)
|
|
157
|
+
view.on('formattedTextChange', invalidate)
|
|
158
|
+
}
|
|
159
|
+
for (const name of changes) view.style.on(name, invalidate)
|
|
160
|
+
if (target.isSupported()) {
|
|
161
|
+
target.setColor(track.values[0]! | 0)
|
|
162
|
+
animator.start()
|
|
163
|
+
} else {
|
|
164
|
+
cleanup()
|
|
165
|
+
onEnd(false)
|
|
166
|
+
}
|
|
167
|
+
return handle
|
|
168
|
+
}
|