@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.
Files changed (132) hide show
  1. package/README.md +78 -13
  2. package/dist/animate.d.ts +3 -2
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +35 -5
  5. package/dist/animate.js.map +1 -1
  6. package/dist/color.d.ts +11 -0
  7. package/dist/color.d.ts.map +1 -0
  8. package/dist/color.js +73 -0
  9. package/dist/color.js.map +1 -0
  10. package/dist/decoration.d.ts +18 -0
  11. package/dist/decoration.d.ts.map +1 -0
  12. package/dist/decoration.js +155 -0
  13. package/dist/decoration.js.map +1 -0
  14. package/dist/driver/android-color.d.ts +20 -0
  15. package/dist/driver/android-color.d.ts.map +1 -0
  16. package/dist/driver/android-color.js +136 -0
  17. package/dist/driver/android-color.js.map +1 -0
  18. package/dist/driver/android-decoration.d.ts +22 -0
  19. package/dist/driver/android-decoration.d.ts.map +1 -0
  20. package/dist/driver/android-decoration.js +139 -0
  21. package/dist/driver/android-decoration.js.map +1 -0
  22. package/dist/driver/android-playback.d.ts +13 -0
  23. package/dist/driver/android-playback.d.ts.map +1 -0
  24. package/dist/driver/android-playback.js +141 -0
  25. package/dist/driver/android-playback.js.map +1 -0
  26. package/dist/driver/android.d.ts.map +1 -1
  27. package/dist/driver/android.js +28 -35
  28. package/dist/driver/android.js.map +1 -1
  29. package/dist/driver/index.d.ts +8 -1
  30. package/dist/driver/index.d.ts.map +1 -1
  31. package/dist/driver/index.js.map +1 -1
  32. package/dist/driver/ios-color.d.ts +16 -0
  33. package/dist/driver/ios-color.d.ts.map +1 -0
  34. package/dist/driver/ios-color.js +392 -0
  35. package/dist/driver/ios-color.js.map +1 -0
  36. package/dist/driver/ios-decoration.d.ts +17 -0
  37. package/dist/driver/ios-decoration.d.ts.map +1 -0
  38. package/dist/driver/ios-decoration.js +271 -0
  39. package/dist/driver/ios-decoration.js.map +1 -0
  40. package/dist/driver/ios-playback.d.ts +10 -0
  41. package/dist/driver/ios-playback.d.ts.map +1 -0
  42. package/dist/driver/ios-playback.js +33 -0
  43. package/dist/driver/ios-playback.js.map +1 -0
  44. package/dist/driver/ios-shadow.d.ts +6 -0
  45. package/dist/driver/ios-shadow.d.ts.map +1 -0
  46. package/dist/driver/ios-shadow.js +52 -0
  47. package/dist/driver/ios-shadow.js.map +1 -0
  48. package/dist/driver/ios.d.ts.map +1 -1
  49. package/dist/driver/ios.js +78 -35
  50. package/dist/driver/ios.js.map +1 -1
  51. package/dist/engine.d.ts +48 -2
  52. package/dist/engine.d.ts.map +1 -1
  53. package/dist/engine.js +217 -21
  54. package/dist/engine.js.map +1 -1
  55. package/dist/haptics.d.ts +4 -1
  56. package/dist/haptics.d.ts.map +1 -1
  57. package/dist/haptics.js +27 -3
  58. package/dist/haptics.js.map +1 -1
  59. package/dist/index.d.ts +4 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/liquid-grid.d.ts +3 -0
  64. package/dist/liquid-grid.d.ts.map +1 -0
  65. package/dist/liquid-grid.js +24 -0
  66. package/dist/liquid-grid.js.map +1 -0
  67. package/dist/liquid.d.ts +18 -0
  68. package/dist/liquid.d.ts.map +1 -0
  69. package/dist/liquid.js +229 -0
  70. package/dist/liquid.js.map +1 -0
  71. package/dist/playback-group.d.ts +6 -0
  72. package/dist/playback-group.d.ts.map +1 -0
  73. package/dist/playback-group.js +113 -0
  74. package/dist/playback-group.js.map +1 -0
  75. package/dist/playback.d.ts +25 -0
  76. package/dist/playback.d.ts.map +1 -0
  77. package/dist/playback.js +35 -0
  78. package/dist/playback.js.map +1 -0
  79. package/dist/press.d.ts.map +1 -1
  80. package/dist/press.js +6 -1
  81. package/dist/press.js.map +1 -1
  82. package/dist/properties.d.ts +5 -2
  83. package/dist/properties.d.ts.map +1 -1
  84. package/dist/properties.js +25 -1
  85. package/dist/properties.js.map +1 -1
  86. package/dist/sequence.d.ts.map +1 -1
  87. package/dist/sequence.js +6 -0
  88. package/dist/sequence.js.map +1 -1
  89. package/dist/text.d.ts.map +1 -1
  90. package/dist/text.js +3 -0
  91. package/dist/text.js.map +1 -1
  92. package/dist/track.d.ts +1 -1
  93. package/dist/track.d.ts.map +1 -1
  94. package/dist/track.js +48 -3
  95. package/dist/track.js.map +1 -1
  96. package/dist/transition.d.ts.map +1 -1
  97. package/dist/transition.js +11 -2
  98. package/dist/transition.js.map +1 -1
  99. package/dist/types.d.ts +20 -0
  100. package/dist/types.d.ts.map +1 -1
  101. package/package.json +11 -2
  102. package/platforms/android/include.gradle +7 -0
  103. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  104. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  105. package/platforms/android/proguard-rules.pro +7 -0
  106. package/src/animate.ts +48 -9
  107. package/src/color.ts +101 -0
  108. package/src/decoration.ts +180 -0
  109. package/src/driver/android-color.ts +170 -0
  110. package/src/driver/android-decoration.ts +182 -0
  111. package/src/driver/android-playback.ts +157 -0
  112. package/src/driver/android.ts +50 -46
  113. package/src/driver/index.ts +19 -1
  114. package/src/driver/ios-color.ts +441 -0
  115. package/src/driver/ios-decoration.ts +332 -0
  116. package/src/driver/ios-playback.ts +37 -0
  117. package/src/driver/ios-shadow.ts +50 -0
  118. package/src/driver/ios.ts +91 -42
  119. package/src/engine.ts +227 -23
  120. package/src/haptics.ts +25 -2
  121. package/src/index.ts +4 -0
  122. package/src/liquid-grid.ts +28 -0
  123. package/src/liquid.ts +282 -0
  124. package/src/playback-group.ts +110 -0
  125. package/src/playback.ts +62 -0
  126. package/src/press.ts +8 -2
  127. package/src/properties.ts +31 -5
  128. package/src/sequence.ts +5 -0
  129. package/src/text.ts +2 -0
  130. package/src/track.ts +72 -3
  131. package/src/transition.ts +13 -2
  132. 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 { Property } from '../properties.js'
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
- layers: CALayer[]
7
- key: string
8
- onStop?: () => void
15
+ stop(): void
16
+ elapsed(): number
17
+ control(elapsed: number, paused: boolean): void
9
18
  }
