@rafaelaugusto/jolt 0.1.0-beta.1 → 0.1.0-beta.11

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 (318) hide show
  1. package/README.md +385 -18
  2. package/dist/animate.d.ts +19 -3
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +186 -14
  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-gradient.d.ts +5 -0
  43. package/dist/driver/android-gradient.d.ts.map +1 -0
  44. package/dist/driver/android-gradient.js +89 -0
  45. package/dist/driver/android-gradient.js.map +1 -0
  46. package/dist/driver/android-playback.d.ts +13 -0
  47. package/dist/driver/android-playback.d.ts.map +1 -0
  48. package/dist/driver/android-playback.js +141 -0
  49. package/dist/driver/android-playback.js.map +1 -0
  50. package/dist/driver/android.d.ts +1 -1
  51. package/dist/driver/android.d.ts.map +1 -1
  52. package/dist/driver/android.js +165 -13
  53. package/dist/driver/android.js.map +1 -1
  54. package/dist/driver/index.d.ts +25 -2
  55. package/dist/driver/index.d.ts.map +1 -1
  56. package/dist/driver/index.js.map +1 -1
  57. package/dist/driver/ios-color.d.ts +16 -0
  58. package/dist/driver/ios-color.d.ts.map +1 -0
  59. package/dist/driver/ios-color.js +392 -0
  60. package/dist/driver/ios-color.js.map +1 -0
  61. package/dist/driver/ios-decoration.d.ts +17 -0
  62. package/dist/driver/ios-decoration.d.ts.map +1 -0
  63. package/dist/driver/ios-decoration.js +271 -0
  64. package/dist/driver/ios-decoration.js.map +1 -0
  65. package/dist/driver/ios-gradient.d.ts +5 -0
  66. package/dist/driver/ios-gradient.d.ts.map +1 -0
  67. package/dist/driver/ios-gradient.js +109 -0
  68. package/dist/driver/ios-gradient.js.map +1 -0
  69. package/dist/driver/ios-layout.d.ts +9 -0
  70. package/dist/driver/ios-layout.d.ts.map +1 -0
  71. package/dist/driver/ios-layout.js +52 -0
  72. package/dist/driver/ios-layout.js.map +1 -0
  73. package/dist/driver/ios-playback.d.ts +10 -0
  74. package/dist/driver/ios-playback.d.ts.map +1 -0
  75. package/dist/driver/ios-playback.js +47 -0
  76. package/dist/driver/ios-playback.js.map +1 -0
  77. package/dist/driver/ios-shadow.d.ts +6 -0
  78. package/dist/driver/ios-shadow.d.ts.map +1 -0
  79. package/dist/driver/ios-shadow.js +52 -0
  80. package/dist/driver/ios-shadow.js.map +1 -0
  81. package/dist/driver/ios.d.ts.map +1 -1
  82. package/dist/driver/ios.js +163 -28
  83. package/dist/driver/ios.js.map +1 -1
  84. package/dist/engine.d.ts +75 -5
  85. package/dist/engine.d.ts.map +1 -1
  86. package/dist/engine.js +293 -23
  87. package/dist/engine.js.map +1 -1
  88. package/dist/gradient.d.ts +90 -0
  89. package/dist/gradient.d.ts.map +1 -0
  90. package/dist/gradient.js +247 -0
  91. package/dist/gradient.js.map +1 -0
  92. package/dist/haptics.d.ts +21 -0
  93. package/dist/haptics.d.ts.map +1 -0
  94. package/dist/haptics.js +120 -0
  95. package/dist/haptics.js.map +1 -0
  96. package/dist/image-effect-native.d.ts +26 -0
  97. package/dist/image-effect-native.d.ts.map +1 -0
  98. package/dist/image-effect-native.js +73 -0
  99. package/dist/image-effect-native.js.map +1 -0
  100. package/dist/image-effect.d.ts +50 -0
  101. package/dist/image-effect.d.ts.map +1 -0
  102. package/dist/image-effect.js +290 -0
  103. package/dist/image-effect.js.map +1 -0
  104. package/dist/image-owner.d.ts +3 -0
  105. package/dist/image-owner.d.ts.map +1 -0
  106. package/dist/image-owner.js +11 -0
  107. package/dist/image-owner.js.map +1 -0
  108. package/dist/index.d.ts +47 -4
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +22 -2
  111. package/dist/index.js.map +1 -1
  112. package/dist/interaction-surface.d.ts +12 -0
  113. package/dist/interaction-surface.d.ts.map +1 -0
  114. package/dist/interaction-surface.js +36 -0
  115. package/dist/interaction-surface.js.map +1 -0
  116. package/dist/items.d.ts +20 -0
  117. package/dist/items.d.ts.map +1 -0
  118. package/dist/items.js +109 -0
  119. package/dist/items.js.map +1 -0
  120. package/dist/keyboard.d.ts +41 -0
  121. package/dist/keyboard.d.ts.map +1 -0
  122. package/dist/keyboard.js +256 -0
  123. package/dist/keyboard.js.map +1 -0
  124. package/dist/label.d.ts +7 -0
  125. package/dist/label.d.ts.map +1 -0
  126. package/dist/label.js +60 -0
  127. package/dist/label.js.map +1 -0
  128. package/dist/layout.d.ts +13 -0
  129. package/dist/layout.d.ts.map +1 -0
  130. package/dist/layout.js +348 -0
  131. package/dist/layout.js.map +1 -0
  132. package/dist/liquid-grid.d.ts +3 -0
  133. package/dist/liquid-grid.d.ts.map +1 -0
  134. package/dist/liquid-grid.js +24 -0
  135. package/dist/liquid-grid.js.map +1 -0
  136. package/dist/liquid.d.ts +18 -0
  137. package/dist/liquid.d.ts.map +1 -0
  138. package/dist/liquid.js +234 -0
  139. package/dist/liquid.js.map +1 -0
  140. package/dist/loader.d.ts +19 -0
  141. package/dist/loader.d.ts.map +1 -0
  142. package/dist/loader.js +173 -0
  143. package/dist/loader.js.map +1 -0
  144. package/dist/motion-scope.d.ts +39 -0
  145. package/dist/motion-scope.d.ts.map +1 -0
  146. package/dist/motion-scope.js +238 -0
  147. package/dist/motion-scope.js.map +1 -0
  148. package/dist/motion-states.d.ts +43 -0
  149. package/dist/motion-states.d.ts.map +1 -0
  150. package/dist/motion-states.js +220 -0
  151. package/dist/motion-states.js.map +1 -0
  152. package/dist/particles.d.ts +39 -0
  153. package/dist/particles.d.ts.map +1 -0
  154. package/dist/particles.js +265 -0
  155. package/dist/particles.js.map +1 -0
  156. package/dist/path.d.ts +12 -0
  157. package/dist/path.d.ts.map +1 -0
  158. package/dist/path.js +90 -0
  159. package/dist/path.js.map +1 -0
  160. package/dist/playback-group.d.ts +6 -0
  161. package/dist/playback-group.d.ts.map +1 -0
  162. package/dist/playback-group.js +141 -0
  163. package/dist/playback-group.js.map +1 -0
  164. package/dist/playback.d.ts +25 -0
  165. package/dist/playback.d.ts.map +1 -0
  166. package/dist/playback.js +35 -0
  167. package/dist/playback.js.map +1 -0
  168. package/dist/press.d.ts.map +1 -1
  169. package/dist/press.js +23 -7
  170. package/dist/press.js.map +1 -1
  171. package/dist/properties.d.ts +6 -3
  172. package/dist/properties.d.ts.map +1 -1
  173. package/dist/properties.js +26 -2
  174. package/dist/properties.js.map +1 -1
  175. package/dist/reveal.d.ts +25 -0
  176. package/dist/reveal.d.ts.map +1 -0
  177. package/dist/reveal.js +294 -0
  178. package/dist/reveal.js.map +1 -0
  179. package/dist/roll-diff.d.ts +22 -0
  180. package/dist/roll-diff.d.ts.map +1 -0
  181. package/dist/roll-diff.js +36 -0
  182. package/dist/roll-diff.js.map +1 -0
  183. package/dist/roll.d.ts +29 -0
  184. package/dist/roll.d.ts.map +1 -0
  185. package/dist/roll.js +442 -0
  186. package/dist/roll.js.map +1 -0
  187. package/dist/scroll-linked.d.ts +10 -0
  188. package/dist/scroll-linked.d.ts.map +1 -0
  189. package/dist/scroll-linked.js +58 -0
  190. package/dist/scroll-linked.js.map +1 -0
  191. package/dist/sequence.d.ts +10 -5
  192. package/dist/sequence.d.ts.map +1 -1
  193. package/dist/sequence.js +208 -14
  194. package/dist/sequence.js.map +1 -1
  195. package/dist/shimmer.d.ts +12 -0
  196. package/dist/shimmer.d.ts.map +1 -0
  197. package/dist/shimmer.js +161 -0
  198. package/dist/shimmer.js.map +1 -0
  199. package/dist/svg-path.d.ts +14 -0
  200. package/dist/svg-path.d.ts.map +1 -0
  201. package/dist/svg-path.js +281 -0
  202. package/dist/svg-path.js.map +1 -0
  203. package/dist/symbol-effect.d.ts +23 -0
  204. package/dist/symbol-effect.d.ts.map +1 -0
  205. package/dist/symbol-effect.js +187 -0
  206. package/dist/symbol-effect.js.map +1 -0
  207. package/dist/text-layout.d.ts +30 -0
  208. package/dist/text-layout.d.ts.map +1 -0
  209. package/dist/text-layout.js +185 -0
  210. package/dist/text-layout.js.map +1 -0
  211. package/dist/text.d.ts +17 -0
  212. package/dist/text.d.ts.map +1 -0
  213. package/dist/text.js +263 -0
  214. package/dist/text.js.map +1 -0
  215. package/dist/track.d.ts +3 -1
  216. package/dist/track.d.ts.map +1 -1
  217. package/dist/track.js +266 -13
  218. package/dist/track.js.map +1 -1
  219. package/dist/transition.d.ts +32 -1
  220. package/dist/transition.d.ts.map +1 -1
  221. package/dist/transition.js +58 -6
  222. package/dist/transition.js.map +1 -1
  223. package/dist/types.d.ts +48 -1
  224. package/dist/types.d.ts.map +1 -1
  225. package/dist/vue/index.d.ts +95 -0
  226. package/dist/vue/index.d.ts.map +1 -0
  227. package/dist/vue/index.js +163 -0
  228. package/dist/vue/index.js.map +1 -0
  229. package/dist/zoom-native.d.ts +30 -0
  230. package/dist/zoom-native.d.ts.map +1 -0
  231. package/dist/zoom-native.js +56 -0
  232. package/dist/zoom-native.js.map +1 -0
  233. package/dist/zoomable.d.ts +48 -0
  234. package/dist/zoomable.d.ts.map +1 -0
  235. package/dist/zoomable.js +249 -0
  236. package/dist/zoomable.js.map +1 -0
  237. package/package.json +29 -5
  238. package/platforms/android/include.gradle +7 -0
  239. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  240. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  241. package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +195 -0
  242. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  243. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  244. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
  245. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  246. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  247. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  248. package/platforms/android/proguard-rules.pro +21 -0
  249. package/platforms/ios/build.xcconfig +1 -0
  250. package/platforms/ios/src/JoltImageEffect.h +25 -0
  251. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  252. package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
  253. package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
  254. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  255. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  256. package/platforms/ios/src/module.modulemap +6 -0
  257. package/src/android-plugin.d.ts +184 -0
  258. package/src/animate.ts +249 -36
  259. package/src/color.ts +101 -0
  260. package/src/config.ts +84 -0
  261. package/src/decay.ts +45 -0
  262. package/src/decoration.ts +180 -0
  263. package/src/drag-input.ts +74 -0
  264. package/src/drag-physics.ts +110 -0
  265. package/src/drag.ts +395 -0
  266. package/src/driver/android-color.ts +170 -0
  267. package/src/driver/android-decoration.ts +182 -0
  268. package/src/driver/android-gradient.ts +109 -0
  269. package/src/driver/android-playback.ts +157 -0
  270. package/src/driver/android.ts +208 -20
  271. package/src/driver/index.ts +37 -2
  272. package/src/driver/ios-color.ts +441 -0
  273. package/src/driver/ios-decoration.ts +332 -0
  274. package/src/driver/ios-gradient.ts +114 -0
  275. package/src/driver/ios-layout.ts +63 -0
  276. package/src/driver/ios-playback.ts +51 -0
  277. package/src/driver/ios-shadow.ts +50 -0
  278. package/src/driver/ios.ts +193 -33
  279. package/src/engine.ts +321 -27
  280. package/src/gradient.ts +372 -0
  281. package/src/haptics.ts +148 -0
  282. package/src/image-effect-native.ts +129 -0
  283. package/src/image-effect.ts +336 -0
  284. package/src/image-owner.ts +12 -0
  285. package/src/index.ts +75 -4
  286. package/src/interaction-surface.ts +41 -0
  287. package/src/ios-plugin.d.ts +81 -0
  288. package/src/items.ts +140 -0
  289. package/src/keyboard.ts +291 -0
  290. package/src/label.ts +76 -0
  291. package/src/layout.ts +408 -0
  292. package/src/liquid-grid.ts +28 -0
  293. package/src/liquid.ts +287 -0
  294. package/src/loader.ts +202 -0
  295. package/src/motion-scope.ts +279 -0
  296. package/src/motion-states.ts +269 -0
  297. package/src/particles.ts +323 -0
  298. package/src/path.ts +124 -0
  299. package/src/playback-group.ts +143 -0
  300. package/src/playback.ts +62 -0
  301. package/src/press.ts +26 -10
  302. package/src/properties.ts +32 -6
  303. package/src/reveal.ts +374 -0
  304. package/src/roll-diff.ts +57 -0
  305. package/src/roll.ts +515 -0
  306. package/src/scroll-linked.ts +73 -0
  307. package/src/sequence.ts +204 -14
  308. package/src/shimmer.ts +180 -0
  309. package/src/svg-path.ts +355 -0
  310. package/src/symbol-effect.ts +226 -0
  311. package/src/text-layout.ts +228 -0
  312. package/src/text.ts +279 -0
  313. package/src/track.ts +347 -12
  314. package/src/transition.ts +93 -6
  315. package/src/types.ts +49 -1
  316. package/src/vue/index.ts +231 -0
  317. package/src/zoom-native.ts +124 -0
  318. package/src/zoomable.ts +330 -0
