@apexnative/navigation 1.0.0-alpha.6 → 1.0.0-alpha.7

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 (236) hide show
  1. package/ApexNavigation.podspec +2 -1
  2. package/CHANGELOG.md +7 -0
  3. package/android/CLAUDE.md +19 -10
  4. package/android/CORE.md +3 -2
  5. package/android/consumer-rules.pro +6 -0
  6. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabOptions.kt +1 -1
  7. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +1 -1
  8. package/android/src/main/java/dev/apexnative/navigation/options/LargeTitleOptions.kt +1 -1
  9. package/android/src/main/java/dev/apexnative/navigation/options/LayoutAnimation.kt +1 -1
  10. package/android/src/main/java/dev/apexnative/navigation/options/OptionKey.kt +18 -0
  11. package/android/src/main/java/dev/apexnative/navigation/options/OptionPaths.kt +91 -0
  12. package/android/src/main/java/dev/apexnative/navigation/options/SheetOptions.kt +7 -4
  13. package/android/src/main/java/dev/apexnative/navigation/options/TitleOptions.kt +2 -2
  14. package/android/src/main/java/dev/apexnative/navigation/options/TopBarAppearanceOptions.kt +1 -1
  15. package/android/src/main/java/dev/apexnative/navigation/options/TopBarOptions.kt +16 -7
  16. package/android/src/main/java/dev/apexnative/navigation/react/ContentHeights.kt +95 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/ContentMounts.kt +66 -0
  18. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +4 -0
  19. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +41 -3
  20. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +16 -1
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +27 -7
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +12 -1
  23. package/android/src/main/java/dev/apexnative/navigation/utils/MaterialTheme.kt +5 -6
  24. package/android/src/main/java/dev/apexnative/navigation/utils/SystemAnimations.kt +36 -0
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +329 -55
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +26 -5
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +60 -2
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -2
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +32 -4
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +46 -7
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +15 -3
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +7 -4
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabMotion.kt +142 -0
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +215 -43
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +25 -26
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarRowTransition.kt +3 -2
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/ButtonPlaces.kt +11 -5
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +36 -38
  39. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +124 -181
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +8 -1
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionLayer.kt +37 -112
  42. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +56 -3
  43. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +170 -153
  44. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMorph.kt +199 -0
  45. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +55 -45
  46. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/{TextChangeAnimator.kt → TextMorph.kt} +79 -129
  47. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewMorph.kt +108 -0
  48. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabCopy.kt +85 -0
  49. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabMenu.kt +13 -0
  50. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +5 -24
  51. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +51 -34
  52. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonComponentCell.kt +6 -6
  53. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonView.kt +113 -0
  54. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarMenu.kt +219 -0
  55. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +121 -175
  56. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonMenus.kt +34 -64
  57. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonRow.kt +216 -0
  58. package/android/src/main/jni/ANNContentHeights.cpp +82 -0
  59. package/android/src/main/res/drawable/apex_navigation_ic_arrow_right.xml +14 -0
  60. package/android/src/main/res/layout/apex_navigation_menu_header.xml +19 -0
  61. package/android/src/main/res/layout/apex_navigation_menu_item.xml +68 -0
  62. package/android/src/main/res/values/styles.xml +1 -1
  63. package/android/src/main/res/values-night/styles.xml +1 -1
  64. package/cli/doctor/checks/app.js +16 -1
  65. package/cli/project.js +4 -0
  66. package/cli/setup/index.js +5 -0
  67. package/cli/setup/steps/metro.js +46 -0
  68. package/docs-md/architecture/native-screens.md +4 -0
  69. package/docs-md/architecture/theme-and-context.md +131 -12
  70. package/docs-md/architecture/transitions.md +43 -4
  71. package/docs-md/flows/bottom-sheets.md +4 -2
  72. package/docs-md/fundamentals/screens-and-props.md +9 -1
  73. package/docs-md/guides/cli.md +3 -1
  74. package/docs-md/help/troubleshooting.md +1 -1
  75. package/docs-md/index.json +29 -1
  76. package/docs-md/screen-ui/back-and-gestures.md +3 -1
  77. package/docs-md/screen-ui/bar-buttons.md +1 -1
  78. package/docs-md/screen-ui/toolbar-and-fab.md +2 -0
  79. package/docs-md/start/installation.md +16 -1
  80. package/ios/Animations/ANNFrameDriver.h +55 -15
  81. package/ios/Animations/ANNFrameDriver.mm +214 -60
  82. package/ios/Animations/ANNTransitionAnimator.h +26 -5
  83. package/ios/Animations/ANNTransitionAnimator.mm +49 -3
  84. package/ios/Animations/ANNTransitionPlan.h +2 -2
  85. package/ios/Animations/ANNTransitionPlan.mm +10 -0
  86. package/ios/Animations/Elements/ANNElementFinder.h +2 -2
  87. package/ios/Animations/Elements/ANNElementFinder.mm +2 -2
  88. package/ios/Animations/Elements/ANNElementTransition.h +3 -3
  89. package/ios/Animations/Elements/ANNElementTransition.mm +2 -2
  90. package/ios/Animations/Elements/ANNSharedElementFlight.h +3 -3
  91. package/ios/Animations/Elements/ANNSharedElementFlight.mm +181 -43
  92. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -5
  93. package/ios/Animations/Interpolators/ANNViewTracks.mm +51 -21
  94. package/ios/CLAUDE.md +15 -7
  95. package/ios/CORE.md +1 -1
  96. package/ios/Core/ANNAppDelegate.mm +0 -2
  97. package/ios/Core/ANNDeepLinkQueue.h +6 -5
  98. package/ios/Core/ANNDeepLinkQueue.mm +22 -10
  99. package/ios/Core/ANNSceneDelegate.mm +2 -2
  100. package/ios/Core/ApexNavigation.mm +6 -2
  101. package/ios/Layouts/Root/ANNRootCoordinator.mm +45 -15
  102. package/ios/Layouts/Screen/ANNComponentViewController.mm +4 -1
  103. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.h +11 -2
  104. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.mm +14 -7
  105. package/ios/Layouts/Stack/ANNStackController.h +8 -0
  106. package/ios/Layouts/Stack/ANNStackController.mm +153 -11
  107. package/ios/Layouts/Stack/ANNSwipePopInteraction.h +26 -0
  108. package/ios/Layouts/Stack/ANNSwipePopInteraction.mm +185 -0
  109. package/ios/Layouts/Stack/InteractivePopGestureDelegate.mm +28 -4
  110. package/ios/Options/ANNAnimationOptions.h +2 -1
  111. package/ios/Options/ANNAnimationOptions.mm +1 -1
  112. package/ios/Options/ANNOptions.h +7 -0
  113. package/ios/Options/ANNOptions.mm +45 -0
  114. package/ios/Options/ANNOptionsReader.mm +2 -4
  115. package/ios/Options/ANNStackPopAnimationOptions.h +13 -0
  116. package/ios/Options/ANNStackPopAnimationOptions.mm +11 -0
  117. package/ios/Options/Params/ColorParser.h +4 -3
  118. package/ios/Options/Params/ColorParser.mm +22 -4
  119. package/ios/React/ANNSurfaceHostingView.h +8 -0
  120. package/ios/React/ANNSurfaceHostingView.mm +28 -1
  121. package/ios/React/Slots/ANNSlotScreenView.mm +8 -2
  122. package/ios/TurboModules/ANNTurboModule.mm +46 -0
  123. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.cpp +20 -0
  124. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.h +5 -14
  125. package/lib/module/ARCHITECTURE.md +9 -3
  126. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  127. package/lib/module/api/app/AppRegistry.js +14 -2
  128. package/lib/module/api/app/AppRegistry.js.map +1 -1
  129. package/lib/module/api/app/createNavigation.js +188 -66
  130. package/lib/module/api/app/createNavigation.js.map +1 -1
  131. package/lib/module/api/events/navigationEvents.js +6 -2
  132. package/lib/module/api/events/navigationEvents.js.map +1 -1
  133. package/lib/module/api/linking/linking.js +11 -0
  134. package/lib/module/api/linking/linking.js.map +1 -1
  135. package/lib/module/api/screens/defineScreen.js +45 -3
  136. package/lib/module/api/screens/defineScreen.js.map +1 -1
  137. package/lib/module/api/screens/nativeScreens.js +31 -6
  138. package/lib/module/api/screens/nativeScreens.js.map +1 -1
  139. package/lib/module/api/screens/registerScreens.js +172 -35
  140. package/lib/module/api/screens/registerScreens.js.map +1 -1
  141. package/lib/module/api/theme/theme.js +5 -0
  142. package/lib/module/api/theme/theme.js.map +1 -1
  143. package/lib/module/engine/commands.js +21 -1
  144. package/lib/module/engine/commands.js.map +1 -1
  145. package/lib/module/engine/layout.js +7 -2
  146. package/lib/module/engine/layout.js.map +1 -1
  147. package/lib/module/engine/nativeScreens.js +9 -0
  148. package/lib/module/engine/nativeScreens.js.map +1 -1
  149. package/lib/module/engine/options.js +5 -4
  150. package/lib/module/engine/options.js.map +1 -1
  151. package/lib/module/engine/optionsChange.js +102 -0
  152. package/lib/module/engine/optionsChange.js.map +1 -0
  153. package/lib/module/engine/refresh.js +85 -0
  154. package/lib/module/engine/refresh.js.map +1 -0
  155. package/lib/module/engine/rememberedOptions.js +60 -0
  156. package/lib/module/engine/rememberedOptions.js.map +1 -1
  157. package/lib/module/engine/screens.js +26 -0
  158. package/lib/module/engine/screens.js.map +1 -1
  159. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +7 -0
  160. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  161. package/lib/typescript/api/app/AppRegistry.d.ts.map +1 -1
  162. package/lib/typescript/api/app/createNavigation.d.ts +5 -0
  163. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  164. package/lib/typescript/api/events/navigationEvents.d.ts +2 -2
  165. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  166. package/lib/typescript/api/index.d.ts +1 -1
  167. package/lib/typescript/api/index.d.ts.map +1 -1
  168. package/lib/typescript/api/linking/linking.d.ts +8 -0
  169. package/lib/typescript/api/linking/linking.d.ts.map +1 -1
  170. package/lib/typescript/api/screens/defineScreen.d.ts +23 -4
  171. package/lib/typescript/api/screens/defineScreen.d.ts.map +1 -1
  172. package/lib/typescript/api/screens/nativeScreens.d.ts +8 -1
  173. package/lib/typescript/api/screens/nativeScreens.d.ts.map +1 -1
  174. package/lib/typescript/api/screens/registerScreens.d.ts.map +1 -1
  175. package/lib/typescript/api/theme/theme.d.ts +2 -0
  176. package/lib/typescript/api/theme/theme.d.ts.map +1 -1
  177. package/lib/typescript/engine/commands.d.ts +5 -0
  178. package/lib/typescript/engine/commands.d.ts.map +1 -1
  179. package/lib/typescript/engine/layout.d.ts.map +1 -1
  180. package/lib/typescript/engine/nativeScreens.d.ts +5 -0
  181. package/lib/typescript/engine/nativeScreens.d.ts.map +1 -1
  182. package/lib/typescript/engine/optionsChange.d.ts +28 -0
  183. package/lib/typescript/engine/optionsChange.d.ts.map +1 -0
  184. package/lib/typescript/engine/refresh.d.ts +35 -0
  185. package/lib/typescript/engine/refresh.d.ts.map +1 -0
  186. package/lib/typescript/engine/rememberedOptions.d.ts +13 -0
  187. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  188. package/lib/typescript/engine/screens.d.ts +7 -0
  189. package/lib/typescript/engine/screens.d.ts.map +1 -1
  190. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  191. package/lib/typescript/interfaces/options/animations.d.ts +40 -15
  192. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  193. package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -2
  194. package/lib/typescript/interfaces/options/buttons.d.ts +2 -2
  195. package/lib/typescript/interfaces/options/modals.d.ts +6 -7
  196. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  197. package/lib/typescript/interfaces/options/stack.d.ts +8 -3
  198. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  199. package/lib/typescript/interfaces/options/topBar.d.ts +14 -0
  200. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  201. package/metro/acceptPlugin.js +57 -0
  202. package/metro/babelTransformer.js +45 -0
  203. package/metro/index.js +36 -0
  204. package/package.json +3 -1
  205. package/src/ARCHITECTURE.md +9 -3
  206. package/src/adapters/NativeANNTurboModule.ts +7 -0
  207. package/src/api/app/AppRegistry.ts +16 -2
  208. package/src/api/app/createNavigation.ts +198 -66
  209. package/src/api/events/navigationEvents.ts +9 -3
  210. package/src/api/index.ts +1 -0
  211. package/src/api/linking/linking.ts +22 -1
  212. package/src/api/screens/defineScreen.ts +49 -5
  213. package/src/api/screens/nativeScreens.ts +40 -4
  214. package/src/api/screens/registerScreens.tsx +170 -42
  215. package/src/api/theme/theme.ts +5 -0
  216. package/src/engine/commands.ts +21 -0
  217. package/src/engine/layout.ts +11 -2
  218. package/src/engine/nativeScreens.ts +10 -1
  219. package/src/engine/options.ts +6 -8
  220. package/src/engine/optionsChange.ts +107 -0
  221. package/src/engine/refresh.ts +97 -0
  222. package/src/engine/rememberedOptions.ts +70 -0
  223. package/src/engine/screens.tsx +24 -0
  224. package/src/interfaces/ComponentEvents.ts +2 -2
  225. package/src/interfaces/options/animations.ts +40 -15
  226. package/src/interfaces/options/bottomTabs.ts +2 -2
  227. package/src/interfaces/options/buttons.ts +2 -2
  228. package/src/interfaces/options/modals.ts +6 -7
  229. package/src/interfaces/options/stack.ts +8 -3
  230. package/src/interfaces/options/topBar.ts +14 -0
  231. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStub.kt +0 -31
  232. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +0 -134
  233. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +0 -78
  234. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PropertyAnimatorCreator.kt +0 -33
  235. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +0 -57
  236. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +0 -146
