@rafaelaugusto/jolt 0.1.0-beta.4 → 0.1.0-beta.6
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 +134 -4
- package/dist/animate.d.ts +7 -4
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +58 -13
- package/dist/animate.js.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +34 -0
- package/dist/config.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 +3 -0
- package/dist/driver/android-color.d.ts.map +1 -1
- package/dist/driver/android-color.js +20 -18
- package/dist/driver/android-color.js.map +1 -1
- 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 +30 -36
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +9 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.d.ts +2 -0
- package/dist/driver/ios-color.d.ts.map +1 -1
- package/dist/driver/ios-color.js +19 -8
- package/dist/driver/ios-color.js.map +1 -1
- 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 +47 -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 +76 -40
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +64 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +243 -19
- 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 +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/items.d.ts +20 -0
- package/dist/items.d.ts.map +1 -0
- package/dist/items.js +109 -0
- package/dist/items.js.map +1 -0
- package/dist/keyboard.d.ts +17 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +116 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/label.d.ts +7 -0
- package/dist/label.d.ts.map +1 -0
- package/dist/label.js +60 -0
- package/dist/label.js.map +1 -0
- package/dist/liquid.d.ts.map +1 -1
- package/dist/liquid.js +2 -1
- package/dist/liquid.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +2 -1
- package/dist/loader.js.map +1 -1
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +4 -3
- package/dist/particles.js.map +1 -1
- package/dist/path.d.ts +12 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +90 -0
- package/dist/path.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 +141 -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 +3 -3
- package/dist/press.js.map +1 -1
- package/dist/properties.d.ts +2 -1
- package/dist/properties.d.ts.map +1 -1
- package/dist/properties.js +14 -1
- package/dist/properties.js.map +1 -1
- package/dist/reveal.d.ts.map +1 -1
- package/dist/reveal.js +3 -2
- package/dist/reveal.js.map +1 -1
- package/dist/roll-diff.d.ts +22 -0
- package/dist/roll-diff.d.ts.map +1 -0
- package/dist/roll-diff.js +36 -0
- package/dist/roll-diff.js.map +1 -0
- package/dist/roll.d.ts +29 -0
- package/dist/roll.d.ts.map +1 -0
- package/dist/roll.js +442 -0
- package/dist/roll.js.map +1 -0
- package/dist/scroll-linked.d.ts +10 -0
- package/dist/scroll-linked.d.ts.map +1 -0
- package/dist/scroll-linked.js +58 -0
- package/dist/scroll-linked.js.map +1 -0
- package/dist/sequence.d.ts +10 -5
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +208 -14
- package/dist/sequence.js.map +1 -1
- package/dist/shimmer.d.ts.map +1 -1
- package/dist/shimmer.js +2 -1
- package/dist/shimmer.js.map +1 -1
- package/dist/svg-path.d.ts +14 -0
- package/dist/svg-path.d.ts.map +1 -0
- package/dist/svg-path.js +281 -0
- package/dist/svg-path.js.map +1 -0
- package/dist/symbol-effect.d.ts.map +1 -1
- package/dist/symbol-effect.js +4 -3
- package/dist/symbol-effect.js.map +1 -1
- package/dist/text-layout.d.ts +8 -0
- package/dist/text-layout.d.ts.map +1 -1
- package/dist/text-layout.js +8 -2
- package/dist/text-layout.js.map +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +60 -64
- package/dist/text.js.map +1 -1
- package/dist/track.d.ts +2 -0
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +72 -16
- package/dist/track.js.map +1 -1
- package/dist/transition.d.ts.map +1 -1
- package/dist/transition.js +4 -1
- package/dist/transition.js.map +1 -1
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +89 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +134 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +15 -3
- package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +2 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
- package/platforms/android/proguard-rules.pro +7 -0
- package/src/android-plugin.d.ts +64 -0
- package/src/animate.ts +96 -33
- package/src/config.ts +46 -0
- package/src/decoration.ts +180 -0
- package/src/driver/android-color.ts +22 -20
- package/src/driver/android-decoration.ts +182 -0
- package/src/driver/android-playback.ts +157 -0
- package/src/driver/android.ts +57 -43
- package/src/driver/index.ts +15 -0
- package/src/driver/ios-color.ts +19 -10
- package/src/driver/ios-decoration.ts +332 -0
- package/src/driver/ios-playback.ts +51 -0
- package/src/driver/ios-shadow.ts +50 -0
- package/src/driver/ios.ts +87 -46
- package/src/engine.ts +267 -22
- package/src/haptics.ts +25 -2
- package/src/index.ts +12 -1
- package/src/items.ts +140 -0
- package/src/keyboard.ts +132 -0
- package/src/label.ts +76 -0
- package/src/liquid.ts +2 -1
- package/src/loader.ts +2 -1
- package/src/particles.ts +4 -4
- package/src/path.ts +124 -0
- package/src/playback-group.ts +143 -0
- package/src/playback.ts +62 -0
- package/src/press.ts +3 -5
- package/src/properties.ts +18 -2
- package/src/reveal.ts +3 -3
- package/src/roll-diff.ts +57 -0
- package/src/roll.ts +515 -0
- package/src/scroll-linked.ts +73 -0
- package/src/sequence.ts +204 -14
- package/src/shimmer.ts +2 -1
- package/src/svg-path.ts +355 -0
- package/src/symbol-effect.ts +4 -3
- package/src/text-layout.ts +14 -2
- package/src/text.ts +62 -81
- package/src/track.ts +98 -25
- package/src/transition.ts +4 -1
- package/src/types.ts +23 -1
- package/src/vue/index.ts +189 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getDriver } from './driver/index.js'
|
|
2
|
+
|
|
3
|
+
export interface JoltConfig {
|
|
4
|
+
/** Playback speed for every engine animation. `0.2` plays everything five times slower. */
|
|
5
|
+
timeScale?: number
|
|
6
|
+
/**
|
|
7
|
+
* When transforms, paths, decorations and decay jump to their end instead of animating.
|
|
8
|
+
* `user` follows the platform setting. Opacity and colors still animate.
|
|
9
|
+
*/
|
|
10
|
+
reducedMotion?: 'user' | 'always' | 'never'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const settings: Required<JoltConfig> = { timeScale: 1, reducedMotion: 'user' }
|
|
14
|
+
const preferences: readonly JoltConfig['reducedMotion'][] = ['user', 'always', 'never']
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Global settings, applied to animations that start after the call.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* configure({ timeScale: 0.2 }) // tune a spring in slow motion
|
|
21
|
+
* configure({ reducedMotion: 'always' }) // check the reduced layout
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function configure(options: JoltConfig): void {
|
|
25
|
+
if (options === null || typeof options !== 'object')
|
|
26
|
+
throw new TypeError('configure() takes an options object.')
|
|
27
|
+
const { timeScale, reducedMotion } = options
|
|
28
|
+
if (timeScale !== undefined && (!Number.isFinite(timeScale) || timeScale <= 0))
|
|
29
|
+
throw new RangeError(`Time scale must be a positive finite number, got ${timeScale}.`)
|
|
30
|
+
if (reducedMotion !== undefined && !preferences.includes(reducedMotion))
|
|
31
|
+
throw new TypeError(
|
|
32
|
+
`Reduced motion must be 'user', 'always' or 'never', got ${String(reducedMotion)}.`,
|
|
33
|
+
)
|
|
34
|
+
if (timeScale !== undefined) settings.timeScale = timeScale
|
|
35
|
+
if (reducedMotion !== undefined) settings.reducedMotion = reducedMotion
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function timeScale(): number {
|
|
39
|
+
return settings.timeScale
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Whether motion is reduced right now. `user` asks the platform each time. */
|
|
43
|
+
export function reducedMotion(): boolean {
|
|
44
|
+
if (settings.reducedMotion === 'user') return getDriver().reducedMotion?.() ?? false
|
|
45
|
+
return settings.reducedMotion === 'always'
|
|
46
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type View } from '@nativescript/core'
|
|
2
2
|
import { readColor, type ColorProperty } from '../color.js'
|
|
3
|
+
import { androidPlayback, type AndroidPlayback } from './android-playback.js'
|
|
4
|
+
import type { PlaybackCommand } from '../playback.js'
|
|
3
5
|
import type { Track } from '../track.js'
|
|
4
6
|
|
|
5
7
|
interface NativeTarget {
|
|
@@ -8,12 +10,18 @@ interface NativeTarget {
|
|
|
8
10
|
isSupported(): boolean
|
|
9
11
|
}
|
|
10
12
|
interface ColorHandle {
|
|
13
|
+
control(elapsed: number, paused: boolean, command?: PlaybackCommand): void
|
|
11
14
|
cancel(): void
|
|
12
15
|
elapsed(): number
|
|
13
16
|
target: NativeTarget
|
|
14
17
|
}
|
|
15
18
|
const playing = new WeakMap<View, Map<ColorProperty, ColorHandle>>()
|
|
16
19
|
|
|
20
|
+
export function syncAndroidColor(view: View) {
|
|
21
|
+
for (const handle of playing.get(view)?.values() ?? [])
|
|
22
|
+
handle.target.setColor(handle.target.getColor())
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
export function presentedAndroidColor(view: View, property: ColorProperty): number {
|
|
18
26
|
const active = playing.get(view)?.get(property)
|
|
19
27
|
if (active) return active.target.getColor() >>> 0
|
|
@@ -62,6 +70,7 @@ export function playAndroidColor(
|
|
|
62
70
|
onEnd(false)
|
|
63
71
|
return {
|
|
64
72
|
cancel() {},
|
|
73
|
+
control() {},
|
|
65
74
|
elapsed: () => 0,
|
|
66
75
|
target: {
|
|
67
76
|
getColor: () => readColor(view, property),
|
|
@@ -98,7 +107,6 @@ export function playAndroidColor(
|
|
|
98
107
|
)
|
|
99
108
|
}
|
|
100
109
|
let stopped = false,
|
|
101
|
-
started = false,
|
|
102
110
|
queued = false
|
|
103
111
|
const cleanup = () => {
|
|
104
112
|
if (stopped) return
|
|
@@ -111,10 +119,11 @@ export function playAndroidColor(
|
|
|
111
119
|
for (const name of changes) view.style.off(name, invalidate)
|
|
112
120
|
if (playing.get(view)?.get(property) === handle) playing.get(view)?.delete(property)
|
|
113
121
|
}
|
|
122
|
+
let player: AndroidPlayback | undefined
|
|
114
123
|
const cancel = () => {
|
|
115
124
|
if (stopped) return
|
|
116
125
|
cleanup()
|
|
117
|
-
|
|
126
|
+
player?.cancel()
|
|
118
127
|
onEnd(false)
|
|
119
128
|
}
|
|
120
129
|
const invalidate = () => {
|
|
@@ -127,30 +136,18 @@ export function playAndroidColor(
|
|
|
127
136
|
}
|
|
128
137
|
const handle: ColorHandle = {
|
|
129
138
|
target,
|
|
130
|
-
elapsed: () => (
|
|
139
|
+
elapsed: () => player?.elapsed() ?? 0,
|
|
140
|
+
control(time, paused, command) {
|
|
141
|
+
player?.control(time, paused, command)
|
|
142
|
+
},
|
|
131
143
|
cancel() {
|
|
132
144
|
cleanup()
|
|
133
|
-
|
|
145
|
+
player?.cancel()
|
|
134
146
|
},
|
|
135
147
|
}
|
|
136
148
|
let entries = playing.get(view)
|
|
137
149
|
if (!entries) playing.set(view, (entries = new Map()))
|
|
138
150
|
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
151
|
view.on('unloaded', cancel)
|
|
155
152
|
if (property === 'color') {
|
|
156
153
|
view.on('textChange', invalidate)
|
|
@@ -159,7 +156,12 @@ export function playAndroidColor(
|
|
|
159
156
|
for (const name of changes) view.style.on(name, invalidate)
|
|
160
157
|
if (target.isSupported()) {
|
|
161
158
|
target.setColor(track.values[0]! | 0)
|
|
162
|
-
animator
|
|
159
|
+
player = androidPlayback(animator, track, () => {
|
|
160
|
+
if (!stopped) {
|
|
161
|
+
cleanup()
|
|
162
|
+
onEnd()
|
|
163
|
+
}
|
|
164
|
+
})
|
|
163
165
|
} else {
|
|
164
166
|
cleanup()
|
|
165
167
|
onEnd(false)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Utils, type View } from '@nativescript/core'
|
|
2
|
+
import {
|
|
3
|
+
isWritingDecoration,
|
|
4
|
+
radiusKeys,
|
|
5
|
+
widthKeys,
|
|
6
|
+
validateDecorationTarget,
|
|
7
|
+
type DecorationProperty,
|
|
8
|
+
} from '../decoration.js'
|
|
9
|
+
import { androidPlayback, type AndroidPlayback } from './android-playback.js'
|
|
10
|
+
import type { PlaybackCommand } from '../playback.js'
|
|
11
|
+
import type { Track } from '../track.js'
|
|
12
|
+
interface NativeTarget {
|
|
13
|
+
getValue(): number
|
|
14
|
+
setValue(value: number): void
|
|
15
|
+
isSupported(): boolean
|
|
16
|
+
acquire(): void
|
|
17
|
+
release(): void
|
|
18
|
+
}
|
|
19
|
+
interface Handle {
|
|
20
|
+
control(elapsed: number, paused: boolean, command?: PlaybackCommand): void
|
|
21
|
+
cancel(): void
|
|
22
|
+
elapsed(): number
|
|
23
|
+
target: NativeTarget
|
|
24
|
+
}
|
|
25
|
+
const playing = new WeakMap<View, Map<DecorationProperty, Handle>>()
|
|
26
|
+
const ids = { borderRadius: 0, borderWidth: 1, elevation: 2, translationZ: 3 } as const
|
|
27
|
+
export function syncAndroidDecoration(view: View) {
|
|
28
|
+
for (const handle of playing.get(view)?.values() ?? [])
|
|
29
|
+
handle.target.setValue(handle.target.getValue())
|
|
30
|
+
}
|
|
31
|
+
export const presentedAndroidDecoration = (view: View, property: DecorationProperty) => {
|
|
32
|
+
const target = playing.get(view)?.get(property)?.target
|
|
33
|
+
return target ? target.getValue() / Utils.layout.getDisplayDensity() : undefined
|
|
34
|
+
}
|
|
35
|
+
export function playAndroidDecoration(
|
|
36
|
+
view: View,
|
|
37
|
+
track: Track,
|
|
38
|
+
onEnd: (finished?: boolean) => void,
|
|
39
|
+
): Handle {
|
|
40
|
+
const property = track.property as keyof typeof ids
|
|
41
|
+
const density = Utils.layout.getDisplayDensity()
|
|
42
|
+
const bridge = globalThis as unknown as {
|
|
43
|
+
org: {
|
|
44
|
+
rafaelaugusto: {
|
|
45
|
+
jolt: {
|
|
46
|
+
DecorationTarget: new (
|
|
47
|
+
view: android.view.View,
|
|
48
|
+
property: number,
|
|
49
|
+
value: number,
|
|
50
|
+
borderColor: number,
|
|
51
|
+
) => NativeTarget
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
let target: NativeTarget
|
|
57
|
+
try {
|
|
58
|
+
target = new bridge.org.rafaelaugusto.jolt.DecorationTarget(
|
|
59
|
+
view.nativeViewProtected,
|
|
60
|
+
ids[property],
|
|
61
|
+
track.values[0]! * density,
|
|
62
|
+
view.borderTopColor?.argb ?? 0,
|
|
63
|
+
)
|
|
64
|
+
} catch {
|
|
65
|
+
onEnd(false)
|
|
66
|
+
return {
|
|
67
|
+
cancel() {},
|
|
68
|
+
control() {},
|
|
69
|
+
elapsed: () => 0,
|
|
70
|
+
target: {
|
|
71
|
+
getValue: () => track.to * density,
|
|
72
|
+
setValue() {},
|
|
73
|
+
isSupported: () => false,
|
|
74
|
+
acquire() {},
|
|
75
|
+
release() {},
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const frames = Array.create(android.animation.Keyframe, track.values.length)
|
|
80
|
+
track.values.forEach((value, index) => {
|
|
81
|
+
frames[index] = android.animation.Keyframe.ofFloat(
|
|
82
|
+
track.times[index]!,
|
|
83
|
+
value * density,
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
const holders = Array.create(android.animation.PropertyValuesHolder, 1)
|
|
87
|
+
holders[0] = android.animation.PropertyValuesHolder.ofKeyframe('value', frames)
|
|
88
|
+
const animator = android.animation.ObjectAnimator.ofPropertyValuesHolder(
|
|
89
|
+
target as unknown as java.lang.Object,
|
|
90
|
+
holders,
|
|
91
|
+
)
|
|
92
|
+
animator.setDuration(Math.max(track.duration, 1))
|
|
93
|
+
animator.setStartDelay(track.delay)
|
|
94
|
+
animator.setInterpolator(new android.view.animation.LinearInterpolator())
|
|
95
|
+
if (track.repeat > 0) {
|
|
96
|
+
animator.setRepeatCount(
|
|
97
|
+
Number.isFinite(track.repeat)
|
|
98
|
+
? track.repeat
|
|
99
|
+
: android.animation.ValueAnimator.INFINITE,
|
|
100
|
+
)
|
|
101
|
+
animator.setRepeatMode(
|
|
102
|
+
track.reverse
|
|
103
|
+
? android.animation.ValueAnimator.REVERSE
|
|
104
|
+
: android.animation.ValueAnimator.RESTART,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
let stopped = false
|
|
108
|
+
const changes = [
|
|
109
|
+
...radiusKeys,
|
|
110
|
+
...widthKeys,
|
|
111
|
+
'clipPath',
|
|
112
|
+
'cornerShape',
|
|
113
|
+
'boxShadow',
|
|
114
|
+
'androidElevation',
|
|
115
|
+
].map((name) => `${name}Change`)
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
if (stopped) return
|
|
118
|
+
stopped = true
|
|
119
|
+
view.off('unloaded', cancel)
|
|
120
|
+
for (const event of changes) view.style.off(event, external)
|
|
121
|
+
target.release()
|
|
122
|
+
if (playing.get(view)?.get(property) === handle) playing.get(view)?.delete(property)
|
|
123
|
+
}
|
|
124
|
+
let player: AndroidPlayback | undefined
|
|
125
|
+
const cancel = () => {
|
|
126
|
+
if (!stopped) {
|
|
127
|
+
cleanup()
|
|
128
|
+
player?.cancel()
|
|
129
|
+
onEnd(false)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const external = (event: { eventName: string }) => {
|
|
133
|
+
if (isWritingDecoration(view)) return
|
|
134
|
+
const own =
|
|
135
|
+
property === 'borderRadius'
|
|
136
|
+
? radiusKeys
|
|
137
|
+
: property === 'borderWidth'
|
|
138
|
+
? widthKeys
|
|
139
|
+
: property === 'elevation'
|
|
140
|
+
? ['androidElevation']
|
|
141
|
+
: []
|
|
142
|
+
void Promise.resolve().then(() => {
|
|
143
|
+
if (stopped) return
|
|
144
|
+
if (own.some((name) => event.eventName === `${name}Change`)) return cancel()
|
|
145
|
+
try {
|
|
146
|
+
validateDecorationTarget(view, property)
|
|
147
|
+
} catch {
|
|
148
|
+
cancel()
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
const handle: Handle = {
|
|
153
|
+
target,
|
|
154
|
+
elapsed: () => player?.elapsed() ?? 0,
|
|
155
|
+
control(time, paused, command) {
|
|
156
|
+
player?.control(time, paused, command)
|
|
157
|
+
},
|
|
158
|
+
cancel() {
|
|
159
|
+
cleanup()
|
|
160
|
+
player?.cancel()
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
const entries = playing.get(view) ?? new Map<DecorationProperty, Handle>()
|
|
164
|
+
playing.set(view, entries)
|
|
165
|
+
entries.set(property, handle)
|
|
166
|
+
view.on('unloaded', cancel)
|
|
167
|
+
for (const event of changes) view.style.on(event, external)
|
|
168
|
+
if (target.isSupported()) {
|
|
169
|
+
target.acquire()
|
|
170
|
+
target.setValue(track.values[0]! * density)
|
|
171
|
+
player = androidPlayback(animator, track, () => {
|
|
172
|
+
if (!stopped) {
|
|
173
|
+
cleanup()
|
|
174
|
+
onEnd()
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
} else {
|
|
178
|
+
cleanup()
|
|
179
|
+
onEnd(false)
|
|
180
|
+
}
|
|
181
|
+
return handle
|
|
182
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Track } from '../track.js'
|
|
2
|
+
import type { PlaybackClock, PlaybackCommand } from '../playback.js'
|
|
3
|
+
|
|
4
|
+
export interface AndroidPlayback {
|
|
5
|
+
cancel(): void
|
|
6
|
+
elapsed(): number
|
|
7
|
+
control(elapsed: number, paused: boolean, command?: PlaybackCommand): void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Reuse native keyframes when seeking, including rewinding into a start delay. */
|
|
11
|
+
export function androidPlayback(
|
|
12
|
+
animator: android.animation.ValueAnimator,
|
|
13
|
+
track: Track,
|
|
14
|
+
onEnd: () => void,
|
|
15
|
+
onStart?: () => void,
|
|
16
|
+
onRelease?: () => void,
|
|
17
|
+
): AndroidPlayback {
|
|
18
|
+
let suppressed = false
|
|
19
|
+
let stopped = false
|
|
20
|
+
let started = false
|
|
21
|
+
let acquired = false
|
|
22
|
+
let origin = 0
|
|
23
|
+
let delay = track.delay
|
|
24
|
+
let held: number | undefined
|
|
25
|
+
animator.addListener(
|
|
26
|
+
new android.animation.Animator.AnimatorListener({
|
|
27
|
+
onAnimationStart() {
|
|
28
|
+
if (!suppressed && !stopped) {
|
|
29
|
+
started = true
|
|
30
|
+
if (!acquired) {
|
|
31
|
+
acquired = true
|
|
32
|
+
onStart?.()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
onAnimationRepeat() {},
|
|
37
|
+
onAnimationCancel() {
|
|
38
|
+
if (!suppressed && !stopped) onRelease?.()
|
|
39
|
+
},
|
|
40
|
+
onAnimationEnd() {
|
|
41
|
+
if (suppressed || stopped || held !== undefined) return
|
|
42
|
+
stopped = true
|
|
43
|
+
onRelease?.()
|
|
44
|
+
onEnd()
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
const elapsed = () =>
|
|
49
|
+
held ?? (started ? origin + delay + animator.getCurrentPlayTime() : origin)
|
|
50
|
+
const control = (
|
|
51
|
+
position: number,
|
|
52
|
+
paused: boolean,
|
|
53
|
+
command: PlaybackCommand = 'seek',
|
|
54
|
+
) => {
|
|
55
|
+
if (stopped) return
|
|
56
|
+
if (command === 'pause') {
|
|
57
|
+
held = position
|
|
58
|
+
animator.pause()
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
if (command === 'play') {
|
|
62
|
+
held = undefined
|
|
63
|
+
animator.resume()
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
suppressed = true
|
|
67
|
+
animator.cancel()
|
|
68
|
+
started = false
|
|
69
|
+
origin = Math.min(position, track.delay)
|
|
70
|
+
delay = Math.max(0, track.delay - position)
|
|
71
|
+
animator.setStartDelay(delay)
|
|
72
|
+
held = paused ? position : undefined
|
|
73
|
+
suppressed = false
|
|
74
|
+
animator.start()
|
|
75
|
+
if (paused) animator.pause()
|
|
76
|
+
if (position >= track.delay)
|
|
77
|
+
animator.setCurrentPlayTime(Math.max(0, position - track.delay))
|
|
78
|
+
}
|
|
79
|
+
animator.start()
|
|
80
|
+
return {
|
|
81
|
+
elapsed,
|
|
82
|
+
control,
|
|
83
|
+
cancel() {
|
|
84
|
+
if (stopped) return
|
|
85
|
+
stopped = true
|
|
86
|
+
onRelease?.()
|
|
87
|
+
animator.cancel()
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Share one native clock among active animations. Pause it when every owner
|
|
93
|
+
* is paused, and dispose it when the last owner settles. No frame callbacks. */
|
|
94
|
+
export function androidClockFactory() {
|
|
95
|
+
let animator: android.animation.ValueAnimator | undefined
|
|
96
|
+
let owners = 0,
|
|
97
|
+
running = 0,
|
|
98
|
+
accumulated = 0
|
|
99
|
+
const now = () => accumulated + (animator?.getCurrentPlayTime() ?? 0)
|
|
100
|
+
const start = () => {
|
|
101
|
+
if (animator) return
|
|
102
|
+
const values = Array.create('float', 2)
|
|
103
|
+
values[0] = 0
|
|
104
|
+
values[1] = 1
|
|
105
|
+
animator = android.animation.ValueAnimator.ofFloat(values)
|
|
106
|
+
animator.setDuration(60_000)
|
|
107
|
+
animator.setRepeatCount(android.animation.ValueAnimator.INFINITE)
|
|
108
|
+
animator.start()
|
|
109
|
+
}
|
|
110
|
+
const stop = () => {
|
|
111
|
+
accumulated = now()
|
|
112
|
+
animator?.cancel()
|
|
113
|
+
animator = undefined
|
|
114
|
+
}
|
|
115
|
+
return (_duration: number): PlaybackClock => {
|
|
116
|
+
start()
|
|
117
|
+
owners++
|
|
118
|
+
running++
|
|
119
|
+
let offset = 0,
|
|
120
|
+
anchor = now(),
|
|
121
|
+
paused = false,
|
|
122
|
+
stopped = false
|
|
123
|
+
const time = () => offset + (paused || stopped ? 0 : now() - anchor)
|
|
124
|
+
const pause = () => {
|
|
125
|
+
if (paused || stopped) return
|
|
126
|
+
offset = time()
|
|
127
|
+
paused = true
|
|
128
|
+
// getCurrentPlayTime keeps counting while a ValueAnimator is paused and
|
|
129
|
+
// corrects itself only on its next frame. Start a fresh marker on resume
|
|
130
|
+
// so command-time reads cannot jump forward and then backward.
|
|
131
|
+
if (--running === 0) stop()
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
time,
|
|
135
|
+
pause,
|
|
136
|
+
play() {
|
|
137
|
+
if (!paused || stopped) return
|
|
138
|
+
if (running++ === 0) start()
|
|
139
|
+
anchor = now()
|
|
140
|
+
paused = false
|
|
141
|
+
},
|
|
142
|
+
seek(ms) {
|
|
143
|
+
offset = ms
|
|
144
|
+
anchor = now()
|
|
145
|
+
},
|
|
146
|
+
cancel() {
|
|
147
|
+
if (stopped) return
|
|
148
|
+
pause()
|
|
149
|
+
stopped = true
|
|
150
|
+
if (--owners === 0) {
|
|
151
|
+
stop()
|
|
152
|
+
accumulated = 0
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|