react-native-image-editor-skia 0.1.0

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 (299) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/lib/commonjs/ImageEditor.js +141 -0
  4. package/lib/commonjs/ImageEditor.js.map +1 -0
  5. package/lib/commonjs/annotations/AnnotationView.js +42 -0
  6. package/lib/commonjs/annotations/AnnotationView.js.map +1 -0
  7. package/lib/commonjs/annotations/ArrowAnnotation.js +29 -0
  8. package/lib/commonjs/annotations/ArrowAnnotation.js.map +1 -0
  9. package/lib/commonjs/annotations/CircleAnnotation.js +31 -0
  10. package/lib/commonjs/annotations/CircleAnnotation.js.map +1 -0
  11. package/lib/commonjs/annotations/FreehandAnnotation.js +29 -0
  12. package/lib/commonjs/annotations/FreehandAnnotation.js.map +1 -0
  13. package/lib/commonjs/annotations/MarkerAnnotation.js +27 -0
  14. package/lib/commonjs/annotations/MarkerAnnotation.js.map +1 -0
  15. package/lib/commonjs/annotations/RotatedGroup.js +34 -0
  16. package/lib/commonjs/annotations/RotatedGroup.js.map +1 -0
  17. package/lib/commonjs/annotations/TextAnnotation.js +40 -0
  18. package/lib/commonjs/annotations/TextAnnotation.js.map +1 -0
  19. package/lib/commonjs/annotations/geometry.js +73 -0
  20. package/lib/commonjs/annotations/geometry.js.map +1 -0
  21. package/lib/commonjs/annotations/geometryPure.js +104 -0
  22. package/lib/commonjs/annotations/geometryPure.js.map +1 -0
  23. package/lib/commonjs/canvas/AnnotationLayer.js +58 -0
  24. package/lib/commonjs/canvas/AnnotationLayer.js.map +1 -0
  25. package/lib/commonjs/canvas/BaseImageLayer.js +27 -0
  26. package/lib/commonjs/canvas/BaseImageLayer.js.map +1 -0
  27. package/lib/commonjs/canvas/CropOverlay.js +135 -0
  28. package/lib/commonjs/canvas/CropOverlay.js.map +1 -0
  29. package/lib/commonjs/canvas/EditorCanvas.js +91 -0
  30. package/lib/commonjs/canvas/EditorCanvas.js.map +1 -0
  31. package/lib/commonjs/canvas/InFlightLayer.js +152 -0
  32. package/lib/commonjs/canvas/InFlightLayer.js.map +1 -0
  33. package/lib/commonjs/canvas/SelectionOverlay.js +90 -0
  34. package/lib/commonjs/canvas/SelectionOverlay.js.map +1 -0
  35. package/lib/commonjs/constants.js +56 -0
  36. package/lib/commonjs/constants.js.map +1 -0
  37. package/lib/commonjs/context/EditorContext.js +132 -0
  38. package/lib/commonjs/context/EditorContext.js.map +1 -0
  39. package/lib/commonjs/export/drawScene.js +97 -0
  40. package/lib/commonjs/export/drawScene.js.map +1 -0
  41. package/lib/commonjs/export/exportImage.js +92 -0
  42. package/lib/commonjs/export/exportImage.js.map +1 -0
  43. package/lib/commonjs/gestures/applyTransform.js +79 -0
  44. package/lib/commonjs/gestures/applyTransform.js.map +1 -0
  45. package/lib/commonjs/gestures/createAnnotation.js +73 -0
  46. package/lib/commonjs/gestures/createAnnotation.js.map +1 -0
  47. package/lib/commonjs/gestures/handles.js +53 -0
  48. package/lib/commonjs/gestures/handles.js.map +1 -0
  49. package/lib/commonjs/gestures/hitTest.js +72 -0
  50. package/lib/commonjs/gestures/hitTest.js.map +1 -0
  51. package/lib/commonjs/gestures/useCropGesture.js +149 -0
  52. package/lib/commonjs/gestures/useCropGesture.js.map +1 -0
  53. package/lib/commonjs/gestures/useEditorGestures.js +289 -0
  54. package/lib/commonjs/gestures/useEditorGestures.js.map +1 -0
  55. package/lib/commonjs/image/disposeRegistry.js +63 -0
  56. package/lib/commonjs/image/disposeRegistry.js.map +1 -0
  57. package/lib/commonjs/image/useLoadedImage.js +121 -0
  58. package/lib/commonjs/image/useLoadedImage.js.map +1 -0
  59. package/lib/commonjs/index.js +52 -0
  60. package/lib/commonjs/index.js.map +1 -0
  61. package/lib/commonjs/package.json +1 -0
  62. package/lib/commonjs/state/history.js +85 -0
  63. package/lib/commonjs/state/history.js.map +1 -0
  64. package/lib/commonjs/state/selectors.js +19 -0
  65. package/lib/commonjs/state/selectors.js.map +1 -0
  66. package/lib/commonjs/state/useEditorReducer.js +83 -0
  67. package/lib/commonjs/state/useEditorReducer.js.map +1 -0
  68. package/lib/commonjs/toolbar/ColorPicker.js +84 -0
  69. package/lib/commonjs/toolbar/ColorPicker.js.map +1 -0
  70. package/lib/commonjs/toolbar/CropControls.js +65 -0
  71. package/lib/commonjs/toolbar/CropControls.js.map +1 -0
  72. package/lib/commonjs/toolbar/RotationSlider.js +73 -0
  73. package/lib/commonjs/toolbar/RotationSlider.js.map +1 -0
  74. package/lib/commonjs/toolbar/TextInputOverlay.js +108 -0
  75. package/lib/commonjs/toolbar/TextInputOverlay.js.map +1 -0
  76. package/lib/commonjs/toolbar/ToolButton.js +56 -0
  77. package/lib/commonjs/toolbar/ToolButton.js.map +1 -0
  78. package/lib/commonjs/toolbar/Toolbar.js +137 -0
  79. package/lib/commonjs/toolbar/Toolbar.js.map +1 -0
  80. package/lib/commonjs/types.js +47 -0
  81. package/lib/commonjs/types.js.map +1 -0
  82. package/lib/commonjs/utils/color.js +37 -0
  83. package/lib/commonjs/utils/color.js.map +1 -0
  84. package/lib/commonjs/utils/id.js +14 -0
  85. package/lib/commonjs/utils/id.js.map +1 -0
  86. package/lib/commonjs/utils/math.js +277 -0
  87. package/lib/commonjs/utils/math.js.map +1 -0
  88. package/lib/module/ImageEditor.js +138 -0
  89. package/lib/module/ImageEditor.js.map +1 -0
  90. package/lib/module/annotations/AnnotationView.js +39 -0
  91. package/lib/module/annotations/AnnotationView.js.map +1 -0
  92. package/lib/module/annotations/ArrowAnnotation.js +26 -0
  93. package/lib/module/annotations/ArrowAnnotation.js.map +1 -0
  94. package/lib/module/annotations/CircleAnnotation.js +27 -0
  95. package/lib/module/annotations/CircleAnnotation.js.map +1 -0
  96. package/lib/module/annotations/FreehandAnnotation.js +25 -0
  97. package/lib/module/annotations/FreehandAnnotation.js.map +1 -0
  98. package/lib/module/annotations/MarkerAnnotation.js +23 -0
  99. package/lib/module/annotations/MarkerAnnotation.js.map +1 -0
  100. package/lib/module/annotations/RotatedGroup.js +29 -0
  101. package/lib/module/annotations/RotatedGroup.js.map +1 -0
  102. package/lib/module/annotations/TextAnnotation.js +37 -0
  103. package/lib/module/annotations/TextAnnotation.js.map +1 -0
  104. package/lib/module/annotations/geometry.js +56 -0
  105. package/lib/module/annotations/geometry.js.map +1 -0
  106. package/lib/module/annotations/geometryPure.js +100 -0
  107. package/lib/module/annotations/geometryPure.js.map +1 -0
  108. package/lib/module/canvas/AnnotationLayer.js +55 -0
  109. package/lib/module/canvas/AnnotationLayer.js.map +1 -0
  110. package/lib/module/canvas/BaseImageLayer.js +23 -0
  111. package/lib/module/canvas/BaseImageLayer.js.map +1 -0
  112. package/lib/module/canvas/CropOverlay.js +131 -0
  113. package/lib/module/canvas/CropOverlay.js.map +1 -0
  114. package/lib/module/canvas/EditorCanvas.js +88 -0
  115. package/lib/module/canvas/EditorCanvas.js.map +1 -0
  116. package/lib/module/canvas/InFlightLayer.js +149 -0
  117. package/lib/module/canvas/InFlightLayer.js.map +1 -0
  118. package/lib/module/canvas/SelectionOverlay.js +85 -0
  119. package/lib/module/canvas/SelectionOverlay.js.map +1 -0
  120. package/lib/module/constants.js +52 -0
  121. package/lib/module/constants.js.map +1 -0
  122. package/lib/module/context/EditorContext.js +126 -0
  123. package/lib/module/context/EditorContext.js.map +1 -0
  124. package/lib/module/export/drawScene.js +93 -0
  125. package/lib/module/export/drawScene.js.map +1 -0
  126. package/lib/module/export/exportImage.js +88 -0
  127. package/lib/module/export/exportImage.js.map +1 -0
  128. package/lib/module/gestures/applyTransform.js +75 -0
  129. package/lib/module/gestures/applyTransform.js.map +1 -0
  130. package/lib/module/gestures/createAnnotation.js +65 -0
  131. package/lib/module/gestures/createAnnotation.js.map +1 -0
  132. package/lib/module/gestures/handles.js +49 -0
  133. package/lib/module/gestures/handles.js.map +1 -0
  134. package/lib/module/gestures/hitTest.js +69 -0
  135. package/lib/module/gestures/hitTest.js.map +1 -0
  136. package/lib/module/gestures/useCropGesture.js +145 -0
  137. package/lib/module/gestures/useCropGesture.js.map +1 -0
  138. package/lib/module/gestures/useEditorGestures.js +285 -0
  139. package/lib/module/gestures/useEditorGestures.js.map +1 -0
  140. package/lib/module/image/disposeRegistry.js +57 -0
  141. package/lib/module/image/disposeRegistry.js.map +1 -0
  142. package/lib/module/image/useLoadedImage.js +117 -0
  143. package/lib/module/image/useLoadedImage.js.map +1 -0
  144. package/lib/module/index.js +8 -0
  145. package/lib/module/index.js.map +1 -0
  146. package/lib/module/package.json +1 -0
  147. package/lib/module/state/history.js +76 -0
  148. package/lib/module/state/history.js.map +1 -0
  149. package/lib/module/state/selectors.js +14 -0
  150. package/lib/module/state/selectors.js.map +1 -0
  151. package/lib/module/state/useEditorReducer.js +79 -0
  152. package/lib/module/state/useEditorReducer.js.map +1 -0
  153. package/lib/module/toolbar/ColorPicker.js +80 -0
  154. package/lib/module/toolbar/ColorPicker.js.map +1 -0
  155. package/lib/module/toolbar/CropControls.js +62 -0
  156. package/lib/module/toolbar/CropControls.js.map +1 -0
  157. package/lib/module/toolbar/RotationSlider.js +69 -0
  158. package/lib/module/toolbar/RotationSlider.js.map +1 -0
  159. package/lib/module/toolbar/TextInputOverlay.js +105 -0
  160. package/lib/module/toolbar/TextInputOverlay.js.map +1 -0
  161. package/lib/module/toolbar/ToolButton.js +52 -0
  162. package/lib/module/toolbar/ToolButton.js.map +1 -0
  163. package/lib/module/toolbar/Toolbar.js +133 -0
  164. package/lib/module/toolbar/Toolbar.js.map +1 -0
  165. package/lib/module/types.js +43 -0
  166. package/lib/module/types.js.map +1 -0
  167. package/lib/module/utils/color.js +33 -0
  168. package/lib/module/utils/color.js.map +1 -0
  169. package/lib/module/utils/id.js +10 -0
  170. package/lib/module/utils/id.js.map +1 -0
  171. package/lib/module/utils/math.js +258 -0
  172. package/lib/module/utils/math.js.map +1 -0
  173. package/lib/typescript/src/ImageEditor.d.ts +9 -0
  174. package/lib/typescript/src/ImageEditor.d.ts.map +1 -0
  175. package/lib/typescript/src/annotations/AnnotationView.d.ts +6 -0
  176. package/lib/typescript/src/annotations/AnnotationView.d.ts.map +1 -0
  177. package/lib/typescript/src/annotations/ArrowAnnotation.d.ts +5 -0
  178. package/lib/typescript/src/annotations/ArrowAnnotation.d.ts.map +1 -0
  179. package/lib/typescript/src/annotations/CircleAnnotation.d.ts +5 -0
  180. package/lib/typescript/src/annotations/CircleAnnotation.d.ts.map +1 -0
  181. package/lib/typescript/src/annotations/FreehandAnnotation.d.ts +5 -0
  182. package/lib/typescript/src/annotations/FreehandAnnotation.d.ts.map +1 -0
  183. package/lib/typescript/src/annotations/MarkerAnnotation.d.ts +5 -0
  184. package/lib/typescript/src/annotations/MarkerAnnotation.d.ts.map +1 -0
  185. package/lib/typescript/src/annotations/RotatedGroup.d.ts +13 -0
  186. package/lib/typescript/src/annotations/RotatedGroup.d.ts.map +1 -0
  187. package/lib/typescript/src/annotations/TextAnnotation.d.ts +10 -0
  188. package/lib/typescript/src/annotations/TextAnnotation.d.ts.map +1 -0
  189. package/lib/typescript/src/annotations/geometry.d.ts +11 -0
  190. package/lib/typescript/src/annotations/geometry.d.ts.map +1 -0
  191. package/lib/typescript/src/annotations/geometryPure.d.ts +14 -0
  192. package/lib/typescript/src/annotations/geometryPure.d.ts.map +1 -0
  193. package/lib/typescript/src/canvas/AnnotationLayer.d.ts +11 -0
  194. package/lib/typescript/src/canvas/AnnotationLayer.d.ts.map +1 -0
  195. package/lib/typescript/src/canvas/BaseImageLayer.d.ts +12 -0
  196. package/lib/typescript/src/canvas/BaseImageLayer.d.ts.map +1 -0
  197. package/lib/typescript/src/canvas/CropOverlay.d.ts +10 -0
  198. package/lib/typescript/src/canvas/CropOverlay.d.ts.map +1 -0
  199. package/lib/typescript/src/canvas/EditorCanvas.d.ts +18 -0
  200. package/lib/typescript/src/canvas/EditorCanvas.d.ts.map +1 -0
  201. package/lib/typescript/src/canvas/InFlightLayer.d.ts +12 -0
  202. package/lib/typescript/src/canvas/InFlightLayer.d.ts.map +1 -0
  203. package/lib/typescript/src/canvas/SelectionOverlay.d.ts +11 -0
  204. package/lib/typescript/src/canvas/SelectionOverlay.d.ts.map +1 -0
  205. package/lib/typescript/src/constants.d.ts +25 -0
  206. package/lib/typescript/src/constants.d.ts.map +1 -0
  207. package/lib/typescript/src/context/EditorContext.d.ts +66 -0
  208. package/lib/typescript/src/context/EditorContext.d.ts.map +1 -0
  209. package/lib/typescript/src/export/drawScene.d.ts +10 -0
  210. package/lib/typescript/src/export/drawScene.d.ts.map +1 -0
  211. package/lib/typescript/src/export/exportImage.d.ts +23 -0
  212. package/lib/typescript/src/export/exportImage.d.ts.map +1 -0
  213. package/lib/typescript/src/gestures/applyTransform.d.ts +17 -0
  214. package/lib/typescript/src/gestures/applyTransform.d.ts.map +1 -0
  215. package/lib/typescript/src/gestures/createAnnotation.d.ts +11 -0
  216. package/lib/typescript/src/gestures/createAnnotation.d.ts.map +1 -0
  217. package/lib/typescript/src/gestures/handles.d.ts +17 -0
  218. package/lib/typescript/src/gestures/handles.d.ts.map +1 -0
  219. package/lib/typescript/src/gestures/hitTest.d.ts +9 -0
  220. package/lib/typescript/src/gestures/hitTest.d.ts.map +1 -0
  221. package/lib/typescript/src/gestures/useCropGesture.d.ts +7 -0
  222. package/lib/typescript/src/gestures/useCropGesture.d.ts.map +1 -0
  223. package/lib/typescript/src/gestures/useEditorGestures.d.ts +8 -0
  224. package/lib/typescript/src/gestures/useEditorGestures.d.ts.map +1 -0
  225. package/lib/typescript/src/image/disposeRegistry.d.ts +25 -0
  226. package/lib/typescript/src/image/disposeRegistry.d.ts.map +1 -0
  227. package/lib/typescript/src/image/useLoadedImage.d.ts +23 -0
  228. package/lib/typescript/src/image/useLoadedImage.d.ts.map +1 -0
  229. package/lib/typescript/src/index.d.ts +6 -0
  230. package/lib/typescript/src/index.d.ts.map +1 -0
  231. package/lib/typescript/src/state/history.d.ts +23 -0
  232. package/lib/typescript/src/state/history.d.ts.map +1 -0
  233. package/lib/typescript/src/state/selectors.d.ts +5 -0
  234. package/lib/typescript/src/state/selectors.d.ts.map +1 -0
  235. package/lib/typescript/src/state/useEditorReducer.d.ts +32 -0
  236. package/lib/typescript/src/state/useEditorReducer.d.ts.map +1 -0
  237. package/lib/typescript/src/toolbar/ColorPicker.d.ts +7 -0
  238. package/lib/typescript/src/toolbar/ColorPicker.d.ts.map +1 -0
  239. package/lib/typescript/src/toolbar/CropControls.d.ts +3 -0
  240. package/lib/typescript/src/toolbar/CropControls.d.ts.map +1 -0
  241. package/lib/typescript/src/toolbar/RotationSlider.d.ts +8 -0
  242. package/lib/typescript/src/toolbar/RotationSlider.d.ts.map +1 -0
  243. package/lib/typescript/src/toolbar/TextInputOverlay.d.ts +9 -0
  244. package/lib/typescript/src/toolbar/TextInputOverlay.d.ts.map +1 -0
  245. package/lib/typescript/src/toolbar/ToolButton.d.ts +7 -0
  246. package/lib/typescript/src/toolbar/ToolButton.d.ts.map +1 -0
  247. package/lib/typescript/src/toolbar/Toolbar.d.ts +4 -0
  248. package/lib/typescript/src/toolbar/Toolbar.d.ts.map +1 -0
  249. package/lib/typescript/src/types.d.ts +170 -0
  250. package/lib/typescript/src/types.d.ts.map +1 -0
  251. package/lib/typescript/src/utils/color.d.ts +8 -0
  252. package/lib/typescript/src/utils/color.d.ts.map +1 -0
  253. package/lib/typescript/src/utils/id.d.ts +3 -0
  254. package/lib/typescript/src/utils/id.d.ts.map +1 -0
  255. package/lib/typescript/src/utils/math.d.ts +68 -0
  256. package/lib/typescript/src/utils/math.d.ts.map +1 -0
  257. package/package.json +90 -0
  258. package/src/ImageEditor.tsx +133 -0
  259. package/src/annotations/AnnotationView.tsx +24 -0
  260. package/src/annotations/ArrowAnnotation.tsx +26 -0
  261. package/src/annotations/CircleAnnotation.tsx +22 -0
  262. package/src/annotations/FreehandAnnotation.tsx +22 -0
  263. package/src/annotations/MarkerAnnotation.tsx +20 -0
  264. package/src/annotations/RotatedGroup.tsx +28 -0
  265. package/src/annotations/TextAnnotation.tsx +42 -0
  266. package/src/annotations/geometry.ts +62 -0
  267. package/src/annotations/geometryPure.ts +73 -0
  268. package/src/canvas/AnnotationLayer.tsx +43 -0
  269. package/src/canvas/BaseImageLayer.tsx +28 -0
  270. package/src/canvas/CropOverlay.tsx +92 -0
  271. package/src/canvas/EditorCanvas.tsx +70 -0
  272. package/src/canvas/InFlightLayer.tsx +140 -0
  273. package/src/canvas/SelectionOverlay.tsx +92 -0
  274. package/src/constants.ts +46 -0
  275. package/src/context/EditorContext.tsx +229 -0
  276. package/src/export/drawScene.ts +120 -0
  277. package/src/export/exportImage.ts +111 -0
  278. package/src/gestures/applyTransform.ts +76 -0
  279. package/src/gestures/createAnnotation.ts +92 -0
  280. package/src/gestures/handles.ts +50 -0
  281. package/src/gestures/hitTest.ts +79 -0
  282. package/src/gestures/useCropGesture.ts +123 -0
  283. package/src/gestures/useEditorGestures.ts +308 -0
  284. package/src/image/disposeRegistry.ts +59 -0
  285. package/src/image/useLoadedImage.ts +131 -0
  286. package/src/index.ts +32 -0
  287. package/src/state/history.ts +71 -0
  288. package/src/state/selectors.ts +16 -0
  289. package/src/state/useEditorReducer.ts +93 -0
  290. package/src/toolbar/ColorPicker.tsx +72 -0
  291. package/src/toolbar/CropControls.tsx +46 -0
  292. package/src/toolbar/RotationSlider.tsx +56 -0
  293. package/src/toolbar/TextInputOverlay.tsx +104 -0
  294. package/src/toolbar/ToolButton.tsx +46 -0
  295. package/src/toolbar/Toolbar.tsx +110 -0
  296. package/src/types.ts +203 -0
  297. package/src/utils/color.ts +34 -0
  298. package/src/utils/id.ts +7 -0
  299. package/src/utils/math.ts +222 -0