@@ -2,11 +2,13 @@ package dev.apexnative.navigation.views.element.animators
2
2
 
3
3
  import android.view.View
4
4
  import android.view.ViewGroup
5
+ import com.facebook.react.uimanager.ReactOverflowView
6
+ import dev.apexnative.navigation.utils.borderRadius
5
7
  import dev.apexnative.navigation.views.animations.ValueInterpolation
6
8
  import kotlin.math.abs
7
9
  import kotlin.math.cos
10
+ import kotlin.math.max
8
11
  import kotlin.math.min
9
- import kotlin.math.roundToInt
10
12
  import kotlin.math.sin
11
13
 
12
14
  /**
@@ -41,6 +43,23 @@ internal class ViewPlacement(
41
43
  )
42
44
  }
43
45
 
46
+ /**
47
+ * The point [x], [y] of the parent in the view's coordinates: [toParent] undone. None when a scale of 0 flattens
48
+ * the view.
49
+ */
50
+ fun toChild(x: Float, y: Float): FloatArray? {
51
+ if (scaleX == 0f || scaleY == 0f) return null
52
+ val dx = x - (pivotX + translationX + left - parentScrollX)
53
+ val dy = y - (pivotY + translationY + top - parentScrollY)
54
+ val radians = Math.toRadians(rotation.toDouble())
55
+ val cos = cos(radians).toFloat()
56
+ val sin = sin(radians).toFloat()
57
+ return floatArrayOf(
58
+ (dx * cos + dy * sin) / scaleX + pivotX,
59
+ (-dx * sin + dy * cos) / scaleY + pivotY,
60
+ )
61
+ }
62
+
44
63
  /** Whether the view shows mirrored: one of its scales is negative. */
