@rafaelaugusto/jolt 0.1.0-beta.0 → 0.1.0-beta.10

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 (307) hide show
  1. package/README.md +441 -25
  2. package/dist/animate.d.ts +21 -5
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +205 -25
  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/config.d.ts +31 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +73 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/decay.d.ts +11 -0
  15. package/dist/decay.d.ts.map +1 -0
  16. package/dist/decay.js +22 -0
  17. package/dist/decay.js.map +1 -0
  18. package/dist/decoration.d.ts +18 -0
  19. package/dist/decoration.d.ts.map +1 -0
  20. package/dist/decoration.js +155 -0
  21. package/dist/decoration.js.map +1 -0
  22. package/dist/drag-input.d.ts +15 -0
  23. package/dist/drag-input.d.ts.map +1 -0
  24. package/dist/drag-input.js +66 -0
  25. package/dist/drag-input.js.map +1 -0
  26. package/dist/drag-physics.d.ts +20 -0
  27. package/dist/drag-physics.d.ts.map +1 -0
  28. package/dist/drag-physics.js +75 -0
  29. package/dist/drag-physics.js.map +1 -0
  30. package/dist/drag.d.ts +58 -0
  31. package/dist/drag.d.ts.map +1 -0
  32. package/dist/drag.js +324 -0
  33. package/dist/drag.js.map +1 -0
  34. package/dist/driver/android-color.d.ts +20 -0
  35. package/dist/driver/android-color.d.ts.map +1 -0
  36. package/dist/driver/android-color.js +136 -0
  37. package/dist/driver/android-color.js.map +1 -0
  38. package/dist/driver/android-decoration.d.ts +22 -0
  39. package/dist/driver/android-decoration.d.ts.map +1 -0
  40. package/dist/driver/android-decoration.js +139 -0
  41. package/dist/driver/android-decoration.js.map +1 -0
  42. package/dist/driver/android-playback.d.ts +13 -0
  43. package/dist/driver/android-playback.d.ts.map +1 -0
  44. package/dist/driver/android-playback.js +141 -0
  45. package/dist/driver/android-playback.js.map +1 -0
  46. package/dist/driver/android.d.ts +1 -1
  47. package/dist/driver/android.d.ts.map +1 -1
  48. package/dist/driver/android.js +165 -13
  49. package/dist/driver/android.js.map +1 -1
  50. package/dist/driver/index.d.ts +25 -2
  51. package/dist/driver/index.d.ts.map +1 -1
  52. package/dist/driver/index.js.map +1 -1
  53. package/dist/driver/ios-color.d.ts +16 -0
  54. package/dist/driver/ios-color.d.ts.map +1 -0
  55. package/dist/driver/ios-color.js +392 -0
  56. package/dist/driver/ios-color.js.map +1 -0
  57. package/dist/driver/ios-decoration.d.ts +17 -0
  58. package/dist/driver/ios-decoration.d.ts.map +1 -0
  59. package/dist/driver/ios-decoration.js +271 -0
  60. package/dist/driver/ios-decoration.js.map +1 -0
  61. package/dist/driver/ios-layout.d.ts +9 -0
  62. package/dist/driver/ios-layout.d.ts.map +1 -0
  63. package/dist/driver/ios-layout.js +52 -0
  64. package/dist/driver/ios-layout.js.map +1 -0
  65. package/dist/driver/ios-playback.d.ts +10 -0
  66. package/dist/driver/ios-playback.d.ts.map +1 -0
  67. package/dist/driver/ios-playback.js +47 -0
  68. package/dist/driver/ios-playback.js.map +1 -0
  69. package/dist/driver/ios-shadow.d.ts +6 -0
  70. package/dist/driver/ios-shadow.d.ts.map +1 -0
  71. package/dist/driver/ios-shadow.js +52 -0
  72. package/dist/driver/ios-shadow.js.map +1 -0
  73. package/dist/driver/ios.d.ts.map +1 -1
  74. package/dist/driver/ios.js +164 -20
  75. package/dist/driver/ios.js.map +1 -1
  76. package/dist/engine.d.ts +76 -5
  77. package/dist/engine.d.ts.map +1 -1
  78. package/dist/engine.js +303 -23
  79. package/dist/engine.js.map +1 -1
  80. package/dist/haptics.d.ts +21 -0
  81. package/dist/haptics.d.ts.map +1 -0
  82. package/dist/haptics.js +120 -0
  83. package/dist/haptics.js.map +1 -0
  84. package/dist/image-effect-native.d.ts +26 -0
  85. package/dist/image-effect-native.d.ts.map +1 -0
  86. package/dist/image-effect-native.js +73 -0
  87. package/dist/image-effect-native.js.map +1 -0
  88. package/dist/image-effect.d.ts +50 -0
  89. package/dist/image-effect.d.ts.map +1 -0
  90. package/dist/image-effect.js +290 -0
  91. package/dist/image-effect.js.map +1 -0
  92. package/dist/image-owner.d.ts +3 -0
  93. package/dist/image-owner.d.ts.map +1 -0
  94. package/dist/image-owner.js +11 -0
  95. package/dist/image-owner.js.map +1 -0
  96. package/dist/index.d.ts +45 -4
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +21 -2
  99. package/dist/index.js.map +1 -1
  100. package/dist/interaction-surface.d.ts +12 -0
  101. package/dist/interaction-surface.d.ts.map +1 -0
  102. package/dist/interaction-surface.js +36 -0
  103. package/dist/interaction-surface.js.map +1 -0
  104. package/dist/items.d.ts +20 -0
  105. package/dist/items.d.ts.map +1 -0
  106. package/dist/items.js +109 -0
  107. package/dist/items.js.map +1 -0
  108. package/dist/keyboard.d.ts +41 -0
  109. package/dist/keyboard.d.ts.map +1 -0
  110. package/dist/keyboard.js +256 -0
  111. package/dist/keyboard.js.map +1 -0
  112. package/dist/label.d.ts +7 -0
  113. package/dist/label.d.ts.map +1 -0
  114. package/dist/label.js +60 -0
  115. package/dist/label.js.map +1 -0
  116. package/dist/layout.d.ts +13 -0
  117. package/dist/layout.d.ts.map +1 -0
  118. package/dist/layout.js +348 -0
  119. package/dist/layout.js.map +1 -0
  120. package/dist/liquid-grid.d.ts +3 -0
  121. package/dist/liquid-grid.d.ts.map +1 -0
  122. package/dist/liquid-grid.js +24 -0
  123. package/dist/liquid-grid.js.map +1 -0
  124. package/dist/liquid.d.ts +18 -0
  125. package/dist/liquid.d.ts.map +1 -0
  126. package/dist/liquid.js +234 -0
  127. package/dist/liquid.js.map +1 -0
  128. package/dist/loader.d.ts +19 -0
  129. package/dist/loader.d.ts.map +1 -0
  130. package/dist/loader.js +173 -0
  131. package/dist/loader.js.map +1 -0
  132. package/dist/motion-scope.d.ts +39 -0
  133. package/dist/motion-scope.d.ts.map +1 -0
  134. package/dist/motion-scope.js +238 -0
  135. package/dist/motion-scope.js.map +1 -0
  136. package/dist/motion-states.d.ts +36 -0
  137. package/dist/motion-states.d.ts.map +1 -0
  138. package/dist/motion-states.js +165 -0
  139. package/dist/motion-states.js.map +1 -0
  140. package/dist/particles.d.ts +39 -0
  141. package/dist/particles.d.ts.map +1 -0
  142. package/dist/particles.js +265 -0
  143. package/dist/particles.js.map +1 -0
  144. package/dist/path.d.ts +12 -0
  145. package/dist/path.d.ts.map +1 -0
  146. package/dist/path.js +90 -0
  147. package/dist/path.js.map +1 -0
  148. package/dist/playback-group.d.ts +6 -0
  149. package/dist/playback-group.d.ts.map +1 -0
  150. package/dist/playback-group.js +141 -0
  151. package/dist/playback-group.js.map +1 -0
  152. package/dist/playback.d.ts +25 -0
  153. package/dist/playback.d.ts.map +1 -0
  154. package/dist/playback.js +35 -0
  155. package/dist/playback.js.map +1 -0
  156. package/dist/presets.d.ts +16 -1
  157. package/dist/presets.d.ts.map +1 -1
  158. package/dist/presets.js +24 -6
  159. package/dist/presets.js.map +1 -1
  160. package/dist/press.d.ts.map +1 -1
  161. package/dist/press.js +25 -7
  162. package/dist/press.js.map +1 -1
  163. package/dist/properties.d.ts +6 -3
  164. package/dist/properties.d.ts.map +1 -1
  165. package/dist/properties.js +26 -2
  166. package/dist/properties.js.map +1 -1
  167. package/dist/reveal.d.ts +25 -0
  168. package/dist/reveal.d.ts.map +1 -0
  169. package/dist/reveal.js +294 -0
  170. package/dist/reveal.js.map +1 -0
  171. package/dist/roll-diff.d.ts +22 -0
  172. package/dist/roll-diff.d.ts.map +1 -0
  173. package/dist/roll-diff.js +36 -0
  174. package/dist/roll-diff.js.map +1 -0
  175. package/dist/roll.d.ts +29 -0
  176. package/dist/roll.d.ts.map +1 -0
  177. package/dist/roll.js +442 -0
  178. package/dist/roll.js.map +1 -0
  179. package/dist/scroll-linked.d.ts +10 -0
  180. package/dist/scroll-linked.d.ts.map +1 -0
  181. package/dist/scroll-linked.js +58 -0
  182. package/dist/scroll-linked.js.map +1 -0
  183. package/dist/sequence.d.ts +13 -7
  184. package/dist/sequence.d.ts.map +1 -1
  185. package/dist/sequence.js +214 -19
  186. package/dist/sequence.js.map +1 -1
  187. package/dist/shimmer.d.ts +12 -0
  188. package/dist/shimmer.d.ts.map +1 -0
  189. package/dist/shimmer.js +161 -0
  190. package/dist/shimmer.js.map +1 -0
  191. package/dist/svg-path.d.ts +14 -0
  192. package/dist/svg-path.d.ts.map +1 -0
  193. package/dist/svg-path.js +281 -0
  194. package/dist/svg-path.js.map +1 -0
  195. package/dist/symbol-effect.d.ts +23 -0
  196. package/dist/symbol-effect.d.ts.map +1 -0
  197. package/dist/symbol-effect.js +187 -0
  198. package/dist/symbol-effect.js.map +1 -0
  199. package/dist/text-layout.d.ts +30 -0
  200. package/dist/text-layout.d.ts.map +1 -0
  201. package/dist/text-layout.js +185 -0
  202. package/dist/text-layout.js.map +1 -0
  203. package/dist/text.d.ts +17 -0
  204. package/dist/text.d.ts.map +1 -0
  205. package/dist/text.js +263 -0
  206. package/dist/text.js.map +1 -0
  207. package/dist/track.d.ts +3 -1
  208. package/dist/track.d.ts.map +1 -1
  209. package/dist/track.js +266 -13
  210. package/dist/track.js.map +1 -1
  211. package/dist/transition.d.ts +17 -1
  212. package/dist/transition.d.ts.map +1 -1
  213. package/dist/transition.js +41 -6
  214. package/dist/transition.js.map +1 -1
  215. package/dist/types.d.ts +50 -1
  216. package/dist/types.d.ts.map +1 -1
  217. package/dist/vue/index.d.ts +95 -0
  218. package/dist/vue/index.d.ts.map +1 -0
  219. package/dist/vue/index.js +163 -0
  220. package/dist/vue/index.js.map +1 -0
  221. package/dist/zoom-native.d.ts +30 -0
  222. package/dist/zoom-native.d.ts.map +1 -0
  223. package/dist/zoom-native.js +56 -0
  224. package/dist/zoom-native.js.map +1 -0
  225. package/dist/zoomable.d.ts +48 -0
  226. package/dist/zoomable.d.ts.map +1 -0
  227. package/dist/zoomable.js +249 -0
  228. package/dist/zoomable.js.map +1 -0
  229. package/package.json +33 -5
  230. package/platforms/android/include.gradle +7 -0
  231. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  232. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  233. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  234. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  235. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
  236. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  237. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  238. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  239. package/platforms/android/proguard-rules.pro +19 -0
  240. package/platforms/ios/build.xcconfig +1 -0
  241. package/platforms/ios/src/JoltImageEffect.h +25 -0
  242. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  243. package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
  244. package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
  245. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  246. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  247. package/platforms/ios/src/module.modulemap +6 -0
  248. package/src/android-plugin.d.ts +155 -0
  249. package/src/animate.ts +278 -38
  250. package/src/color.ts +101 -0
  251. package/src/config.ts +84 -0
  252. package/src/decay.ts +45 -0
  253. package/src/decoration.ts +180 -0
  254. package/src/drag-input.ts +74 -0
  255. package/src/drag-physics.ts +110 -0
  256. package/src/drag.ts +395 -0
  257. package/src/driver/android-color.ts +170 -0
  258. package/src/driver/android-decoration.ts +182 -0
  259. package/src/driver/android-playback.ts +157 -0
  260. package/src/driver/android.ts +208 -20
  261. package/src/driver/index.ts +37 -2
  262. package/src/driver/ios-color.ts +441 -0
  263. package/src/driver/ios-decoration.ts +332 -0
  264. package/src/driver/ios-layout.ts +63 -0
  265. package/src/driver/ios-playback.ts +51 -0
  266. package/src/driver/ios-shadow.ts +50 -0
  267. package/src/driver/ios.ts +194 -25
  268. package/src/engine.ts +329 -27
  269. package/src/haptics.ts +148 -0
  270. package/src/image-effect-native.ts +129 -0
  271. package/src/image-effect.ts +336 -0
  272. package/src/image-owner.ts +12 -0
  273. package/src/index.ts +61 -4
  274. package/src/interaction-surface.ts +41 -0
  275. package/src/ios-plugin.d.ts +81 -0
  276. package/src/items.ts +140 -0
  277. package/src/keyboard.ts +291 -0
  278. package/src/label.ts +76 -0
  279. package/src/layout.ts +408 -0
  280. package/src/liquid-grid.ts +28 -0
  281. package/src/liquid.ts +287 -0
  282. package/src/loader.ts +202 -0
  283. package/src/motion-scope.ts +279 -0
  284. package/src/motion-states.ts +215 -0
  285. package/src/particles.ts +323 -0
  286. package/src/path.ts +124 -0
  287. package/src/playback-group.ts +143 -0
  288. package/src/playback.ts +62 -0
  289. package/src/presets.ts +31 -7
  290. package/src/press.ts +32 -10
  291. package/src/properties.ts +32 -6
  292. package/src/reveal.ts +374 -0
  293. package/src/roll-diff.ts +57 -0
  294. package/src/roll.ts +515 -0
  295. package/src/scroll-linked.ts +73 -0
  296. package/src/sequence.ts +212 -19
  297. package/src/shimmer.ts +180 -0
  298. package/src/svg-path.ts +355 -0
  299. package/src/symbol-effect.ts +226 -0
  300. package/src/text-layout.ts +228 -0
  301. package/src/text.ts +279 -0
  302. package/src/track.ts +347 -12
  303. package/src/transition.ts +63 -6
  304. package/src/types.ts +51 -1
  305. package/src/vue/index.ts +231 -0
  306. package/src/zoom-native.ts +124 -0
  307. package/src/zoomable.ts +330 -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