package/src/press.ts CHANGED
@@ -1,6 +1,8 @@
1
- import type { EventData, TouchGestureEventData } from '@nativescript/core'
2
- import { animate, toView } from './animate.js'
3
- import { isProperty, readValue, type Property } from './properties.js'
1
+ import { Color, type EventData, type TouchGestureEventData } from '@nativescript/core'
2
+ import { isColorProperty } from './color.js'
3
+ import { animate, startingValues, toView } from './animate.js'
4
+ import type { PlaybackAnimation } from './playback.js'
5
+ import { readValue } from './properties.js'
4
6
  import type { SpringName, Transition } from './transition.js'
5
7
  import type { AnimateOptions, Keyframes, Target } from './types.js'
6
8
 
@@ -22,18 +24,22 @@ export function press(
22
24
  options?: AnimateOptions | SpringName,
23
25
  ): () => void {
24
26
  const view = toView(target)
25
- const properties = Object.keys(pressed).flatMap((key): Property[] =>
26
- key === 'scale' ? ['scaleX', 'scaleY'] : isProperty(key) ? [key] : [],
27
- )
27
+ const properties = [...startingValues(pressed, options).keys()]
28
28
  let rest: Keyframes | undefined
29
+ let active: PlaybackAnimation | undefined
29
30
 
30
31
  const onTouch = (event: EventData) => {
31
32
  const { action } = event as TouchGestureEventData
32
33
  if (action === 'down' && !rest && view.isEnabled !== false) {
33
- rest = Object.fromEntries(properties.map((p) => [p, readValue(view, p)]))
34
- animate(view, pressed, options ?? pressIn)
34
+ rest = Object.fromEntries(
35
+ properties.map((p) => [
36
+ p,
37
+ isColorProperty(p) ? new Color(readValue(view, p)) : readValue(view, p),
38
+ ]),
39
+ )
40
+ active = animate(view, pressed, options ?? pressIn)
35
41
  } else if ((action === 'up' || action === 'cancel') && rest) {
36
- animate(
42
+ active = animate(
37
43
  view,
38
44
  rest,
39
45
  typeof options === 'object'
@@ -44,6 +50,16 @@ export function press(
44
50
  }
45
51
  }
46
52
 
53
+ const cancel = () => {
54
+ active?.stop()
55
+ active = undefined
56
+ rest = undefined
57
+ }
47
58
  view.on('touch', onTouch)
48
- return () => view.off('touch', onTouch)
59
+ view.on('unloaded', cancel)
60
+ return () => {
61
+ view.off('touch', onTouch)
62
+ view.off('unloaded', cancel)
63
+ cancel()
64
+ }
49
65
  }
package/src/properties.ts CHANGED
@@ -1,7 +1,15 @@
1
- import type { View } from '@nativescript/core'
1
+ import { Color, type View } from '@nativescript/core'
2
+ import {
3
+ isDecorationProperty,
4
+ readDecoration,
5
+ writeDecoration,
6
+ type DecorationProperty,
7
+ } from './decoration.js'
8
+ import { isColorProperty, readColor, type ColorProperty } from './color.js'
2
9
 
3
- export type Property =
10
+ export type NumericProperty =
4
11
  'opacity' | 'x' | 'y' | 'scaleX' | 'scaleY' | 'rotate' | 'rotateX' | 'rotateY'
12
+ export type Property = NumericProperty | ColorProperty | DecorationProperty
5
13
 
6
14
  const viewKeys = {
7
15
  opacity: 'opacity',
@@ -12,9 +20,9 @@ const viewKeys = {
12
20
  rotate: 'rotate',
13
21
  rotateX: 'rotateX',
14
22
  rotateY: 'rotateY',
15
- } as const satisfies Record<Property, keyof View>
23
+ } as const satisfies Record<NumericProperty, keyof View>
16
24
 
17
- const defaults: Record<Property, number> = {
25
+ const defaults: Record<NumericProperty, number> = {
18
26
  opacity: 1,
19
27
  x: 0,
20
28
  y: 0,
@@ -25,8 +33,19 @@ const defaults: Record<Property, number> = {
25
33
  rotateY: 0,
26
34
  }
27
35
 
28
- /** How close to its target a spring has to be to count as settled. */
36
+ /** How close to its target a spring or decay has to be to count as settled. */
29
37
  export const precision: Record<Property, number> = {
38
+ borderRadius: 0.1,
39
+ borderWidth: 0.1,
40
+ shadowOpacity: 0.002,
41
+ shadowRadius: 0.1,
42
+ shadowOffsetX: 0.1,
43
+ shadowOffsetY: 0.1,
44
+ elevation: 0.1,
45
+ translationZ: 0.1,
46
+ backgroundColor: 0.002,
47
+ borderColor: 0.002,
48
+ color: 0.002,
30
49
  opacity: 0.002,
31
50
  x: 0.25,
32
51
  y: 0.25,
@@ -38,14 +57,21 @@ export const precision: Record<Property, number> = {
38
57
  }
39
58
 
40
59
  export function isProperty(key: string): key is Property {
41
- return key in viewKeys
60
+ return Object.hasOwn(viewKeys, key) || isColorProperty(key) || isDecorationProperty(key)
42
61
  }
43
62
 
44
63
  export function readValue(view: View, property: Property): number {
64
+ if (isColorProperty(property)) return readColor(view, property)
65
+ if (isDecorationProperty(property)) return readDecoration(view, property)
45
66
  const value = view[viewKeys[property]]
46
67
  return typeof value === 'number' && !Number.isNaN(value) ? value : defaults[property]
47
68
  }
48
69
 
49
70
  export function writeValue(view: View, property: Property, value: number) {
71
+ if (isColorProperty(property)) {
72
+ view[property] = new Color(value)
73
+ return
74
+ }
75
+ if (isDecorationProperty(property)) return writeDecoration(view, property, value)
50
76
  view[viewKeys[property]] = value
51
77
  }
package/src/reveal.ts ADDED
@@ -0,0 +1,374 @@
1
+ import { Application, isIOS, type View } from '@nativescript/core'
2
+ import { toView } from './animate.js'
3
+ import { resolveEasing } from './easing.js'
4
+ import type { Animation } from './engine.js'
5
+ import type { Target } from './types.js'
6
+ import { reducedMotion } from './config.js'
7
+
8
+ export interface RevealOptions {
9
+ /** Circle origin in DIPs from the target's top-left corner. Defaults to its center. */
10
+ from?: { x: number; y: number }
11
+ /** Milliseconds. Defaults to 400; zero changes visibility immediately. */
12
+ duration?: number
13
+ }
14
+
15
+ export interface ConcealOptions {
16
+ /** Circle destination in DIPs from the target's top-left corner. Defaults to its center. */
17
+ to?: { x: number; y: number }
18
+ /** Milliseconds. Defaults to 400; zero changes visibility immediately. */
19
+ duration?: number
20
+ }
21
+
22
+ type Circle = { x: number; y: number; radius: number }
23
+ type MaskedView = UIView & { originalMask?: CALayer | null; maskType?: unknown }
24
+ type Snapshot = { circle: Circle; native: UIView; bounds: CGRect }
25
+ const running = new WeakMap<View, CircularAnimation>()
26
+ const callbacks = new WeakMap<NSObject, (finished: boolean) => void>()
27
+ const ease = resolveEasing('easeInOut')
28
+ const key = 'jolt:reveal'
29
+ let Delegate: typeof NSObject | undefined
30
+
31
+ /** Reveals an iOS view through an expanding circle, then removes the temporary mask. */
32
+ export function reveal(target: Target, options: RevealOptions = {}): Animation {
33
+ return start(target, true, options.from, options.duration ?? 400)
34
+ }
35
+
36
+ /** Contracts an iOS view to a point, then sets visibility to 'hidden' without collapsing layout. */
37
+ export function conceal(target: Target, options: ConcealOptions = {}): Animation {
38
+ return start(target, false, options.to, options.duration ?? 400)
39
+ }
40
+
41
+ function start(
42
+ target: Target,
43
+ opening: boolean,
44
+ point: { x: number; y: number } | undefined,
45
+ duration: number,
46
+ ): Animation {
47
+ if (!Number.isFinite(duration) || duration < 0)
48
+ throw new RangeError('Reveal duration must be a nonnegative finite number.')
49
+ if (
50
+ point !== undefined &&
51
+ (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y))
52
+ )
53
+ throw new TypeError('Reveal origin must have finite x and y coordinates.')
54
+ const view = toView(target)
55
+ const previous = running.get(view)
56
+ const snapshot = previous?.snapshot()
57
+ const original = previous?.originalVisibility ?? view.visibility
58
+ previous?.stop()
59
+ return new CircularAnimation(
60
+ view,
61
+ opening,
62
+ point && { ...point },
63
+ duration,
64
+ original,
65
+ snapshot,
66
+ )
67
+ }
68
+
69
+ function sameBounds(a: CGRect, b: CGRect) {
70
+ return (
71
+ a.origin.x === b.origin.x &&
72
+ a.origin.y === b.origin.y &&
73
+ a.size.width === b.size.width &&
74
+ a.size.height === b.size.height
75
+ )
76
+ }
77
+
78
+ function path(circle: Circle): CAShapeLayer['path'] {
79
+ // Keep the same oval topology even at the closed endpoint.
80
+ const radius = Math.max(0.01, circle.radius)
81
+ return UIBezierPath.bezierPathWithOvalInRect(
82
+ CGRectMake(circle.x - radius, circle.y - radius, radius * 2, radius * 2),
83
+ ).CGPath
84
+ }
85
+
86
+ function completion(
87
+ callback: (finished: boolean) => void,
88
+ ): NSObject & CAAnimationDelegate {
89
+ if (!Delegate) {
90
+ // Use the runtime subclass API so published JS needs no NativeClass transform.
91
+ const base = NSObject as typeof NSObject & {
92
+ extend(
93
+ methods: object,
94
+ options: { protocols: unknown[]; name: string },
95
+ ): typeof NSObject
96
+ }
97
+ Delegate = base.extend(
98
+ {
99
+ animationDidStopFinished(
100
+ this: NSObject,
101
+ _animation: CAAnimation,
102
+ finished: boolean,
103
+ ) {
104
+ callbacks.get(this)?.(finished)
105
+ },
106
+ },
107
+ { protocols: [CAAnimationDelegate], name: 'JoltCircularRevealDelegate' },
108
+ )
109
+ }
110
+ const delegate = Delegate.new()
111
+ callbacks.set(delegate, callback)
112
+ return delegate
113
+ }
114
+
115
+ class CircularAnimation implements Animation {
116
+ readonly finished: Promise<boolean>
117
+ private resolve!: (finished: boolean) => void
118
+ private settled = false
119
+ private queued = false
120
+ private changingVisibility = false
121
+ private waitingForLoad: boolean
122
+ private native: MaskedView | undefined
123
+ private bounds: CGRect | undefined
124
+ private wrapper: CALayer | undefined
125
+ private shape: CAShapeLayer | undefined
126
+ private originalMask: CALayer | null = null
127
+ private savedMask: CALayer | null = null
128
+ private delegate: NSObject | undefined
129
+ private observer: ReturnType<typeof Application.ios.addNotificationObserver> | undefined
130
+ private from: Circle | undefined
131
+ private to: Circle | undefined
132
+ private startedAt = 0
133
+
134
+ constructor(
135
+ private readonly view: View,
136
+ private readonly opening: boolean,
137
+ private readonly point: { x: number; y: number } | undefined,
138
+ private readonly duration: number,
139
+ readonly originalVisibility: View['visibility'],
140
+ private readonly inherited?: Snapshot,
141
+ ) {
142
+ this.finished = new Promise((resolve) => {
143
+ this.resolve = resolve
144
+ })
145
+ this.waitingForLoad = !view.isLoaded
146
+ running.set(view, this)
147
+ if (!isIOS) {
148
+ this.end(false)
149
+ return
150
+ }
151
+ view.on('loaded', this.loaded)
152
+ view.on('layoutChanged', this.layoutChanged)
153
+ view.on('unloaded', this.stop)
154
+ view.style.on('backgroundInternalChange', this.stop)
155
+ view.style.on('visibilityChange', this.visibilityChanged)
156
+ if (opening && view.visibility === 'collapse') this.visibility('hidden')
157
+ this.schedule()
158
+ }
159
+
160
+ then<A = boolean, B = never>(
161
+ onfulfilled?: ((finished: boolean) => A | PromiseLike<A>) | null,
162
+ onrejected?: ((reason: unknown) => B | PromiseLike<B>) | null,
163
+ ): Promise<A | B> {
164
+ return this.finished.then(onfulfilled, onrejected)
165
+ }
166
+
167
+ /** Cancels and restores visibility from before the interrupted reveal/conceal chain. */
168
+ stop = () => this.end(false)
169
+
170
+ snapshot(): Snapshot | undefined {
171
+ if (!this.shape || !this.native || !this.bounds || !this.from || !this.to) return
172
+ const presented = this.shape.presentationLayer() as CAShapeLayer | null
173
+ let circle: Circle
174
+ if (presented?.path) {
175
+ const box = CGPathGetBoundingBox(presented.path)
176
+ circle = {
177
+ x: box.origin.x + box.size.width / 2,
178
+ y: box.origin.y + box.size.height / 2,
179
+ radius: box.size.width / 2,
180
+ }
181
+ } else {
182
+ const progress = ease(
183
+ Math.max(
184
+ 0,
185
+ Math.min(1, ((CACurrentMediaTime() - this.startedAt) * 1000) / this.duration),
186
+ ),
187
+ )
188
+ const lerp = (a: number, b: number) => a + (b - a) * progress
189
+ circle = {
190
+ x: lerp(this.from.x, this.to.x),
191
+ y: lerp(this.from.y, this.to.y),
192
+ radius: lerp(this.from.radius, this.to.radius),
193
+ }
194
+ }
195
+ return { circle, native: this.native, bounds: this.bounds }
196
+ }
197
+
198
+ private visibility(value: View['visibility']) {
199
+ this.changingVisibility = true
200
+ try {
201
+ this.view.visibility = value
202
+ } finally {
203
+ this.changingVisibility = false
204
+ }
205
+ }
206
+
207
+ private visibilityChanged = () => {
208
+ if (!this.changingVisibility) this.end(false, true)
209
+ }
210
+
211
+ private loaded = () => {
212
+ this.waitingForLoad = false
213
+ this.schedule()
214
+ }
215
+
216
+ private layoutChanged = () => {
217
+ if (this.shape) {
218
+ const native = this.view.nativeViewProtected as UIView | undefined
219
+ if (
220
+ native !== this.native ||
221
+ !native?.window ||
222
+ !sameBounds(native.layer.bounds, this.bounds!)
223
+ ) {
224
+ this.stop()
225
+ return
226
+ }
227
+ }
228
+ this.schedule()
229
+ }
230
+
231
+ private schedule = () => {
232
+ if (this.settled || this.queued) return
233
+ this.queued = true
234
+ void Promise.resolve().then(() => {
235
+ this.queued = false
236
+ if (this.settled) return
237
+ try {
238
+ this.update()
239
+ } catch (error) {
240
+ this.stop()
241
+ throw error
242
+ }
243
+ })
244
+ }
245
+
246
+ private motionChanged = () => {
247
+ if (reducedMotion()) this.end(true)
248
+ }
249
+
250
+ private update() {
251
+ if (!this.opening && !this.inherited && this.view.visibility !== 'visible')
252
+ return this.end(true, true)
253
+ if (this.duration === 0 || reducedMotion()) return this.end(true)
254
+ if (this.waitingForLoad || !this.view.isLoaded) return
255
+ const native = this.view.nativeViewProtected as MaskedView | undefined
256
+ if (this.shape) {
257
+ if (native !== this.native || native?.layer.mask !== this.wrapper) this.stop()
258
+ return
259
+ }
260
+ if (!native?.window) return
261
+ const bounds = native.layer.bounds
262
+ if (bounds.size.width <= 0 || bounds.size.height <= 0) return
263
+ const inherited =
264
+ this.inherited?.native === native && sameBounds(this.inherited.bounds, bounds)
265
+ ? this.inherited.circle
266
+ : undefined
267
+ const x = this.point
268
+ ? bounds.origin.x + this.point.x
269
+ : (inherited?.x ?? bounds.origin.x + bounds.size.width / 2)
270
+ const y = this.point
271
+ ? bounds.origin.y + this.point.y
272
+ : (inherited?.y ?? bounds.origin.y + bounds.size.height / 2)
273
+ const radius =
274
+ Math.hypot(
275
+ Math.max(
276
+ Math.abs(x - bounds.origin.x),
277
+ Math.abs(x - bounds.origin.x - bounds.size.width),
278
+ ),
279
+ Math.max(
280
+ Math.abs(y - bounds.origin.y),
281
+ Math.abs(y - bounds.origin.y - bounds.size.height),
282
+ ),
283
+ ) + 1
284
+ this.from = inherited ?? { x, y, radius: this.opening ? 0 : radius }
285
+ this.to = { x, y, radius: this.opening ? radius : 0 }
286
+ this.native = native
287
+ this.bounds = bounds
288
+ this.originalMask = native.layer.mask
289
+ this.savedMask =
290
+ native.maskType != null ? (native.originalMask ?? null) : this.originalMask
291
+
292
+ CATransaction.begin()
293
+ CATransaction.setDisableActions(true)
294
+ try {
295
+ // A plain container prevents Core from mistaking our circle for its own clip path.
296
+ const wrapper = (this.wrapper = CALayer.new())
297
+ const shape = (this.shape = CAShapeLayer.new())
298
+ wrapper.name = key
299
+ for (const layer of [wrapper, shape]) {
300
+ layer.bounds = bounds
301
+ layer.position = CGPointMake(
302
+ bounds.origin.x + bounds.size.width / 2,
303
+ bounds.origin.y + bounds.size.height / 2,
304
+ )
305
+ }
306
+ shape.fillColor = UIColor.blackColor.CGColor
307
+ shape.path = path(this.to)
308
+ wrapper.addSublayer(shape)
309
+ // Same coordinate space as the original host, including bounds origins.
310
+ native.layer.mask = null
311
+ wrapper.mask = this.originalMask
312
+ native.layer.mask = wrapper
313
+ const animation = CABasicAnimation.animationWithKeyPath('path')
314
+ animation.fromValue = path(this.from)
315
+ animation.toValue = shape.path
316
+ animation.duration = this.duration / 1000
317
+ animation.timingFunction = CAMediaTimingFunction.functionWithName(
318
+ kCAMediaTimingFunctionEaseInEaseOut,
319
+ )
320
+ this.delegate = animation.delegate = completion((finished) => this.end(finished))
321
+ this.startedAt = CACurrentMediaTime()
322
+ shape.addAnimationForKey(animation, key)
323
+ this.observer = Application.ios.addNotificationObserver(
324
+ UIAccessibilityReduceMotionStatusDidChangeNotification,
325
+ this.motionChanged,
326
+ )
327
+ this.visibility('visible')
328
+ } finally {
329
+ CATransaction.commit()
330
+ }
331
+ }
332
+
333
+ private end(finished: boolean, keepVisibility = false) {
334
+ if (this.settled) return
335
+ this.settled = true
336
+ const { view, native, wrapper, shape } = this
337
+ view.off('loaded', this.loaded)
338
+ view.off('layoutChanged', this.layoutChanged)
339
+ view.off('unloaded', this.stop)
340
+ view.style.off('backgroundInternalChange', this.stop)
341
+ view.style.off('visibilityChange', this.visibilityChanged)
342
+ if (running.get(view) === this) running.delete(view)
343
+ if (this.observer)
344
+ Application.ios.removeNotificationObserver(
345
+ this.observer,
346
+ UIAccessibilityReduceMotionStatusDidChangeNotification,
347
+ )
348
+ if (this.delegate) callbacks.delete(this.delegate)
349
+ if (native && wrapper) {
350
+ CATransaction.begin()
351
+ CATransaction.setDisableActions(true)
352
+ try {
353
+ // Hide before removing a closed mask so conceal never flashes its content.
354
+ if (!keepVisibility)
355
+ this.visibility(
356
+ finished ? (this.opening ? 'visible' : 'hidden') : this.originalVisibility,
357
+ )
358
+ shape?.removeAnimationForKey(key)
359
+ wrapper.mask = null
360
+ if (native.layer.mask === wrapper) native.layer.mask = this.originalMask
361
+ // Core can save the temporary mask while installing a new clip/border mask.
362
+ if (native.originalMask === wrapper) native.originalMask = this.savedMask
363
+ } finally {
364
+ CATransaction.commit()
365
+ }
366
+ } else if (isIOS && !keepVisibility) {
367
+ this.visibility(
368
+ finished ? (this.opening ? 'visible' : 'hidden') : this.originalVisibility,
369
+ )
370
+ }
371
+ this.native = this.wrapper = this.shape = this.delegate = this.observer = undefined
372
+ this.resolve(finished)
373
+ }
374
+ }
@@ -0,0 +1,57 @@
1
+ export interface Grapheme {
2
+ text: string
3
+ /** UTF-16 offsets, the units NSString locations and Java char indices use. */
4
+ start: number
5
+ end: number
6
+ }
7
+
8
+ export interface RollChange {
9
+ /** The grapheme that rolls in, placed in the new text. */
10
+ next: Grapheme
11
+ /** The grapheme it replaces at the same position from the end, when there is one. */
12
+ previous: Grapheme | undefined
13
+ }
14
+
15
+ export interface RollDiff {
16
+ /** Positions that differ, from the last one backwards. */
17
+ changes: RollChange[]
18
+ /** Old graphemes to the left of the new text's first one; they roll out alone. */
19
+ removed: Grapheme[]
20
+ }
21
+
22
+ const segmenter =
23
+ typeof Intl !== 'undefined' && 'Segmenter' in Intl
24
+ ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
25
+ : undefined
26
+
27
+ export function graphemes(text: string): Grapheme[] {
28
+ if (segmenter)
29
+ return [...segmenter.segment(text)].map(({ segment, index }) => ({
30
+ text: segment,
31
+ start: index,
32
+ end: index + segment.length,
33
+ }))
34
+ const pieces: Grapheme[] = []
35
+ let offset = 0
36
+ for (const character of text) {
37
+ pieces.push({ text: character, start: offset, end: offset + character.length })
38
+ offset += character.length
39
+ }
40
+ return pieces
41
+ }
42
+
43
+ /** Aligns both texts on their last grapheme, so the units digit stays put when the count grows. */
44
+ export function diffFromEnd(previous: string, next: string): RollDiff {
45
+ const before = graphemes(previous)
46
+ const after = graphemes(next)
47
+ const changes: RollChange[] = []
48
+ const removed: Grapheme[] = []
49
+ const count = Math.max(before.length, after.length)
50
+ for (let offset = 1; offset <= count; offset++) {
51
+ const old = before[before.length - offset]
52
+ const piece = after[after.length - offset]
53
+ if (!piece) removed.push(old!)
54
+ else if (!old || old.text !== piece.text) changes.push({ next: piece, previous: old })
55
+ }
56
+ return { changes, removed }
57
+ }