45
64
  val mirrors: Boolean get() = scaleX * scaleY < 0
46
65
 
@@ -49,12 +68,12 @@ internal class ViewPlacement(
49
68
  private const val FLIP_TOLERANCE = 0.5f
50
69
  private const val HALF_TURN = 180f
51
70
 
52
- /** [view] as it lies now; [frameOf] the view whose frame and parent it takes (a stand-in where it was). */
53
- fun of(view: View, frameOf: View = view): ViewPlacement {
54
- val parent = frameOf.parent as? View
71
+ /** [view] as it lies now. */
72
+ fun of(view: View): ViewPlacement {
73
+ val parent = view.parent as? View
55
74
  return withTilts(
56
- left = frameOf.left.toFloat(),
57
- top = frameOf.top.toFloat(),
75
+ left = view.left.toFloat(),
76
+ top = view.top.toFloat(),
58
77
  translationX = view.translationX,
59
78
  translationY = view.translationY,
60
79
  scaleX = view.scaleX,
@@ -113,12 +132,13 @@ internal class ViewPlacement(
113
132
  * Where the points of a view are drawn in a transition's layer: through its own placement and each parent's ([chain],
114
133
  * the view first, up to the top view of the window but not it), then moved by [offsetX], [offsetY] (where the top view
115
134
  * is from the layer's corner). The angle and the scale are those of the whole chain: a turned or scaled parent turns
116
- * or scales what is in it.
135
+ * or scales what is in it. [clips] are the parents that cut what the view draws, the innermost first.
117
136
  */
118
137
  internal class LayerPlace(
119
138
  private val chain: List<ViewPlacement>,
120
139
  private val offsetX: Float,
121
140
  private val offsetY: Float,
141
+ private val clips: List<Clip> = emptyList(),
122
142
  ) {
123
143
  /** The angle the view is drawn at, degrees, clockwise: a mirrored parent turns what is in it the other way. */
124
144
  val rotation: Float = chain.fold(0f) { inner, placement ->
@@ -153,28 +173,158 @@ internal class LayerPlace(
153
173
  )
154
174
  }
155
175
 
176
+ /**
177
+ * The part of a view of [width] × [height] drawn here that its [clips] leave visible, as iOS works it out: the
178
+ * view's bounds cut by the bounds of each clip, brought into the view's coordinates (the box around them where a
179
+ * turn tilts them), with the corners of the last clip that cut something. Uncut, it is the whole view with its own
180
+ * [cornerRadius] (in its pixels).
181
+ */
182
+ fun visible(width: Int, height: Int, cornerRadius: Float): VisiblePart {
183
+ val w = width.toFloat()
184
+ val h = height.toFloat()
185
+ val visible = floatArrayOf(0f, 0f, w, h)
186
+ var radius = 0f
187
+ for (clip in clips) {
188
+ val cut = intersection(visible, boundsInView(clip))
189
+ if (!cut.contentEquals(visible)) radius = clip.radius
190
+ cut.copyInto(visible)
191
+ }
192
+ val clipped = abs(visible[0]) > SAME_RECT || abs(visible[1]) > SAME_RECT ||
193
+ abs(visible[2] - w) > SAME_RECT || abs(visible[3] - h) > SAME_RECT
194
+ val shown = if (clipped) visible else floatArrayOf(0f, 0f, w, h)
195
+ val scaleX = abs(scaleX)
196
+ val scaleY = abs(scaleY)
197
+ return VisiblePart(
198
+ left = (shown[0] - w / 2) * scaleX,
199
+ top = (shown[1] - h / 2) * scaleY,
200
+ right = (shown[2] - w / 2) * scaleX,
201
+ bottom = (shown[3] - h / 2) * scaleY,
202
+ radius = (if (clipped) radius else cornerRadius) * min(scaleX, scaleY),
203
+ clipped = clipped,
204
+ )
205
+ }
206
+
207
+ // The bounds of [clip] in the view's coordinates: the box around its corners brought down the chain. Empty when a
208
+ // placement on the way flattens the view.
209
+ private fun boundsInView(clip: Clip): FloatArray {
210
+ val box = floatArrayOf(Float.MAX_VALUE, Float.MAX_VALUE, -Float.MAX_VALUE, -Float.MAX_VALUE)
211
+ for (corner in 0 until CORNERS) {
212
+ var point: FloatArray? = floatArrayOf(
213
+ if (corner % 2 == 0) 0f else clip.width,
214
+ if (corner < 2) 0f else clip.height,
215
+ )
216
+ for (level in clip.level - 1 downTo 0) point = point?.let { chain[level].toChild(it[0], it[1]) }
217
+ point ?: return FloatArray(RECT)
218
+ box[0] = min(box[0], point[0])
219
+ box[1] = min(box[1], point[1])
220
+ box[2] = max(box[2], point[0])
221
+ box[3] = max(box[3], point[1])
222
+ }
223
+ return box
224
+ }
225
+
156
226
  companion object {
227
+ private const val CORNERS = 4
228
+ private const val RECT = 4
229
+
230
+ // How close two rects count as the same, in pixels: a visible part that is the whole view needs no mask.
231
+ private const val SAME_RECT = 0.5f
232
+ private const val VISIBLE = "visible"
233
+
157
234
  /**
158
- * Where [view] is drawn in [layer]. With [standIn], [view] counts as lying where that one lies (its frame and
159
- * parents) with its own properties: an element that moved into the layer, at the place it goes back to.
235
+ * Where [view] is drawn in [layer]. With [clipRoot] (the view of its screen), the parents up to it that cut
236
+ * what they draw are its clips: as on iOS, the parents above the screen don't count.
160
237
  */
161
- fun of(view: View, layer: View, standIn: View? = null): LayerPlace {
162
- val placed = standIn ?: view
163
- val chain = arrayListOf(ViewPlacement.of(view, placed))
164
- var parent = placed.parent as? View
238
+ fun of(view: View, layer: View, clipRoot: View? = null): LayerPlace {
239
+ val chain = arrayListOf(ViewPlacement.of(view))
240
+ val clips = ArrayList<Clip>()
241
+ var clipping = clipRoot != null
242
+ var parent = view.parent as? View
165
243
  while (parent != null) {
244
+ if (clipping) {
245
+ clipOf(parent, chain.size)?.let(clips::add)
246
+ clipping = parent !== clipRoot
247
+ }
166
248
  val above = parent.parent as? View ?: break
167
249
  chain += ViewPlacement.of(parent)
168
250
  parent = above
169
251
  }
170
252
  // The window's top view (no parent view) lies where it is on the screen, with no properties of its own.
171
- val base = parent ?: placed
253
+ val base = parent ?: view
172
254
  val screen = IntArray(2)
173
255
  base.getLocationOnScreen(screen)
174
256
  val origin = IntArray(2)
175
257
  layer.getLocationOnScreen(origin)
176
- return LayerPlace(chain, (screen[0] - origin[0]).toFloat(), (screen[1] - origin[1]).toFloat())
258
+ return LayerPlace(chain, (screen[0] - origin[0]).toFloat(), (screen[1] - origin[1]).toFloat(), clips)
177
259
  }
260
+
261
+ /** The rect [a] cut by [b] (left, top, right, bottom); an empty one at 0, 0 when they don't meet, as on iOS. */
262
+ internal fun intersection(a: FloatArray, b: FloatArray): FloatArray {
263
+ val left = max(a[0], b[0])
264
+ val top = max(a[1], b[1])
265
+ val right = min(a[2], b[2])
266
+ val bottom = min(a[3], b[3])
267
+ return if (left < right && top < bottom) floatArrayOf(left, top, right, bottom) else FloatArray(RECT)
268
+ }
269
+
270
+ // The clip of [parent], at [level] of the chain, when it cuts what it draws to its bounds: a React Native view
271
+ // whose `overflow` isn't visible (a scroll view's is `scroll` unless set) with its corners, and any view whose
272
+ // own parent keeps its children in (`clipChildren`: Android cuts each child to its bounds, square), as the
273
+ // screen's own views do. React Native's views let their children out, so their `overflow` alone counts.
274
+ private fun clipOf(parent: View, level: Int): Clip? {
275
+ val overflows = parent is ReactOverflowView && parent.overflow.let { it != null && it != VISIBLE }
276
+ val keptIn = (parent.parent as? ViewGroup)?.clipChildren == true
277
+ if (!overflows && !keptIn) return null
278
+ val radius = if (overflows) parent.borderRadius else 0f
279
+ return Clip(level, parent.width.toFloat(), parent.height.toFloat(), radius)
280
+ }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * A parent that cuts what a view draws to its bounds: its [level] in the view's chain (1 the view's parent: its
286
+ * coordinates come down to the view's through the placements before it), its size and its corner radius. Pixels.
287
+ */
288
+ internal class Clip(val level: Int, val width: Float, val height: Float, val radius: Float = 0f)
289
+
290
+ /**
291
+ * The part of a shared element's copy that shows at one end of its flight: left, top, right, bottom from the copy's
292
+ * center at the size it is drawn at, and its corner radius there. [clipped]: a parent cuts the view; else it is the
293
+ * whole copy with its own corners.
294
+ */
295
+ internal class VisiblePart(
296
+ val left: Float,
297
+ val top: Float,
298
+ val right: Float,
299
+ val bottom: Float,
300
+ val radius: Float,
301
+ val clipped: Boolean,
302
+ )
303
+
304
+ /**
305
+ * The mask of a copy cut by its views' parents, as on iOS: a rounded rect that goes from the part of the copy that
306
+ * shows at one end to the part that shows at the other, evenly.
307
+ */
308
+ internal class FlightMask private constructor(private val from: VisiblePart, private val to: VisiblePart) {
309
+ /**
310
+ * Fills [out] with the mask at [fraction] of the way (the curve's value), [time] the part of the time: left, top,
311
+ * right, bottom from the copy's center, then the corner radius.
312
+ */
313
+ fun at(fraction: Float, time: Float, out: FloatArray) {
314
+ out[0] = ValueInterpolation.number(from.left, to.left, fraction, time)
315
+ out[1] = ValueInterpolation.number(from.top, to.top, fraction, time)
316
+ out[2] = ValueInterpolation.number(from.right, to.right, fraction, time)
317
+ out[3] = ValueInterpolation.number(from.bottom, to.bottom, fraction, time)
318
+ out[4] = ValueInterpolation.number(from.radius, to.radius, fraction, time)
319
+ }
320
+
321
+ companion object {
322
+ /** The values [at] fills. */
323
+ const val SIZE = 5
324
+
325
+ /** The mask between [from] and [to]; none when neither is cut, and the copy shows whole. */
326
+ fun of(from: VisiblePart, to: VisiblePart): FlightMask? =
327
+ if (from.clipped || to.clipped) FlightMask(from, to) else null
178
328
  }
179
329
  }
180
330
 
@@ -191,48 +341,14 @@ internal class FlightEnd(
191
341
  val mirrorX: Float = 1f,
192
342
  val mirrorY: Float = 1f,
193
343
  val scale: Float = 1f,
194
- ) {
195
- /** Whether [other] is drawn at the same place, size, angle and mirror: within half a pixel and 0.1°. */
196
- fun sameAs(other: FlightEnd): Boolean =
197
- abs(centerX - other.centerX) < SAME_PLACE && abs(centerY - other.centerY) < SAME_PLACE &&
198
- abs(width - other.width) < SAME_PLACE && abs(height - other.height) < SAME_PLACE &&
199
- abs(rotation - other.rotation) < SAME_ANGLE && mirrorX == other.mirrorX && mirrorY == other.mirrorY
200
-
201
- companion object {
202
- private const val SAME_PLACE = 0.5f
203
- private const val SAME_ANGLE = 0.1f
204
-
205
- /**
206
- * Whether `to` shows elsewhere in its screen than in the layer with its own properties alone ([standIn] is
207
- * where it was): a parent of its scales or turns it. A flight that landed then keeps it where it lands until
208
- * the transition gives it back, or it would jump to its place in the layer meanwhile.
209
- */
210
- fun holdsAtEnd(to: View, standIn: View?, layer: View): Boolean {
211
- if (standIn == null) return false
212
- val inScreen = LayerPlace.of(to, layer, standIn).end(to.width, to.height)
213
- return !inScreen.sameAs(LayerPlace.of(to, layer).end(to.width, to.height))
214
- }
215
- }
216
- }
344
+ )
217
345
 
218
346
  /**
219
- * Where a shared element flies in its transition's layer: from where `from` is drawn to where `to` is drawn once it
220
- * is back in its screen, both through their own and their parents' translation, scale and rotation. The center, the
221
- * size and the mirror go evenly, the angle along the shorter arc (as on iOS). `to` lies in the layer at its margins
222
- * ([toLeft], [toTop]) meanwhile.
347
+ * Where a shared element flies in its transition's layer: from where `from` is drawn to where `to` is drawn in its
348
+ * screen, both through their own and their parents' translation, scale and rotation. The center, the size and the
349
+ * mirror go evenly, the angle along the shorter arc (as on iOS).
223
350
  */
224
- internal class FlightGeometry(
225
- val from: FlightEnd,
226
- val to: FlightEnd,
227
- val toLeft: Int,
228
- val toTop: Int,
229
- /** Whether `to` keeps the flight's last frame at the end ([FlightEnd.holdsAtEnd]). */
230
- val holdsAtEnd: Boolean = false,
231
- ) {
232
- /** The width and height `to` is laid out at meanwhile: the larger of the two sizes. */
233
- val largest: Pair<Int, Int>
234
- get() = maxOf(from.width, to.width).roundToInt() to maxOf(from.height, to.height).roundToInt()
235
-
351
+ internal class FlightGeometry(val from: FlightEnd, val to: FlightEnd) {
236
352
  /** The flight at [fraction] of the way (the curve's value), [time] the part of the time. */
237
353
  fun at(fraction: Float, time: Float): FlightEnd = FlightEnd(
238
354
  centerX = ValueInterpolation.number(from.centerX, to.centerX, fraction, time),
@@ -244,103 +360,4 @@ internal class FlightGeometry(
244
360
  mirrorY = ValueInterpolation.number(from.mirrorY, to.mirrorY, fraction, time),
245
361
  scale = ValueInterpolation.number(from.scale, to.scale, fraction, time),
246
362
  )
247
-
248
- /**
249
- * The translation and pivot that draw a view laid out at its margins with [width] × [height] (or showing that part
250
- * of a larger layout from its corner) at [frame]'s center and angle, unscaled: x, y of the translation, then of
251
- * the pivot.
252
- */
253
- fun moveOf(frame: FlightEnd, width: Int, height: Int): FloatArray = floatArrayOf(
254
- frame.centerX - (toLeft + width / 2f),
255
- frame.centerY - (toTop + height / 2f),
256
- width / 2f,
257
- height / 2f,
258
- )
259
-
260
- companion object {
261
- /**
262
- * The flight of [from] to [to], which is in [layer] already in place of [toStandIn] (the stub where it was);
263
- * none when [to] has no place there.
264
- */
265
- fun of(from: View, to: View, toStandIn: View?, layer: View): FlightGeometry? {
266
- val place = to.layoutParams as? ViewGroup.MarginLayoutParams ?: return null
267
- val fromEnd = LayerPlace.of(from, layer).end(from.width, from.height)
268
- val toEnd = LayerPlace.of(to, layer, toStandIn).end(to.width, to.height)
269
- val holds = FlightEnd.holdsAtEnd(to, toStandIn, layer)
270
- return FlightGeometry(fromEnd, toEnd, place.leftMargin, place.topMargin, holds)
271
- }
272
- }
273
- }
274
-
275
- /**
276
- * The properties a moving `to` is drawn with in the layer when its flight starts, put back when it ends: the
277
- * flight draws it unscaled (but mirrored where an end is) and untilted at the flight's angle about the middle of what
278
- * shows.
279
- */
280
- internal class MovedProperties(private val view: View) {
281
- private val translationX = view.translationX
282
- private val translationY = view.translationY
283
- private val scaleX = view.scaleX
284
- private val scaleY = view.scaleY
285
- private val rotation = view.rotation
286
- private val rotationX = view.rotationX
287
- private val rotationY = view.rotationY
288
- private val pivotX = view.pivotX
289
- private val pivotY = view.pivotY
290
- private val ownWidth = view.width
291
- private val ownHeight = view.height
292
-
293
- /** [view] at [frame], laid out at [width] × [height] from its margins (or showing that part from its corner). */
294
- fun show(geometry: FlightGeometry, frame: FlightEnd, width: Int, height: Int) {
295
- val move = geometry.moveOf(frame, width, height)
296
- view.pivotX = move[2]
297
- view.pivotY = move[3]
298
- view.translationX = move[0]
299
- view.translationY = move[1]
300
- view.scaleX = frame.mirrorX
301
- view.scaleY = frame.mirrorY
302
- view.rotation = frame.rotation
303
- view.rotationX = 0f
304
- view.rotationY = 0f
305
- }
306
-
307
- /**
308
- * [view] at the end of a flight that [landed]: kept at the flight's last frame, laid out by [layOut] at the size it
309
- * shows, when the geometry holds it there; else as it was at the start, laid out by [layOut] at its own size.
310
- */
311
- fun end(geometry: FlightGeometry, landed: Boolean, layOut: (width: Int, height: Int) -> Unit) {
312
- if (geometry.holdsAtEnd && landed) {
313
- val width = geometry.to.width.roundToInt()
314
- val height = geometry.to.height.roundToInt()
315
- layOut(width, height)
316
- show(geometry, geometry.to, width, height)
317
- } else {
318
- restore()
319
- layOut(ownWidth, ownHeight)
320
- }
321
- }
322
-
323
- /** [view] as it was at the start. */
324
- fun restore() {
325
- view.translationX = translationX
326
- view.translationY = translationY
327
- view.scaleX = scaleX
328
- view.scaleY = scaleY
329
- view.rotation = rotation
330
- view.rotationX = rotationX
331
- view.rotationY = rotationY
332
- view.pivotX = pivotX
333
- view.pivotY = pivotY
334
- }
335
- }
336
-
337
- /** An outline of the size and corners a flying view shows meanwhile: its own bounds are larger. */
338
- internal class FlightOutline : android.view.ViewOutlineProvider() {
339
- var width = 0
340
- var height = 0
341
- var radius = 0f
342
-
343
- override fun getOutline(view: View, outline: android.graphics.Outline) {
344
- outline.setRoundRect(0, 0, width, height, radius)
345
- }
346
363
  }
@@ -0,0 +1,199 @@
1
+ package dev.apexnative.navigation.views.element.animators
2
+
3
+ import android.graphics.Canvas
4
+ import android.graphics.Color
5
+ import android.graphics.Paint
6
+ import android.graphics.PorterDuff
7
+ import android.graphics.PorterDuffXfermode
8
+ import android.graphics.RectF
9
+ import android.graphics.drawable.Drawable
10
+ import android.view.View
11
+ import android.widget.ImageView
12
+ import com.facebook.react.uimanager.BackgroundStyleApplicator
13
+ import com.facebook.react.uimanager.style.LogicalEdge
14
+ import com.facebook.react.views.image.ReactImageView
15
+ import com.facebook.react.views.view.ReactViewGroup
16
+ import dev.apexnative.navigation.utils.borderRadius
17
+ import kotlin.math.abs
18
+ import kotlin.math.roundToInt
19
+
20
+ /**
21
+ * A shared image flies as its image, fitted all the way: the copy goes from where `from` is drawn to where `to` is
22
+ * drawn in its screen ([FlightGeometry]), clipped to its corners, and the image of `to` in it goes from the part and
23
+ * place `from` shows its image at (its fit, `resizeMode`) to those of `to`, keeping its proportions where both fits
24
+ * do. The image is `to`'s own drawable, drawn on the layer's canvas and only read: an animated image plays on. The
25
+ * corners are the image's own radius, else that of a parent that clips it (`overflow: hidden`) and that it fills.
26
+ * Where a parent cuts the view, only the part that shows flies, as with a view ([ViewMorph]). A pair whose views draw
27
+ * more than their images (a background, a border) or whose images differ in proportions flies as pictures of the views
28
+ * ([ViewMorph]).
29
+ */
30
+ internal class ImageMorph private constructor(
31
+ private val start: ImageEnd,
32
+ private val end: ImageEnd,
33
+ private val image: Drawable,
34
+ // Where the image's drawable draws the image, in the coordinates it draws in.
35
+ private val drawn: RectF,
36
+ ) : FlightPicture() {
37
+ private val flight = FlightGeometry(start.end, end.end)
38
+ private val cut = FlightMask.of(start.visible, end.visible)?.let(::MaskCut)
39
+ private var at = start.end
40
+ private var progress = 0f
41
+ private val rect = RectF()
42
+ private val imageRect = FloatArray(RECT)
43
+ private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG)
44
+ // What is drawn over the mask keeps only the mask's pixels: the corners stay smooth.
45
+ private val contentPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) }
46
+
47
+ override val live: Boolean get() = true
48
+
49
+ override fun update(time: Float, progress: Float) {
50
+ this.progress = progress
51
+ at = flight.at(progress, time)
52
+ for (i in 0 until RECT) imageRect[i] = lerp(start.image[i], end.image[i])
53
+ cut?.update(time, progress)
54
+ }
55
+
56
+ override fun draw(canvas: Canvas) {
57
+ rect.set(-at.width / 2, -at.height / 2, at.width / 2, at.height / 2)
58
+ val imageWidth = imageRect[2] - imageRect[0]
59
+ val imageHeight = imageRect[3] - imageRect[1]
60
+ if (rect.isEmpty || imageWidth <= 0 || imageHeight <= 0 || drawn.isEmpty) return
61
+ val alpha = (lerp(start.alpha, end.alpha) * 255).roundToInt().coerceIn(0, 255)
62
+ if (alpha == 0) return
63
+ val radius = lerp(start.radius, end.radius)
64
+ // The copy turns about its center, mirrored where a view is.
65
+ canvas.translate(at.centerX, at.centerY)
66
+ canvas.rotate(at.rotation)
67
+ canvas.scale(at.mirrorX, at.mirrorY)
68
+ canvas.saveLayer(rect, null)
69
+ canvas.drawRoundRect(rect, radius, radius, maskPaint)
70
+ cut?.draw(canvas, rect)
71
+ contentPaint.alpha = alpha
72
+ canvas.saveLayer(rect, contentPaint)
73
+ canvas.translate(imageRect[0], imageRect[1])
74
+ canvas.scale(imageWidth / drawn.width(), imageHeight / drawn.height())
75
+ canvas.translate(-drawn.left, -drawn.top)
76
+ image.draw(canvas)
77
+ }
78
+
79
+ private fun lerp(from: Float, to: Float) = from + (to - from) * progress
80
+
81
+ // One end of the flight: where the view is drawn, where its image shows in it (left, top, right, bottom from the
82
+ // view's center, at the size it is drawn at), its corner radius there, its opacity and the part of it that shows.
83
+ private class ImageEnd(
84
+ val end: FlightEnd,
85
+ val image: FloatArray,
86
+ val radius: Float,
87
+ val alpha: Float,
88
+ val visible: VisiblePart,
89
+ )
90
+
91
+ companion object {
92
+ private const val RECT = 4
93
+
94
+ // How far from its parent's size an image still fills it, in pixels.
95
+ private const val FILLS = 1
96
+ private const val HIDDEN = "hidden"
97
+
98
+ // How far the proportions of two images may differ for one to stand for the other, in parts of one.
99
+ private const val SAME_ASPECT = 0.02f
100
+
101
+ /**
102
+ * The morph of [from] into [to] in [layer], each cut by its parents up to the view of its screen ([fromRoot],
103
+ * [toRoot]); none when an image isn't there to fit, the images differ in proportions, or a view draws more
104
+ * than its image.
105
+ */
106
+ fun of(from: ImageView, to: ImageView, fromRoot: View?, toRoot: View?, layer: View): ImageMorph? {
107
+ if (!onlyImage(from) || !onlyImage(to)) return null
108
+ val image = to.drawable ?: return null
109
+ val drawn = drawnRect(to) ?: return null
110
+ val fromDrawn = drawnRect(from) ?: return null
111
+ if (!sameAspect(fromDrawn.width(), fromDrawn.height(), drawn.width(), drawn.height())) return null
112
+ val start = endOf(from, layer, fromRoot) ?: return null
113
+ val end = endOf(to, layer, toRoot) ?: return null
114
+ return ImageMorph(start, end, image, drawn)
115
+ }
116
+
117
+ /** Whether an image of [width] × [height] has the proportions of one of [otherWidth] × [otherHeight]. */
118
+ internal fun sameAspect(width: Float, height: Float, otherWidth: Float, otherHeight: Float): Boolean {
119
+ if (width <= 0f || height <= 0f || otherWidth <= 0f || otherHeight <= 0f) return false
120
+ val aspect = width / height
121
+ val other = otherWidth / otherHeight
122
+ return abs(aspect - other) <= SAME_ASPECT * other
123
+ }
124
+
125
+ // The view draws nothing but its image: no background color, no border (React Native draws them as the
126
+ // view's background, around the image).
127
+ private fun onlyImage(view: View): Boolean = try {
128
+ val background = BackgroundStyleApplicator.getBackgroundColor(view)
129
+ (background == null || Color.alpha(background) == 0) && LogicalEdge.entries.none {
130
+ (BackgroundStyleApplicator.getBorderWidth(view, it) ?: 0f) > 0f
131
+ }
132
+ } catch (e: RuntimeException) {
133
+ false
134
+ }
135
+
136
+ private fun endOf(view: ImageView, layer: View, root: View?): ImageEnd? {
137
+ if (view.width <= 0 || view.height <= 0) return null
138
+ val shown = shownRect(view) ?: return null
139
+ val place = LayerPlace.of(view, layer, root)
140
+ val end = place.end(view.width, view.height)
141
+ val image = imageFromCenter(
142
+ floatArrayOf(shown.left, shown.top, shown.right, shown.bottom),
143
+ view.width, view.height, end.width, end.height,
144
+ )
145
+ val radius = cornerRadius(view)
146
+ return ImageEnd(
147
+ end, image, radius * end.scale, view.alpha, place.visible(view.width, view.height, radius),
148
+ )
149
+ }
150
+
151
+ // Where the drawable draws the image when the view draws it: Fresco's image where its fit puts it in the
152
+ // drawable, another image at its drawable's bounds (the view's matrix comes on top).
153
+ private fun drawnRect(view: ImageView): RectF? {
154
+ val rect = if (view is ReactImageView) {
155
+ if (!view.hasHierarchy()) return null
156
+ RectF().also { view.hierarchy.getActualImageBounds(it) }
157
+ } else {
158
+ RectF(view.drawable?.bounds ?: return null)
159
+ }
160
+ return rect.takeUnless { it.isEmpty }
161
+ }
162
+
163
+ // Where the image shows in the view, in its pixels.
164
+ private fun shownRect(view: ImageView): RectF? {
165
+ val rect = drawnRect(view) ?: return null
166
+ if (view !is ReactImageView) view.imageMatrix.mapRect(rect)
167
+ rect.offset(view.paddingLeft.toFloat(), view.paddingTop.toFloat())
168
+ return rect
169
+ }
170
+
171
+ private fun cornerRadius(view: ImageView): Float {
172
+ view.borderRadius.takeIf { it > 0f }?.let { return it }
173
+ val parent = view.parent as? ReactViewGroup ?: return 0f
174
+ val fills = abs(parent.width - view.width) <= FILLS && abs(parent.height - view.height) <= FILLS
175
+ return if (fills && parent.overflow == HIDDEN) parent.borderRadius else 0f
176
+ }
177
+
178
+ /**
179
+ * The rect [shown] (left, top, right, bottom) of a view of [width] × [height], from the view's center, at the
180
+ * size [shownWidth] × [shownHeight] the view is drawn at.
181
+ */
182
+ internal fun imageFromCenter(
183
+ shown: FloatArray,
184
+ width: Int,
185
+ height: Int,
186
+ shownWidth: Float,
187
+ shownHeight: Float,
188
+ ): FloatArray {
189
+ val scaleX = shownWidth / width
190
+ val scaleY = shownHeight / height
191
+ return floatArrayOf(
192
+ (shown[0] - width / 2f) * scaleX,
193
+ (shown[1] - height / 2f) * scaleY,
194
+ (shown[2] - width / 2f) * scaleX,
195
+ (shown[3] - height / 2f) * scaleY,
196
+ )
197
+ }
198
+ }
199
+ }