@@ -0,0 +1,79 @@
1
+ import type { Annotation, Vec2 } from '../types';
2
+ import { annotationBounds, annotationCenter } from '../annotations/geometryPure';
3
+ import { distanceToSegment, rotatePoint } from '../utils/math';
4
+
5
+ // NOTE: `hitOne` is declared BEFORE `hitTest`. The worklets Babel plugin rewrites
6
+ // `'worklet'` function declarations into `const`s (no hoisting), so a worklet that
7
+ // calls another must be defined after its callee.
8
+ function hitOne(a: Annotation, p: Vec2, slop: number): boolean {
9
+ 'worklet';
10
+ switch (a.type) {
11
+ case 'circle': {
12
+ const d = Math.hypot(p.x - a.center.x, p.y - a.center.y);
13
+ if (a.fill !== undefined) {
14
+ return d <= a.radius + slop;
15
+ }
16
+ // Outline: hit near the ring.
17
+ return Math.abs(d - a.radius) <= slop + a.strokeWidth / 2;
18
+ }
19
+ case 'arrow':
20
+ return distanceToSegment(p, a.start, a.end) <= slop + a.strokeWidth / 2;
21
+ case 'marker': {
22
+ const r = a.rect;
23
+ return (
24
+ p.x >= r.x - slop &&
25
+ p.x <= r.x + r.width + slop &&
26
+ p.y >= r.y - slop &&
27
+ p.y <= r.y + r.height + slop
28
+ );
29
+ }
30
+ case 'freehand': {
31
+ const pts = a.points;
32
+ const tol = slop + a.strokeWidth / 2;
33
+ for (let i = 1; i < pts.length; i++) {
34
+ if (distanceToSegment(p, pts[i - 1]!, pts[i]!) <= tol) {
35
+ return true;
36
+ }
37
+ }
38
+ return pts.length === 1
39
+ ? Math.hypot(p.x - pts[0]!.x, p.y - pts[0]!.y) <= tol
40
+ : false;
41
+ }
42
+ case 'text': {
43
+ const b = annotationBounds(a);
44
+ return (
45
+ p.x >= b.x - slop &&
46
+ p.x <= b.x + b.width + slop &&
47
+ p.y >= b.y - slop &&
48
+ p.y <= b.y + b.height + slop
49
+ );
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Return the id of the top-most annotation hit by `point` (image space), or null.
56
+ * Worklet-safe so selection resolves on the UI thread during a tap.
57
+ *
58
+ * `slop` is an extra hit margin, in image pixels (screen slop / current scale).
59
+ */
60
+ export function hitTest(
61
+ annotations: Annotation[],
62
+ point: Vec2,
63
+ slop: number
64
+ ): string | null {
65
+ 'worklet';
66
+ // Iterate from top (highest z) to bottom.
67
+ const ordered = [...annotations].sort((a, b) => b.z - a.z);
68
+ for (let i = 0; i < ordered.length; i++) {
69
+ const a = ordered[i]!;
70
+ // Transform the touch into the annotation's local (unrotated) frame.
71
+ const local = a.rotation
72
+ ? rotatePoint(point, annotationCenter(a), -a.rotation)
73
+ : point;
74
+ if (hitOne(a, local, slop)) {
75
+ return a.id;
76
+ }
77
+ }
78
+ return null;
79
+ }
@@ -0,0 +1,123 @@
1
+ import { useMemo } from 'react';
2
+ import { Gesture } from 'react-native-gesture-handler';
3
+ import { useSharedValue } from 'react-native-reanimated';
4
+
5
+ import type { Rect, Vec2 } from '../types';
6
+ import { useEditor } from '../context/EditorContext';
7
+ import { HANDLE_SIZE } from '../constants';
8
+ import { applyToPoint, distance, invert } from '../utils/math';
9
+
10
+ const MIN_CROP = 24; // minimum crop size, in image pixels
11
+
12
+ type CropMode = 'none' | 'move' | 'resize';
13
+
14
+ /**
15
+ * Pan gesture that edits `cropRectSV` (image space) while the crop tool is
16
+ * active. Grab a corner to resize, or the interior to move. All values stay
17
+ * clamped inside the image bounds.
18
+ */
19
+ export function useCropGesture() {
20
+ const { tool, matrix, cropRectSV, imageSize } = useEditor();
21
+ const invMatrix = useMemo(() => invert(matrix), [matrix]);
22
+
23
+ const cropMode = useSharedValue<CropMode>('none');
24
+ const corner = useSharedValue(-1);
25
+ const startRect = useSharedValue<Rect>({ x: 0, y: 0, width: 0, height: 0 });
26
+ const startImg = useSharedValue<Vec2>({ x: 0, y: 0 });
27
+
28
+ return useMemo(() => {
29
+ return Gesture.Pan()
30
+ .enabled(tool === 'crop')
31
+ .maxPointers(1)
32
+ .onBegin((e) => {
33
+ 'worklet';
34
+ const screen = { x: e.x, y: e.y };
35
+ const img = applyToPoint(invMatrix, screen);
36
+ const r = cropRectSV.value;
37
+ startRect.value = r;
38
+ startImg.value = img;
39
+
40
+ const corners: Vec2[] = [
41
+ { x: r.x, y: r.y },
42
+ { x: r.x + r.width, y: r.y },
43
+ { x: r.x + r.width, y: r.y + r.height },
44
+ { x: r.x, y: r.y + r.height },
45
+ ];
46
+ for (let i = 0; i < corners.length; i++) {
47
+ const cs = applyToPoint(matrix, corners[i]!);
48
+ if (distance(screen, cs) <= HANDLE_SIZE * 1.5) {
49
+ cropMode.value = 'resize';
50
+ corner.value = i;
51
+ return;
52
+ }
53
+ }
54
+ if (
55
+ img.x >= r.x &&
56
+ img.x <= r.x + r.width &&
57
+ img.y >= r.y &&
58
+ img.y <= r.y + r.height
59
+ ) {
60
+ cropMode.value = 'move';
61
+ } else {
62
+ cropMode.value = 'none';
63
+ }
64
+ })
65
+ .onChange((e) => {
66
+ 'worklet';
67
+ const img = applyToPoint(invMatrix, { x: e.x, y: e.y });
68
+ const dx = img.x - startImg.value.x;
69
+ const dy = img.y - startImg.value.y;
70
+ const s = startRect.value;
71
+
72
+ if (cropMode.value === 'move') {
73
+ let nx = s.x + dx;
74
+ let ny = s.y + dy;
75
+ nx = Math.max(0, Math.min(nx, imageSize.width - s.width));
76
+ ny = Math.max(0, Math.min(ny, imageSize.height - s.height));
77
+ cropRectSV.value = { x: nx, y: ny, width: s.width, height: s.height };
78
+ } else if (cropMode.value === 'resize') {
79
+ // Fixed corner = the one opposite the grabbed corner.
80
+ const left = s.x;
81
+ const top = s.y;
82
+ const right = s.x + s.width;
83
+ const bottom = s.y + s.height;
84
+ let x0 = left;
85
+ let y0 = top;
86
+ let x1 = right;
87
+ let y1 = bottom;
88
+ switch (corner.value) {
89
+ case 0: // TL moves
90
+ x0 = Math.min(right - MIN_CROP, Math.max(0, left + dx));
91
+ y0 = Math.min(bottom - MIN_CROP, Math.max(0, top + dy));
92
+ break;
93
+ case 1: // TR moves
94
+ x1 = Math.max(left + MIN_CROP, Math.min(imageSize.width, right + dx));
95
+ y0 = Math.min(bottom - MIN_CROP, Math.max(0, top + dy));
96
+ break;
97
+ case 2: // BR moves
98
+ x1 = Math.max(left + MIN_CROP, Math.min(imageSize.width, right + dx));
99
+ y1 = Math.max(top + MIN_CROP, Math.min(imageSize.height, bottom + dy));
100
+ break;
101
+ case 3: // BL moves
102
+ x0 = Math.min(right - MIN_CROP, Math.max(0, left + dx));
103
+ y1 = Math.max(top + MIN_CROP, Math.min(imageSize.height, bottom + dy));
104
+ break;
105
+ default:
106
+ break;
107
+ }
108
+ cropRectSV.value = {
109
+ x: x0,
110
+ y: y0,
111
+ width: x1 - x0,
112
+ height: y1 - y0,
113
+ };
114
+ }
115
+ })
116
+ .onEnd(() => {
117
+ 'worklet';
118
+ cropMode.value = 'none';
119
+ corner.value = -1;
120
+ });
121
+ // eslint-disable-next-line react-hooks/exhaustive-deps
122
+ }, [tool, matrix, imageSize]);
123
+ }
@@ -0,0 +1,308 @@
1
+ import { useMemo } from 'react';
2
+ import { Gesture } from 'react-native-gesture-handler';
3
+ import { runOnJS, useSharedValue } from 'react-native-reanimated';
4
+
5
+ import type { Annotation, Rect, ToolType, Vec2 } from '../types';
6
+ import { useEditor } from '../context/EditorContext';
7
+ import {
8
+ FREEHAND_MIN_DISTANCE,
9
+ HANDLE_SIZE,
10
+ HIT_SLOP,
11
+ } from '../constants';
12
+ import { applyToPoint, distance, invert } from '../utils/math';
13
+ import { annotationCenter } from '../annotations/geometry';
14
+ import { hitTest } from './hitTest';
15
+ import { selectionHandles } from './handles';
16
+ import { applyTransformToAnnotation } from './applyTransform';
17
+ import type { LiveValues } from './applyTransform';
18
+ import {
19
+ makeArrow,
20
+ makeCircle,
21
+ makeFreehand,
22
+ makeMarker,
23
+ makeText,
24
+ } from './createAnnotation';
25
+
26
+ type Mode = 'none' | 'draw' | 'text' | 'move' | 'resize' | 'rotate';
27
+
28
+ function isDrawTool(tool: ToolType): boolean {
29
+ 'worklet';
30
+ return (
31
+ tool === 'circle' ||
32
+ tool === 'arrow' ||
33
+ tool === 'marker' ||
34
+ tool === 'freehand'
35
+ );
36
+ }
37
+
38
+ /**
39
+ * A single composed Pan gesture over the canvas. It branches on the active tool
40
+ * and, in select mode, on where the touch began (rotate handle → rotate, corner
41
+ * handle → resize, body → move, empty → (de)select). All geometry math runs on
42
+ * the UI thread; only final commits hop to JS via runOnJS.
43
+ */
44
+ export function useEditorGestures() {
45
+ const editor = useEditor();
46
+ const {
47
+ tool,
48
+ selectedId,
49
+ annotations,
50
+ matrix,
51
+ draw,
52
+ live,
53
+ strokeColor,
54
+ strokeWidth,
55
+ dispatch,
56
+ setSelectedId,
57
+ setEditingTextId,
58
+ } = editor;
59
+
60
+ const invMatrix = useMemo(() => invert(matrix), [matrix]);
61
+ const scaleFactor = useMemo(
62
+ () => Math.hypot(matrix.a, matrix.b) || 1,
63
+ [matrix]
64
+ );
65
+ const slopImage = HIT_SLOP / scaleFactor;
66
+
67
+ // Per-gesture UI-thread scratch state.
68
+ const mode = useSharedValue<Mode>('none');
69
+ const startImg = useSharedValue<Vec2>({ x: 0, y: 0 });
70
+ const startDist = useSharedValue(0);
71
+ const startAngle = useSharedValue(0);
72
+ const activeId = useSharedValue<string | null>(null);
73
+
74
+ // ---- JS-thread commit callbacks --------------------------------------
75
+ const selectId = (id: string | null) => setSelectedId(id);
76
+
77
+ const commitCircle = (center: Vec2, radius: number) =>
78
+ dispatch({
79
+ type: 'ADD_ANNOTATION',
80
+ annotation: makeCircle(center, radius, { strokeColor, strokeWidth }),
81
+ });
82
+
83
+ const commitArrow = (start: Vec2, end: Vec2) =>
84
+ dispatch({
85
+ type: 'ADD_ANNOTATION',
86
+ annotation: makeArrow(start, end, { strokeColor, strokeWidth }),
87
+ });
88
+
89
+ const commitMarker = (rect: Rect) =>
90
+ dispatch({
91
+ type: 'ADD_ANNOTATION',
92
+ annotation: makeMarker(rect, strokeColor),
93
+ });
94
+
95
+ const commitFreehand = (points: Vec2[]) =>
96
+ dispatch({
97
+ type: 'ADD_ANNOTATION',
98
+ annotation: makeFreehand(points, { strokeColor, strokeWidth }),
99
+ });
100
+
101
+ const placeText = (origin: Vec2) => {
102
+ const annotation = makeText(origin, editor.textColor);
103
+ dispatch({ type: 'ADD_ANNOTATION', annotation });
104
+ setSelectedId(annotation.id);
105
+ setEditingTextId(annotation.id);
106
+ };
107
+
108
+ const commitTransform = (id: string | null, vals: LiveValues) => {
109
+ if (!id) {
110
+ return;
111
+ }
112
+ // Skip no-op transforms (a plain tap) so they don't pollute history.
113
+ const identity =
114
+ vals.tx === 0 && vals.ty === 0 && vals.rotate === 0 && vals.scale === 1;
115
+ if (identity) {
116
+ return;
117
+ }
118
+ const target = annotations.find((a) => a.id === id);
119
+ if (!target) {
120
+ return;
121
+ }
122
+ const updated = applyTransformToAnnotation(target, vals);
123
+ dispatch({ type: 'UPDATE_ANNOTATION', id, changes: updated });
124
+ };
125
+
126
+ const resetLive = () => {
127
+ live.active.value = false;
128
+ live.tx.value = 0;
129
+ live.ty.value = 0;
130
+ live.rotate.value = 0;
131
+ live.scale.value = 1;
132
+ };
133
+
134
+ // ---- The gesture ------------------------------------------------------
135
+ const pan = useMemo(() => {
136
+ return Gesture.Pan()
137
+ .maxPointers(1)
138
+ .onBegin((e) => {
139
+ 'worklet';
140
+ if (tool === 'crop') {
141
+ mode.value = 'none';
142
+ return;
143
+ }
144
+ const screen = { x: e.x, y: e.y };
145
+ const img = applyToPoint(invMatrix, screen);
146
+ startImg.value = img;
147
+ activeId.value = selectedId;
148
+
149
+ if (isDrawTool(tool)) {
150
+ mode.value = 'draw';
151
+ draw.active.value = true;
152
+ draw.start.value = img;
153
+ draw.current.value = img;
154
+ if (tool === 'freehand') {
155
+ draw.points.value = [img];
156
+ }
157
+ return;
158
+ }
159
+
160
+ if (tool === 'text') {
161
+ mode.value = 'text';
162
+ return;
163
+ }
164
+
165
+ // select tool: figure out what was grabbed.
166
+ const sel: Annotation | undefined = selectedId
167
+ ? annotations.find((a) => a.id === selectedId)
168
+ : undefined;
169
+ if (sel) {
170
+ const h = selectionHandles(sel, matrix);
171
+ const center = annotationCenter(sel);
172
+ if (distance(screen, h.rotate) <= HANDLE_SIZE * 1.5) {
173
+ mode.value = 'rotate';
174
+ live.origin.value = center;
175
+ live.active.value = true;
176
+ startAngle.value = Math.atan2(img.y - center.y, img.x - center.x);
177
+ return;
178
+ }
179
+ for (let i = 0; i < h.corners.length; i++) {
180
+ if (distance(screen, h.corners[i]!) <= HANDLE_SIZE * 1.5) {
181
+ mode.value = 'resize';
182
+ live.origin.value = center;
183
+ live.active.value = true;
184
+ startDist.value = distance(center, img);
185
+ return;
186
+ }
187
+ }
188
+ // Body of the selected annotation → move it.
189
+ if (hitTest([sel], img, slopImage) === sel.id) {
190
+ mode.value = 'move';
191
+ live.origin.value = center;
192
+ live.active.value = true;
193
+ return;
194
+ }
195
+ }
196
+
197
+ // Otherwise (de)select whatever is under the touch.
198
+ const hitId = hitTest(annotations, img, slopImage);
199
+ runOnJS(selectId)(hitId);
200
+ activeId.value = hitId;
201
+ if (hitId) {
202
+ const target = annotations.find((a) => a.id === hitId)!;
203
+ mode.value = 'move';
204
+ live.origin.value = annotationCenter(target);
205
+ live.active.value = true;
206
+ } else {
207
+ mode.value = 'none';
208
+ }
209
+ })
210
+ .onChange((e) => {
211
+ 'worklet';
212
+ const screen = { x: e.x, y: e.y };
213
+ const img = applyToPoint(invMatrix, screen);
214
+ switch (mode.value) {
215
+ case 'draw':
216
+ if (tool === 'freehand') {
217
+ const pts = draw.points.value;
218
+ const last = pts[pts.length - 1];
219
+ if (!last || distance(last, img) >= FREEHAND_MIN_DISTANCE) {
220
+ draw.points.value = [...pts, img];
221
+ }
222
+ } else {
223
+ draw.current.value = img;
224
+ }
225
+ break;
226
+ case 'move':
227
+ live.tx.value = img.x - startImg.value.x;
228
+ live.ty.value = img.y - startImg.value.y;
229
+ break;
230
+ case 'resize':
231
+ live.scale.value =
232
+ startDist.value > 0 ? distance(live.origin.value, img) / startDist.value : 1;
233
+ break;
234
+ case 'rotate': {
235
+ const ang = Math.atan2(
236
+ img.y - live.origin.value.y,
237
+ img.x - live.origin.value.x
238
+ );
239
+ live.rotate.value = ang - startAngle.value;
240
+ break;
241
+ }
242
+ default:
243
+ break;
244
+ }
245
+ })
246
+ .onEnd(() => {
247
+ 'worklet';
248
+ if (mode.value === 'draw') {
249
+ draw.active.value = false;
250
+ const s = draw.start.value;
251
+ const c = draw.current.value;
252
+ if (tool === 'circle') {
253
+ const r = distance(s, c);
254
+ if (r > 2) runOnJS(commitCircle)(s, r);
255
+ } else if (tool === 'arrow') {
256
+ if (distance(s, c) > 2) runOnJS(commitArrow)(s, c);
257
+ } else if (tool === 'marker') {
258
+ const rect: Rect = {
259
+ x: Math.min(s.x, c.x),
260
+ y: Math.min(s.y, c.y),
261
+ width: Math.abs(c.x - s.x),
262
+ height: Math.abs(c.y - s.y),
263
+ };
264
+ if (rect.width > 2 && rect.height > 2) runOnJS(commitMarker)(rect);
265
+ } else if (tool === 'freehand') {
266
+ const pts = draw.points.value;
267
+ if (pts.length > 1) runOnJS(commitFreehand)(pts);
268
+ draw.points.value = [];
269
+ }
270
+ mode.value = 'none';
271
+ return;
272
+ }
273
+
274
+ if (mode.value === 'text') {
275
+ runOnJS(placeText)(startImg.value);
276
+ mode.value = 'none';
277
+ return;
278
+ }
279
+
280
+ if (
281
+ mode.value === 'move' ||
282
+ mode.value === 'resize' ||
283
+ mode.value === 'rotate'
284
+ ) {
285
+ const vals: LiveValues = {
286
+ tx: live.tx.value,
287
+ ty: live.ty.value,
288
+ rotate: live.rotate.value,
289
+ scale: live.scale.value,
290
+ origin: live.origin.value,
291
+ };
292
+ runOnJS(commitTransform)(activeId.value, vals);
293
+ runOnJS(resetLive)();
294
+ mode.value = 'none';
295
+ }
296
+ })
297
+ .onFinalize(() => {
298
+ 'worklet';
299
+ // Safety: ensure draw preview is cleared if the gesture is cancelled.
300
+ if (draw.active.value && mode.value !== 'draw') {
301
+ draw.active.value = false;
302
+ }
303
+ });
304
+ // eslint-disable-next-line react-hooks/exhaustive-deps
305
+ }, [tool, selectedId, annotations, matrix, strokeColor, strokeWidth]);
306
+
307
+ return pan;
308
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Tracks Skia native objects (SkImage / SkData / SkSurface / SkParagraph …) so
3
+ * they can be released deterministically. Skia objects are JSI HostObjects; the
4
+ * Hermes GC under-counts their native footprint and may never collect them, so
5
+ * we must call `.dispose()` explicitly. This registry is a safety net: register
6
+ * anything created imperatively, and `flush()` on unmount to guarantee cleanup.
7
+ */
8
+
9
+ export interface Disposable {
10
+ dispose: () => void;
11
+ }
12
+
13
+ export class DisposeRegistry {
14
+ private items = new Set<Disposable>();
15
+
16
+ /** Track an object and return it for convenient chaining. */
17
+ add<T extends Disposable>(item: T): T {
18
+ this.items.add(item);
19
+ return item;
20
+ }
21
+
22
+ /** Stop tracking without disposing (e.g. ownership handed elsewhere). */
23
+ forget(item: Disposable): void {
24
+ this.items.delete(item);
25
+ }
26
+
27
+ /** Dispose a single tracked object now. */
28
+ release(item: Disposable | null | undefined): void {
29
+ if (!item) {
30
+ return;
31
+ }
32
+ this.items.delete(item);
33
+ safeDispose(item);
34
+ }
35
+
36
+ /** Dispose everything still tracked. Call on unmount. */
37
+ flush(): void {
38
+ for (const item of this.items) {
39
+ safeDispose(item);
40
+ }
41
+ this.items.clear();
42
+ }
43
+
44
+ get size(): number {
45
+ return this.items.size;
46
+ }
47
+ }
48
+
49
+ /** Dispose without throwing if the object was already released. */
50
+ export function safeDispose(item: Disposable | null | undefined): void {
51
+ if (!item) {
52
+ return;
53
+ }
54
+ try {
55
+ item.dispose();
56
+ } catch {
57
+ // Already disposed or not a real disposable — ignore.
58
+ }
59
+ }
@@ -0,0 +1,131 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { Skia } from '@shopify/react-native-skia';
3
+ import type { SkImage } from '@shopify/react-native-skia';
4
+
5
+ import type { ImageSource } from '../types';
6
+ import { safeDispose } from './disposeRegistry';
7
+
8
+ export interface LoadedImage {
9
+ image: SkImage | null;
10
+ width: number;
11
+ height: number;
12
+ loading: boolean;
13
+ error: Error | null;
14
+ }
15
+
16
+ function stripDataUri(base64: string): string {
17
+ const comma = base64.indexOf(',');
18
+ if (base64.startsWith('data:') && comma !== -1) {
19
+ return base64.slice(comma + 1);
20
+ }
21
+ return base64;
22
+ }
23
+
24
+ /**
25
+ * Decode an {@link ImageSource} into an `SkImage`, managing native memory.
26
+ *
27
+ * Memory rules enforced here:
28
+ * - The encoded `SkData` is disposed immediately after `MakeImageFromEncoded`;
29
+ * the decoded `SkImage` owns its pixels, so retaining the encoded bytes would
30
+ * roughly double memory usage.
31
+ * - When `source` changes (or the component unmounts) the previous `SkImage` is
32
+ * disposed before the next one is created.
33
+ * - The input base64 string is consumed inside the effect and never copied into
34
+ * state, so a large payload is not retained by this hook.
35
+ */
36
+ export function useLoadedImage(source: ImageSource): LoadedImage {
37
+ const [state, setState] = useState<LoadedImage>({
38
+ image: null,
39
+ width: 0,
40
+ height: 0,
41
+ loading: true,
42
+ error: null,
43
+ });
44
+
45
+ // Serialize the source so the effect re-runs only on a real change, without
46
+ // holding the (possibly huge) base64 string in a memoized ref.
47
+ const sourceKey =
48
+ 'base64' in source ? `b64:${source.base64.length}:${source.base64.slice(-64)}` : `uri:${source.uri}`;
49
+
50
+ const currentImage = useRef<SkImage | null>(null);
51
+
52
+ useEffect(() => {
53
+ let cancelled = false;
54
+
55
+ const commit = (image: SkImage) => {
56
+ if (cancelled) {
57
+ // A newer load superseded us — drop this result.
58
+ safeDispose(image);
59
+ return;
60
+ }
61
+ // Dispose the previous image before swapping in the new one.
62
+ if (currentImage.current && currentImage.current !== image) {
63
+ safeDispose(currentImage.current);
64
+ }
65
+ currentImage.current = image;
66
+ setState({
67
+ image,
68
+ width: image.width(),
69
+ height: image.height(),
70
+ loading: false,
71
+ error: null,
72
+ });
73
+ };
74
+
75
+ const fail = (error: Error) => {
76
+ if (cancelled) {
77
+ return;
78
+ }
79
+ setState((prev) => ({ ...prev, loading: false, error }));
80
+ };
81
+
82
+ setState((prev) => ({ ...prev, loading: true, error: null }));
83
+
84
+ try {
85
+ if ('base64' in source) {
86
+ const data = Skia.Data.fromBase64(stripDataUri(source.base64));
87
+ const image = Skia.Image.MakeImageFromEncoded(data);
88
+ safeDispose(data); // decoded pixels are owned by `image` now
89
+ if (!image) {
90
+ throw new Error('Failed to decode image from base64.');
91
+ }
92
+ commit(image);
93
+ } else {
94
+ // fromURI is async (may fetch a remote/local file).
95
+ Skia.Data.fromURI(source.uri)
96
+ .then((data) => {
97
+ if (cancelled) {
98
+ safeDispose(data);
99
+ return;
100
+ }
101
+ const image = Skia.Image.MakeImageFromEncoded(data);
102
+ safeDispose(data);
103
+ if (!image) {
104
+ throw new Error(`Failed to decode image from URI: ${source.uri}`);
105
+ }
106
+ commit(image);
107
+ })
108
+ .catch((e: unknown) =>
109
+ fail(e instanceof Error ? e : new Error(String(e)))
110
+ );
111
+ }
112
+ } catch (e) {
113
+ fail(e instanceof Error ? e : new Error(String(e)));
114
+ }
115
+
116
+ return () => {
117
+ cancelled = true;
118
+ };
119
+ // eslint-disable-next-line react-hooks/exhaustive-deps
120
+ }, [sourceKey]);
121
+
122
+ // Final safety net: dispose the last image when the hook unmounts.
123
+ useEffect(() => {
124
+ return () => {
125
+ safeDispose(currentImage.current);
126
+ currentImage.current = null;
127
+ };
128
+ }, []);
129
+
130
+ return state;
131
+ }