+ if (track.repeat > 0) {
238
+ animation.autoreverses = track.reverse
239
+ animation.repeatCount = track.reverse
240
+ ? (track.repeat + 1) / 2
241
+ : track.repeat + 1
242
+ }
243
+ timing.configure(animation, layer)
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,63 @@
1
+ import type { View } from '@nativescript/core'
2
+
3
+ type LayoutView = View & {
4
+ _isTransformed: boolean
5
+ _modifyNativeViewFrame(native: UIView, frame: CGRect): void
6
+ }
7
+ const retained = new WeakMap<View, { count: number; restore(): void }>()
8
+
9
+ /**
10
+ * NativeScript caches whether its JS transform is nonidentity. Interactive native
11
+ * playback can move the actual model while that cached destination is identity.
12
+ * Let Core reset/restore the real transform when it next assigns a layout frame.
13
+ * This hook belongs only to the opted-in instance and only while tracks own it.
14
+ */
15
+ export function retainInteractiveLayout(view: View): () => void {
16
+ const host = view as LayoutView
17
+ let entry = retained.get(view)
18
+ if (!entry) {
19
+ const original = host._modifyNativeViewFrame
20
+ if (typeof original !== 'function') return () => {}
21
+ const descriptor = Object.getOwnPropertyDescriptor(host, '_modifyNativeViewFrame')
22
+ let active = true
23
+ const wrapper: LayoutView['_modifyNativeViewFrame'] = function (
24
+ this: LayoutView,
25
+ native,
26
+ frame,
27
+ ) {
28
+ if (active)
29
+ this._isTransformed = !CATransform3DEqualToTransform(
30
+ native.layer.transform,
31
+ CATransform3DIdentity,
32
+ )
33
+ original.call(this, native, frame)
34
+ }
35
+ host._modifyNativeViewFrame = wrapper
36
+ entry = {
37
+ count: 0,
38
+ restore() {
39
+ active = false
40
+ const native = host.nativeViewProtected as UIView | undefined
41
+ if (native)
42
+ host._isTransformed = !CATransform3DEqualToTransform(
43
+ native.layer.transform,
44
+ CATransform3DIdentity,
45
+ )
46
+ if (host._modifyNativeViewFrame !== wrapper) return
47
+ if (descriptor) Object.defineProperty(host, '_modifyNativeViewFrame', descriptor)
48
+ else delete (host as Partial<LayoutView>)._modifyNativeViewFrame
49
+ },
50
+ }
51
+ retained.set(view, entry)
52
+ }
53
+ entry.count++
54
+ let released = false
55
+ return () => {
56
+ if (released) return
57
+ released = true
58
+ if (--entry.count === 0) {
59
+ entry.restore()
60
+ retained.delete(view)
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,51 @@
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: CAKeyframeAnimation, 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
+ // With speed 0, CA folds an exact duration boundary back to the first
36
+ // frame. Hold the actual endpoint with a constant copy, also when a
37
+ // shorter property waits at its end while another one is being scrubbed.
38
+ if (paused && Number.isFinite(track.end) && position >= track.end) {
39
+ const values = animation.values!
40
+ const index = track.reverse && track.repeat % 2 === 1 ? 0 : values.count - 1
41
+ const value = values.objectAtIndex(index)
42
+ animation.values = NSArray.arrayWithArray([value, value])
43
+ animation.keyTimes = NSArray.arrayWithArray([0, 1])
44
+ animation.duration = 1
45
+ animation.repeatCount = 0
46
+ animation.autoreverses = false
47
+ animation.timeOffset = 0
48
+ }
49
+ },
50
+ }
51
+ }
@@ -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
+ }