@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,332 @@
|
|
|
1
|
+
import type { View } from '@nativescript/core'
|
|
2
|
+
import {
|
|
3
|
+
dip,
|
|
4
|
+
isWritingDecoration,
|
|
5
|
+
radiusKeys,
|
|
6
|
+
widthKeys,
|
|
7
|
+
validateDecorationTarget,
|
|
8
|
+
type DecorationProperty,
|
|
9
|
+
} from '../decoration.js'
|
|
10
|
+
import { iosPlayback } from './ios-playback.js'
|
|
11
|
+
import type { Track } from '../track.js'
|
|
12
|
+
|
|
13
|
+
type NativeView = UIView & { outerShadowContainerLayer?: CALayer }
|
|
14
|
+
type Binding = { layer: CALayer; path: string; values: unknown[] }
|
|
15
|
+
interface Handle {
|
|
16
|
+
elapsed(): number
|
|
17
|
+
control(elapsed: number, paused: boolean): void
|
|
18
|
+
track: Track
|
|
19
|
+
stop(): void
|
|
20
|
+
presented(): number | undefined
|
|
21
|
+
attach(): void
|
|
22
|
+
}
|
|
23
|
+
const playing = new WeakMap<View, Map<DecorationProperty, Handle>>()
|
|
24
|
+
const paths = {
|
|
25
|
+
borderRadius: 'cornerRadius',
|
|
26
|
+
borderWidth: 'borderWidth',
|
|
27
|
+
shadowOpacity: 'shadowOpacity',
|
|
28
|
+
shadowRadius: 'shadowRadius',
|
|
29
|
+
shadowOffsetX: 'shadowOffset.width',
|
|
30
|
+
shadowOffsetY: 'shadowOffset.height',
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
export function syncIOSDecoration(view: View, excluding?: string) {
|
|
34
|
+
for (const [property, handle] of playing.get(view) ?? [])
|
|
35
|
+
if (property !== excluding) handle.attach()
|
|
36
|
+
}
|
|
37
|
+
export const presentedIOSDecoration = (view: View, property: DecorationProperty) =>
|
|
38
|
+
playing.get(view)?.get(property)?.presented()
|
|
39
|
+
|
|
40
|
+
/** Animate Core's uniform border and its separate outer-shadow layers. */
|
|
41
|
+
export function playIOSDecoration(
|
|
42
|
+
view: View,
|
|
43
|
+
track: Track,
|
|
44
|
+
startedAt: number,
|
|
45
|
+
onEnd: (finished?: boolean) => void,
|
|
46
|
+
): Handle {
|
|
47
|
+
const timing = iosPlayback(track, startedAt)
|
|
48
|
+
const property = track.property as Exclude<
|
|
49
|
+
DecorationProperty,
|
|
50
|
+
'elevation' | 'translationZ'
|
|
51
|
+
>
|
|
52
|
+
const native = view.nativeViewProtected as NativeView
|
|
53
|
+
const key = `jolt:${property}`
|
|
54
|
+
let bindings: Binding[] = []
|
|
55
|
+
let stopped = false
|
|
56
|
+
let queued = false
|
|
57
|
+
let generation = 0
|
|
58
|
+
const entries = playing.get(view) ?? new Map<DecorationProperty, Handle>()
|
|
59
|
+
playing.set(view, entries)
|
|
60
|
+
const cleanup = () => {
|
|
61
|
+
if (stopped) return
|
|
62
|
+
stopped = true
|
|
63
|
+
generation++
|
|
64
|
+
view.off('unloaded', cancel)
|
|
65
|
+
view.off('layoutChanged', refresh)
|
|
66
|
+
view.style.off('backgroundInternalChange', refresh)
|
|
67
|
+
for (const event of changes) view.style.off(event, external)
|
|
68
|
+
for (const binding of bindings) binding.layer.removeAnimationForKey(key)
|
|
69
|
+
if (entries.get(property) === handle) entries.delete(property)
|
|
70
|
+
}
|
|
71
|
+
const cancel = () => {
|
|
72
|
+
if (!stopped) {
|
|
73
|
+
cleanup()
|
|
74
|
+
onEnd(false)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const refresh = () => {
|
|
78
|
+
if (stopped || queued) return
|
|
79
|
+
queued = true
|
|
80
|
+
void Promise.resolve().then(() => {
|
|
81
|
+
queued = false
|
|
82
|
+
if (!stopped && view.nativeViewProtected === native) handle.attach()
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
const external = (event: { eventName: string }) => {
|
|
86
|
+
if (isWritingDecoration(view)) return
|
|
87
|
+
const own =
|
|
88
|
+
property === 'borderRadius'
|
|
89
|
+
? radiusKeys
|
|
90
|
+
: property === 'borderWidth'
|
|
91
|
+
? widthKeys
|
|
92
|
+
: ['boxShadow']
|
|
93
|
+
void Promise.resolve().then(() => {
|
|
94
|
+
if (stopped) return
|
|
95
|
+
if (own.some((name) => event.eventName === `${name}Change`)) return cancel()
|
|
96
|
+
try {
|
|
97
|
+
validateDecorationTarget(view, property)
|
|
98
|
+
} catch {
|
|
99
|
+
cancel()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
const changes = [
|
|
104
|
+
...radiusKeys,
|
|
105
|
+
...widthKeys,
|
|
106
|
+
'borderTopColor',
|
|
107
|
+
'borderRightColor',
|
|
108
|
+
'borderBottomColor',
|
|
109
|
+
'borderLeftColor',
|
|
110
|
+
'clipPath',
|
|
111
|
+
'cornerShape',
|
|
112
|
+
'boxShadow',
|
|
113
|
+
].map((name) => `${name}Change`)
|
|
114
|
+
const handle: Handle = {
|
|
115
|
+
track,
|
|
116
|
+
stop: cancel,
|
|
117
|
+
elapsed: timing.elapsed,
|
|
118
|
+
control(time, paused) {
|
|
119
|
+
if (!stopped) {
|
|
120
|
+
timing.control(time, paused)
|
|
121
|
+
handle.attach()
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
presented() {
|
|
125
|
+
if (timing.controlled) return undefined
|
|
126
|
+
const binding = bindings[0]
|
|
127
|
+
const layer = binding?.layer.presentationLayer()
|
|
128
|
+
if (!layer) return track.valueAt(timing.elapsed())
|
|
129
|
+
if (property === 'shadowOffsetX') return layer.shadowOffset.width
|
|
130
|
+
if (property === 'shadowOffsetY') return layer.shadowOffset.height
|
|
131
|
+
return (layer as unknown as Record<string, number>)[paths[property]]!
|
|
132
|
+
},
|
|
133
|
+
attach() {
|
|
134
|
+
if (stopped) return
|
|
135
|
+
const instance = ++generation
|
|
136
|
+
for (const binding of bindings) binding.layer.removeAnimationForKey(key)
|
|
137
|
+
bindings = []
|
|
138
|
+
// Path clipping and spread are nonlinear. Sample even a linear radius
|
|
139
|
+
// tween so its shadow and inner cutout follow the capped native corner.
|
|
140
|
+
const steps = Math.max(1, Math.min(6000, Math.ceil((track.duration * 60) / 1000)))
|
|
141
|
+
const times =
|
|
142
|
+
property === 'borderRadius'
|
|
143
|
+
? [
|
|
144
|
+
...new Set([
|
|
145
|
+
...track.times,
|
|
146
|
+
...Array.from({ length: steps + 1 }, (_, i) => i / steps),
|
|
147
|
+
]),
|
|
148
|
+
].sort((a, b) => a - b)
|
|
149
|
+
: [...track.times]
|
|
150
|
+
const values =
|
|
151
|
+
property === 'borderRadius'
|
|
152
|
+
? times.map((time) =>
|
|
153
|
+
time === 1
|
|
154
|
+
? track.values.at(-1)!
|
|
155
|
+
: track.valueAt(track.delay + time * track.duration),
|
|
156
|
+
)
|
|
157
|
+
: [...track.values]
|
|
158
|
+
if (property === 'borderRadius' || property === 'borderWidth') {
|
|
159
|
+
bindings.push({
|
|
160
|
+
layer: native.layer,
|
|
161
|
+
path: paths[property],
|
|
162
|
+
values:
|
|
163
|
+
property === 'borderRadius' && native.bounds?.size
|
|
164
|
+
? values.map((value) =>
|
|
165
|
+
Math.min(
|
|
166
|
+
value,
|
|
167
|
+
native.bounds.size.width / 2,
|
|
168
|
+
native.bounds.size.height / 2,
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
: values,
|
|
172
|
+
})
|
|
173
|
+
if (property === 'borderRadius') {
|
|
174
|
+
const shadows = view.boxShadow
|
|
175
|
+
const layers = native.outerShadowContainerLayer?.sublayers
|
|
176
|
+
if (Array.isArray(shadows) && layers) {
|
|
177
|
+
for (let i = 0; i < Math.min(shadows.length, layers.count); i++) {
|
|
178
|
+
const layer = layers.objectAtIndex(i)
|
|
179
|
+
const spread = dip(shadows[i]!.spreadRadius)
|
|
180
|
+
bindings.push({
|
|
181
|
+
layer,
|
|
182
|
+
path: 'shadowPath',
|
|
183
|
+
values: values.map((radius) =>
|
|
184
|
+
roundedPath(native.bounds, radius, spread),
|
|
185
|
+
),
|
|
186
|
+
})
|
|
187
|
+
if (layer.mask)
|
|
188
|
+
bindings.push({
|
|
189
|
+
layer: layer.mask,
|
|
190
|
+
path: 'path',
|
|
191
|
+
values: values.map((radius) =>
|
|
192
|
+
shadowMask(native.bounds, radius, maskExtent(view, entries)),
|
|
193
|
+
),
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
const layer = native.outerShadowContainerLayer?.sublayers?.firstObject
|
|
200
|
+
if (!layer) return cancel()
|
|
201
|
+
bindings.push({ layer, path: paths[property], values: [...track.values] })
|
|
202
|
+
// Core's mask bounds are sized for the final blur/offset. Give intermediate
|
|
203
|
+
// samples room too. Radius owns the animated inner edge when it is active.
|
|
204
|
+
if (layer.mask && !entries.has('borderRadius')) {
|
|
205
|
+
CATransaction.begin()
|
|
206
|
+
CATransaction.setDisableActions(true)
|
|
207
|
+
;(layer.mask as CAShapeLayer).path = shadowMask(
|
|
208
|
+
native.bounds,
|
|
209
|
+
native.layer.cornerRadius,
|
|
210
|
+
maskExtent(view, entries),
|
|
211
|
+
)
|
|
212
|
+
CATransaction.commit()
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
CATransaction.begin()
|
|
216
|
+
CATransaction.setDisableActions(true)
|
|
217
|
+
if (!timing.paused && track.repeat !== Infinity)
|
|
218
|
+
CATransaction.setCompletionBlock(() => {
|
|
219
|
+
if (stopped || generation !== instance) return
|
|
220
|
+
cleanup()
|
|
221
|
+
onEnd()
|
|
222
|
+
})
|
|
223
|
+
// Core treats a zero-alpha shadow as opaque. Commit the native endpoint
|
|
224
|
+
// explicitly too; otherwise a fade to zero would flash back on completion.
|
|
225
|
+
const paint = bindings[0]!.layer
|
|
226
|
+
if (property === 'shadowOpacity') paint.shadowOpacity = track.to
|
|
227
|
+
else if (property === 'shadowRadius') paint.shadowRadius = track.to
|
|
228
|
+
else if (property === 'shadowOffsetX')
|
|
229
|
+
paint.shadowOffset = CGSizeMake(track.to, paint.shadowOffset.height)
|
|
230
|
+
else if (property === 'shadowOffsetY')
|
|
231
|
+
paint.shadowOffset = CGSizeMake(paint.shadowOffset.width, track.to)
|
|
232
|
+
for (const { layer, path, values } of bindings) {
|
|
233
|
+
const animation = CAKeyframeAnimation.animationWithKeyPath(path)
|
|
234
|
+
animation.values = NSArray.arrayWithArray(values)
|
|
235
|
+
animation.keyTimes = NSArray.arrayWithArray(times)
|
|
236
|
+
animation.duration = Math.max(1, track.duration) / 1000
|
|
237
|
+
timing.configure(animation, layer)
|
|
238
|
+
if (track.repeat > 0) {
|
|
239
|
+
animation.autoreverses = track.reverse
|
|
240
|
+
animation.repeatCount = track.reverse
|
|
241
|
+
? (track.repeat + 1) / 2
|
|
242
|
+
: track.repeat + 1
|
|
243
|
+
}
|
|
244
|
+
if (animation.respondsToSelector('setPreferredFrameRateRange:')) {
|
|
245
|
+
const max = Math.min(
|
|
246
|
+
120,
|
|
247
|
+
native.window?.screen.maximumFramesPerSecond ??
|
|
248
|
+
UIScreen.mainScreen.maximumFramesPerSecond,
|
|
249
|
+
)
|
|
250
|
+
animation.preferredFrameRateRange = CAFrameRateRangeMake(
|
|
251
|
+
Math.min(80, max),
|
|
252
|
+
max,
|
|
253
|
+
max,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
layer.addAnimationForKey(animation, key)
|
|
257
|
+
}
|
|
258
|
+
CATransaction.commit()
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
entries.set(property, handle)
|
|
262
|
+
view.on('unloaded', cancel)
|
|
263
|
+
view.on('layoutChanged', refresh)
|
|
264
|
+
view.style.on('backgroundInternalChange', refresh)
|
|
265
|
+
for (const event of changes) view.style.on(event, external)
|
|
266
|
+
// All model endpoints have been committed before driver.play(). Each handle
|
|
267
|
+
// follows the same timeline even if Core redraws its layers during layout.
|
|
268
|
+
for (const active of entries.values()) active.attach()
|
|
269
|
+
return handle
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function maskExtent(view: View, entries: Map<DecorationProperty, Handle>): number {
|
|
273
|
+
const shadows = Array.isArray(view.boxShadow) ? view.boxShadow : []
|
|
274
|
+
const maximum = (property: DecorationProperty, fallback: number) =>
|
|
275
|
+
Math.max(
|
|
276
|
+
Math.abs(fallback),
|
|
277
|
+
...(entries.get(property)?.track.values.map(Math.abs) ?? []),
|
|
278
|
+
)
|
|
279
|
+
return Math.max(
|
|
280
|
+
1,
|
|
281
|
+
...shadows.map(
|
|
282
|
+
(shadow) =>
|
|
283
|
+
maximum('shadowRadius', dip(shadow.blurRadius) / 2) * 6 +
|
|
284
|
+
Math.abs(dip(shadow.spreadRadius)) +
|
|
285
|
+
maximum('shadowOffsetX', dip(shadow.offsetX)) +
|
|
286
|
+
maximum('shadowOffsetY', dip(shadow.offsetY)) +
|
|
287
|
+
1,
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Keep identical path topology at radius zero so CA can interpolate it. */
|
|
293
|
+
function roundedPath(bounds: CGRect, radius: number, spread = 0): interop.Pointer {
|
|
294
|
+
const rect = CGRectInset(bounds, -spread, -spread)
|
|
295
|
+
const r = Math.min(
|
|
296
|
+
Math.max(0, Math.min(radius, bounds.size.width / 2, bounds.size.height / 2) + spread),
|
|
297
|
+
Math.max(0, rect.size.width / 2),
|
|
298
|
+
Math.max(0, rect.size.height / 2),
|
|
299
|
+
)
|
|
300
|
+
const x = rect.origin.x,
|
|
301
|
+
y = rect.origin.y,
|
|
302
|
+
right = x + rect.size.width,
|
|
303
|
+
bottom = y + rect.size.height
|
|
304
|
+
const k = r * 0.5522847498307936
|
|
305
|
+
const path = CGPathCreateMutable()
|
|
306
|
+
CGPathMoveToPoint(path, null, x + r, y)
|
|
307
|
+
CGPathAddLineToPoint(path, null, right - r, y)
|
|
308
|
+
CGPathAddCurveToPoint(path, null, right - r + k, y, right, y + r - k, right, y + r)
|
|
309
|
+
CGPathAddLineToPoint(path, null, right, bottom - r)
|
|
310
|
+
CGPathAddCurveToPoint(
|
|
311
|
+
path,
|
|
312
|
+
null,
|
|
313
|
+
right,
|
|
314
|
+
bottom - r + k,
|
|
315
|
+
right - r + k,
|
|
316
|
+
bottom,
|
|
317
|
+
right - r,
|
|
318
|
+
bottom,
|
|
319
|
+
)
|
|
320
|
+
CGPathAddLineToPoint(path, null, x + r, bottom)
|
|
321
|
+
CGPathAddCurveToPoint(path, null, x + r - k, bottom, x, bottom - r + k, x, bottom - r)
|
|
322
|
+
CGPathAddLineToPoint(path, null, x, y + r)
|
|
323
|
+
CGPathAddCurveToPoint(path, null, x, y + r - k, x + r - k, y, x + r, y)
|
|
324
|
+
CGPathCloseSubpath(path)
|
|
325
|
+
return path
|
|
326
|
+
}
|
|
327
|
+
function shadowMask(bounds: CGRect, radius: number, extent: number): interop.Pointer {
|
|
328
|
+
const path = CGPathCreateMutable()
|
|
329
|
+
CGPathAddPath(path, null, roundedPath(bounds, radius))
|
|
330
|
+
CGPathAddRect(path, null, CGRectInset(bounds, -extent, -extent))
|
|
331
|
+
return path
|
|
332
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Track } from '../track.js'
|
|
2
|
+
|
|
3
|
+
/** Each CAAnimation owns its clock; the view and its children keep their speed. */
|
|
4
|
+
export function iosPlayback(track: Track, startedAt: number) {
|
|
5
|
+
let anchor = startedAt
|
|
6
|
+
let offset = 0
|
|
7
|
+
let paused = false
|
|
8
|
+
let controlled = false
|
|
9
|
+
const elapsed = () => offset + (paused ? 0 : CACurrentMediaTime() * 1000 - anchor)
|
|
10
|
+
return {
|
|
11
|
+
elapsed,
|
|
12
|
+
get controlled() {
|
|
13
|
+
return controlled
|
|
14
|
+
},
|
|
15
|
+
get paused() {
|
|
16
|
+
return paused
|
|
17
|
+
},
|
|
18
|
+
control(time: number, hold: boolean) {
|
|
19
|
+
controlled = true
|
|
20
|
+
offset = time
|
|
21
|
+
anchor = CACurrentMediaTime() * 1000
|
|
22
|
+
paused = hold
|
|
23
|
+
},
|
|
24
|
+
configure(animation: CAAnimation, layer: CALayer) {
|
|
25
|
+
const now = CACurrentMediaTime()
|
|
26
|
+
const position = elapsed()
|
|
27
|
+
animation.speed = paused ? 0 : 1
|
|
28
|
+
animation.timeOffset = paused ? Math.max(0, position - track.delay) / 1000 : 0
|
|
29
|
+
animation.beginTime = layer.convertTimeFromLayer(
|
|
30
|
+
paused ? now : now - position / 1000 + track.delay / 1000,
|
|
31
|
+
null,
|
|
32
|
+
)
|
|
33
|
+
animation.fillMode = paused ? 'both' : kCAFillModeBackwards
|
|
34
|
+
animation.removedOnCompletion = !paused && track.repeat !== Infinity
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { View } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
type BackgroundView = { _redrawNativeBackground?: (...args: unknown[]) => void }
|
|
4
|
+
const repairs = new WeakSet<View>()
|
|
5
|
+
|
|
6
|
+
/** Core treats alpha 0 as 1. Repair its model layer after background redraws,
|
|
7
|
+
* not on animation frames. The wrapper belongs only to this view and is
|
|
8
|
+
* removed when the shadow color changes. */
|
|
9
|
+
export function preserveZeroShadow(view: View) {
|
|
10
|
+
const target = view as unknown as BackgroundView
|
|
11
|
+
const hasZero = () =>
|
|
12
|
+
Array.isArray(view.boxShadow) &&
|
|
13
|
+
view.boxShadow.some((shadow) => shadow.color.argb >>> 24 === 0)
|
|
14
|
+
if (!hasZero() || repairs.has(view)) return
|
|
15
|
+
const descriptor = Object.getOwnPropertyDescriptor(view, '_redrawNativeBackground')
|
|
16
|
+
const original = target._redrawNativeBackground
|
|
17
|
+
const apply = () => {
|
|
18
|
+
if (!hasZero()) return dispose()
|
|
19
|
+
if (!view.isLoaded) return
|
|
20
|
+
const layers = view.nativeViewProtected?.outerShadowContainerLayer?.sublayers as
|
|
21
|
+
NSArray<CALayer> | undefined
|
|
22
|
+
if (!layers || !Array.isArray(view.boxShadow)) return
|
|
23
|
+
CATransaction.begin()
|
|
24
|
+
CATransaction.setDisableActions(true)
|
|
25
|
+
for (let i = 0; i < Math.min(layers.count, view.boxShadow.length); i++)
|
|
26
|
+
if (view.boxShadow[i]!.color.argb >>> 24 === 0)
|
|
27
|
+
layers.objectAtIndex(i).shadowOpacity = 0
|
|
28
|
+
CATransaction.commit()
|
|
29
|
+
}
|
|
30
|
+
const redraw = function (this: BackgroundView, ...args: unknown[]) {
|
|
31
|
+
original!.apply(this, args)
|
|
32
|
+
apply()
|
|
33
|
+
}
|
|
34
|
+
const dispose = () => {
|
|
35
|
+
view.style.off('boxShadowChange', apply)
|
|
36
|
+
view.off('loaded', apply)
|
|
37
|
+
view.off('layoutChanged', apply)
|
|
38
|
+
if (original && target._redrawNativeBackground === redraw) {
|
|
39
|
+
if (descriptor) Object.defineProperty(view, '_redrawNativeBackground', descriptor)
|
|
40
|
+
else delete target._redrawNativeBackground
|
|
41
|
+
}
|
|
42
|
+
repairs.delete(view)
|
|
43
|
+
}
|
|
44
|
+
repairs.add(view)
|
|
45
|
+
if (original) target._redrawNativeBackground = redraw
|
|
46
|
+
view.style.on('boxShadowChange', apply)
|
|
47
|
+
view.on('loaded', apply)
|
|
48
|
+
view.on('layoutChanged', apply)
|
|
49
|
+
apply()
|
|
50
|
+
}
|
package/src/driver/ios.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
+
import { iosPlayback } from './ios-playback.js'
|
|
2
|
+
import { isDecorationProperty } from '../decoration.js'
|
|
3
|
+
import {
|
|
4
|
+
playIOSDecoration,
|
|
5
|
+
presentedIOSDecoration,
|
|
6
|
+
syncIOSDecoration,
|
|
7
|
+
} from './ios-decoration.js'
|
|
1
8
|
import type { View } from '@nativescript/core'
|
|
2
|
-
import type {
|
|
9
|
+
import type { NumericProperty } from '../properties.js'
|
|
10
|
+
import { isColorProperty } from '../color.js'
|
|
11
|
+
import { playIOSColor, presentedIOSColor } from './ios-color.js'
|
|
3
12
|
import { after, type Driver } from './index.js'
|
|
4
13
|
|
|
5
14
|
interface Handle {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
stop(): void
|
|
16
|
+
elapsed(): number
|
|
17
|
+
control(elapsed: number, paused: boolean): void
|
|
9
18
|
}
|
|
10
19
|
|
|
11
|
-
const keyPaths: Record<
|
|
20
|
+
const keyPaths: Record<NumericProperty, string> = {
|
|
12
21
|
opacity: 'opacity',
|
|
13
22
|
x: 'transform.translation.x',
|
|
14
23
|
y: 'transform.translation.y',
|
|
@@ -49,6 +58,10 @@ export function createIOSDriver(): Driver {
|
|
|
49
58
|
now: () => CACurrentMediaTime() * 1000,
|
|
50
59
|
|
|
51
60
|
play(view, track, startedAt, onEnd): Handle {
|
|
61
|
+
if (isDecorationProperty(track.property))
|
|
62
|
+
return playIOSDecoration(view, track, startedAt, onEnd)
|
|
63
|
+
if (isColorProperty(track.property))
|
|
64
|
+
return playIOSColor(view, track, startedAt, onEnd)
|
|
52
65
|
const layers = layersOf(view)
|
|
53
66
|
const key = `jolt:${track.property}`
|
|
54
67
|
const rotation = track.property.startsWith('rotate')
|
|
@@ -57,51 +70,87 @@ export function createIOSDriver(): Driver {
|
|
|
57
70
|
ensurePerspective(view, layers)
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
Math.min(80, maximum),
|
|
68
|
-
maximum,
|
|
69
|
-
maximum,
|
|
73
|
+
const timing = iosPlayback(track, startedAt)
|
|
74
|
+
let stopped = false,
|
|
75
|
+
generation = 0
|
|
76
|
+
const attach = () => {
|
|
77
|
+
const instance = ++generation
|
|
78
|
+
const animation = CAKeyframeAnimation.animationWithKeyPath(
|
|
79
|
+
keyPaths[track.property as NumericProperty],
|
|
70
80
|
)
|
|
81
|
+
// iOS 15+. The app must opt in through CADisableMinimumFrameDurationOnPhone.
|
|
82
|
+
if (animation.respondsToSelector('setPreferredFrameRateRange:')) {
|
|
83
|
+
const nativeView = view.nativeViewProtected as UIView
|
|
84
|
+
const screen = nativeView.window?.screen ?? UIScreen.mainScreen
|
|
85
|
+
const maximum = Math.min(120, screen.maximumFramesPerSecond)
|
|
86
|
+
animation.preferredFrameRateRange = CAFrameRateRangeMake(
|
|
87
|
+
Math.min(80, maximum),
|
|
88
|
+
maximum,
|
|
89
|
+
maximum,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
animation.values = NSArray.arrayWithArray(
|
|
93
|
+
track.values.map((value) => (rotation ? radians(value) : value)),
|
|
94
|
+
)
|
|
95
|
+
animation.keyTimes = NSArray.arrayWithArray(track.times as number[])
|
|
96
|
+
animation.duration = Math.max(track.duration, 1) / 1000
|
|
97
|
+
// Keep infinite loops when iOS removes animations on backgrounding.
|
|
98
|
+
// They are removed explicitly when stopped or replaced.
|
|
99
|
+
if (track.repeat > 0) {
|
|
100
|
+
animation.autoreverses = track.reverse
|
|
101
|
+
animation.repeatCount = track.reverse
|
|
102
|
+
? (track.repeat + 1) / 2
|
|
103
|
+
: track.repeat + 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
CATransaction.begin()
|
|
107
|
+
if (!timing.paused && !infinite)
|
|
108
|
+
CATransaction.setCompletionBlock(() => {
|
|
109
|
+
if (stopped || instance !== generation) return
|
|
110
|
+
stopped = true
|
|
111
|
+
onEnd()
|
|
112
|
+
})
|
|
113
|
+
for (const layer of layers) {
|
|
114
|
+
timing.configure(animation, layer)
|
|
115
|
+
layer.addAnimationForKey(animation, key)
|
|
116
|
+
}
|
|
117
|
+
CATransaction.commit()
|
|
71
118
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
119
|
+
attach()
|
|
120
|
+
return {
|
|
121
|
+
elapsed: timing.elapsed,
|
|
122
|
+
control(time, paused) {
|
|
123
|
+
if (!stopped) {
|
|
124
|
+
timing.control(time, paused)
|
|
125
|
+
attach()
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
stop() {
|
|
129
|
+
if (stopped) return
|
|
130
|
+
stopped = true
|
|
131
|
+
generation++
|
|
132
|
+
for (const layer of layers) layer.removeAnimationForKey(key)
|
|
133
|
+
onEnd(false)
|
|
134
|
+
},
|
|
85
135
|
}
|
|
86
|
-
|
|
87
|
-
CATransaction.begin()
|
|
88
|
-
// iOS completes the transaction on backgrounding even if the loop is retained.
|
|
89
|
-
if (!infinite) CATransaction.setCompletionBlock(onEnd)
|
|
90
|
-
for (const layer of layers) layer.addAnimationForKey(animation, key)
|
|
91
|
-
CATransaction.commit()
|
|
92
|
-
|
|
93
|
-
return { layers, key, onStop: infinite ? onEnd : undefined }
|
|
94
136
|
},
|
|
95
137
|
|
|
96
138
|
stop(_view, handle) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
onStop?.()
|
|
139
|
+
;(handle as Handle).stop()
|
|
140
|
+
},
|
|
141
|
+
control(_view, handle, elapsed, paused) {
|
|
142
|
+
;(handle as Handle).control(elapsed, paused)
|
|
102
143
|
},
|
|
144
|
+
elapsed: (handle) => (handle as Handle).elapsed(),
|
|
145
|
+
|
|
146
|
+
sync: syncIOSDecoration,
|
|
103
147
|
|
|
104
|
-
presented: () =>
|
|
148
|
+
presented: (view, property) =>
|
|
149
|
+
isDecorationProperty(property)
|
|
150
|
+
? presentedIOSDecoration(view, property)
|
|
151
|
+
: isColorProperty(property)
|
|
152
|
+
? presentedIOSColor(view, property)
|
|
153
|
+
: undefined,
|
|
105
154
|
|
|
106
155
|
after,
|
|
107
156
|
}
|