10
19
 
11
- const keyPaths: Record<Property, string> = {
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 animation = CAKeyframeAnimation.animationWithKeyPath(keyPaths[track.property])
61
- // iOS 15+. The app must opt in through CADisableMinimumFrameDurationOnPhone.
62
- if (animation.respondsToSelector('setPreferredFrameRateRange:')) {
63
- const nativeView = view.nativeViewProtected as UIView
64
- const screen = nativeView.window?.screen ?? UIScreen.mainScreen
65
- const maximum = Math.min(120, screen.maximumFramesPerSecond)
66
- animation.preferredFrameRateRange = CAFrameRateRangeMake(
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
- animation.values = NSArray.arrayWithArray(
73
- track.values.map((value) => (rotation ? radians(value) : value)),
74
- )
75
- animation.keyTimes = NSArray.arrayWithArray(track.times as number[])
76
- animation.duration = Math.max(track.duration, 1) / 1000
77
- animation.beginTime = (startedAt + track.delay) / 1000
78
- animation.fillMode = kCAFillModeBackwards
79
- // Keep infinite loops when iOS removes animations on backgrounding.
80
- // They are removed explicitly when stopped or replaced.
81
- animation.removedOnCompletion = !infinite
82
- if (track.repeat > 0) {
83
- animation.autoreverses = track.reverse
84
- animation.repeatCount = track.reverse ? (track.repeat + 1) / 2 : track.repeat + 1
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
- const playing = handle as Handle
98
- const { layers, key, onStop } = playing
99
- playing.onStop = undefined
100
- for (const layer of layers) layer.removeAnimationForKey(key)
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: () => undefined,
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
  }