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,42 @@
1
+ import { useMemo } from 'react';
2
+ import { Text, matchFont } from '@shopify/react-native-skia';
3
+
4
+ import type { TextAnnotation as TextAnnotationType } from '../types';
5
+ import { annotationCenter } from './geometry';
6
+ import { RotatedGroup } from './RotatedGroup';
7
+
8
+ /**
9
+ * Renders text using a system font via `matchFont` (no bundled font file
10
+ * required). Honors explicit newlines; automatic width-wrapping is intentionally
11
+ * out of scope for v1 (would require SkParagraph + a shared FontMgr).
12
+ */
13
+ export function TextAnnotationView({ a }: { a: TextAnnotationType }) {
14
+ const font = useMemo(
15
+ () =>
16
+ matchFont({
17
+ fontFamily: 'sans-serif',
18
+ fontSize: a.fontSize,
19
+ fontStyle: 'normal',
20
+ fontWeight: 'normal',
21
+ }),
22
+ [a.fontSize]
23
+ );
24
+
25
+ const lines = a.text.split('\n');
26
+ const lineHeight = a.fontSize * 1.2;
27
+
28
+ return (
29
+ <RotatedGroup center={annotationCenter(a)} rotation={a.rotation}>
30
+ {lines.map((line, i) => (
31
+ <Text
32
+ key={i}
33
+ x={a.origin.x}
34
+ y={a.origin.y + a.fontSize + i * lineHeight}
35
+ text={line}
36
+ font={font}
37
+ color={a.color}
38
+ />
39
+ ))}
40
+ </RotatedGroup>
41
+ );
42
+ }
@@ -0,0 +1,62 @@
1
+ import { Skia } from '@shopify/react-native-skia';
2
+ import type { SkPath } from '@shopify/react-native-skia';
3
+
4
+ import type { Vec2 } from '../types';
5
+
6
+ // Re-export the pure helpers so existing imports from './geometry' keep working.
7
+ // (Worklet/test-safe versions with no Skia dependency live in ./geometryPure.)
8
+ export { annotationCenter, annotationBounds } from './geometryPure';
9
+
10
+ /**
11
+ * Build a Skia path for an arrow (shaft + two arrowhead barbs).
12
+ * Not a worklet — it allocates an `SkPath` (a Skia object) on the JS thread.
13
+ */
14
+ export function buildArrowPath(
15
+ start: Vec2,
16
+ end: Vec2,
17
+ headSize: number
18
+ ): SkPath {
19
+ const path = Skia.Path.Make();
20
+ path.moveTo(start.x, start.y);
21
+ path.lineTo(end.x, end.y);
22
+
23
+ const angle = Math.atan2(end.y - start.y, end.x - start.x);
24
+ const barb = (Math.PI * 5) / 6; // 150° from the shaft direction
25
+ const b1 = {
26
+ x: end.x + headSize * Math.cos(angle + barb),
27
+ y: end.y + headSize * Math.sin(angle + barb),
28
+ };
29
+ const b2 = {
30
+ x: end.x + headSize * Math.cos(angle - barb),
31
+ y: end.y + headSize * Math.sin(angle - barb),
32
+ };
33
+ path.moveTo(end.x, end.y);
34
+ path.lineTo(b1.x, b1.y);
35
+ path.moveTo(end.x, end.y);
36
+ path.lineTo(b2.x, b2.y);
37
+ return path;
38
+ }
39
+
40
+ /** Build a smoothed (quadratic) path through freehand points. */
41
+ export function buildFreehandPath(points: Vec2[]): SkPath {
42
+ const path = Skia.Path.Make();
43
+ if (points.length === 0) {
44
+ return path;
45
+ }
46
+ path.moveTo(points[0]!.x, points[0]!.y);
47
+ if (points.length === 1) {
48
+ // Draw a dot.
49
+ path.lineTo(points[0]!.x + 0.01, points[0]!.y + 0.01);
50
+ return path;
51
+ }
52
+ for (let i = 1; i < points.length - 1; i++) {
53
+ const p = points[i]!;
54
+ const next = points[i + 1]!;
55
+ const midX = (p.x + next.x) / 2;
56
+ const midY = (p.y + next.y) / 2;
57
+ path.quadTo(p.x, p.y, midX, midY);
58
+ }
59
+ const last = points[points.length - 1]!;
60
+ path.lineTo(last.x, last.y);
61
+ return path;
62
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Pure annotation geometry (no Skia imports) so it is safe to use from worklets
3
+ * AND from plain unit tests without loading the native Skia module.
4
+ */
5
+ import type { Annotation, Rect, Vec2 } from '../types';
6
+ import { boundsOfPoints } from '../utils/math';
7
+
8
+ /** Center of an annotation in image space (used as rotation/scale origin). */
9
+ export function annotationCenter(a: Annotation): Vec2 {
10
+ 'worklet';
11
+ switch (a.type) {
12
+ case 'circle':
13
+ return { x: a.center.x, y: a.center.y };
14
+ case 'arrow':
15
+ return { x: (a.start.x + a.end.x) / 2, y: (a.start.y + a.end.y) / 2 };
16
+ case 'marker':
17
+ return { x: a.rect.x + a.rect.width / 2, y: a.rect.y + a.rect.height / 2 };
18
+ case 'freehand': {
19
+ const b = boundsOfPoints(a.points);
20
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
21
+ }
22
+ case 'text':
23
+ return {
24
+ x: a.origin.x + a.width / 2,
25
+ y: a.origin.y + (a.fontSize * 1.2) / 2,
26
+ };
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Axis-aligned bounding box of an annotation in its LOCAL (unrotated) frame.
32
+ * The selection overlay rotates this box by `annotation.rotation` about the
33
+ * center when drawing handles.
34
+ */
35
+ export function annotationBounds(a: Annotation): Rect {
36
+ 'worklet';
37
+ switch (a.type) {
38
+ case 'circle':
39
+ return {
40
+ x: a.center.x - a.radius,
41
+ y: a.center.y - a.radius,
42
+ width: a.radius * 2,
43
+ height: a.radius * 2,
44
+ };
45
+ case 'arrow': {
46
+ const pad = a.headSize + a.strokeWidth;
47
+ const minX = Math.min(a.start.x, a.end.x) - pad;
48
+ const minY = Math.min(a.start.y, a.end.y) - pad;
49
+ const maxX = Math.max(a.start.x, a.end.x) + pad;
50
+ const maxY = Math.max(a.start.y, a.end.y) + pad;
51
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
52
+ }
53
+ case 'marker':
54
+ return { ...a.rect };
55
+ case 'freehand': {
56
+ const b = boundsOfPoints(a.points);
57
+ const pad = a.strokeWidth / 2;
58
+ return {
59
+ x: b.x - pad,
60
+ y: b.y - pad,
61
+ width: b.width + pad * 2,
62
+ height: b.height + pad * 2,
63
+ };
64
+ }
65
+ case 'text':
66
+ return {
67
+ x: a.origin.x,
68
+ y: a.origin.y,
69
+ width: a.width,
70
+ height: a.fontSize * 1.2,
71
+ };
72
+ }
73
+ }
@@ -0,0 +1,43 @@
1
+ import { Group } from '@shopify/react-native-skia';
2
+ import { useDerivedValue } from 'react-native-reanimated';
3
+
4
+ import type { EditorContextValue } from '../context/EditorContext';
5
+ import { sortedByZ } from '../state/selectors';
6
+ import { AnnotationView } from '../annotations/AnnotationView';
7
+
8
+ /**
9
+ * Renders all committed annotations in paint order. The selected annotation is
10
+ * wrapped in a Group whose transform is driven by the `live` shared values, so
11
+ * move/resize/rotate previews run on the UI thread with zero React re-renders.
12
+ * When idle the live transform is identity, so this is a no-op visually.
13
+ */
14
+ export function AnnotationLayer({ editor }: { editor: EditorContextValue }) {
15
+ const { annotations, selectedId, live, editingTextId } = editor;
16
+
17
+ const liveTransform = useDerivedValue(() => [
18
+ { translateX: live.tx.value },
19
+ { translateY: live.ty.value },
20
+ { rotate: live.rotate.value },
21
+ { scale: live.scale.value },
22
+ ]);
23
+ const liveOrigin = useDerivedValue(() => live.origin.value);
24
+
25
+ return (
26
+ <>
27
+ {sortedByZ(annotations).map((a) => {
28
+ // Hide the text annotation currently being edited in the native overlay.
29
+ if (a.type === 'text' && a.id === editingTextId) {
30
+ return null;
31
+ }
32
+ if (a.id === selectedId) {
33
+ return (
34
+ <Group key={a.id} origin={liveOrigin} transform={liveTransform}>
35
+ <AnnotationView a={a} />
36
+ </Group>
37
+ );
38
+ }
39
+ return <AnnotationView key={a.id} a={a} />;
40
+ })}
41
+ </>
42
+ );
43
+ }
@@ -0,0 +1,28 @@
1
+ import { Image } from '@shopify/react-native-skia';
2
+ import type { SkImage } from '@shopify/react-native-skia';
3
+
4
+ import type { Size } from '../context/EditorContext';
5
+
6
+ /**
7
+ * Draws the base image in IMAGE space (x=0,y=0, native width/height). The parent
8
+ * scene `<Group>` maps it onto the screen, so this same layer renders identically
9
+ * off-screen at full resolution during export.
10
+ */
11
+ export function BaseImageLayer({
12
+ image,
13
+ size,
14
+ }: {
15
+ image: SkImage;
16
+ size: Size;
17
+ }) {
18
+ return (
19
+ <Image
20
+ image={image}
21
+ x={0}
22
+ y={0}
23
+ width={size.width}
24
+ height={size.height}
25
+ fit="fill"
26
+ />
27
+ );
28
+ }
@@ -0,0 +1,92 @@
1
+ import { useEffect } from 'react';
2
+ import { Circle, Group, Path, Rect, Skia } from '@shopify/react-native-skia';
3
+ import { useDerivedValue } from 'react-native-reanimated';
4
+
5
+ import type { EditorContextValue } from '../context/EditorContext';
6
+ import { HANDLE_SIZE } from '../constants';
7
+ import { applyToPoint } from '../utils/math';
8
+ import type { Vec2 } from '../types';
9
+
10
+ /**
11
+ * Crop UI drawn in screen space: a dimmed full-screen scrim plus the crop
12
+ * rectangle's border and corner handles, which follow `cropRectSV` on the UI
13
+ * thread (works even when the scene is rotated, since the border is a quad).
14
+ */
15
+ export function CropOverlay({ editor }: { editor: EditorContextValue }) {
16
+ const { tool, doc, cropRectSV, matrixSV, imageSize, layout } = editor;
17
+
18
+ // When entering the crop tool, seed the crop rect from the current scene.
19
+ useEffect(() => {
20
+ if (tool === 'crop') {
21
+ cropRectSV.value =
22
+ doc.scene.cropRect ?? {
23
+ x: 0,
24
+ y: 0,
25
+ width: imageSize.width,
26
+ height: imageSize.height,
27
+ };
28
+ }
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ }, [tool]);
31
+
32
+ const cropPath = useDerivedValue(() => {
33
+ const r = cropRectSV.value;
34
+ const m = matrixSV.value;
35
+ const p = Skia.Path.Make();
36
+ const c0 = applyToPoint(m, { x: r.x, y: r.y });
37
+ const c1 = applyToPoint(m, { x: r.x + r.width, y: r.y });
38
+ const c2 = applyToPoint(m, { x: r.x + r.width, y: r.y + r.height });
39
+ const c3 = applyToPoint(m, { x: r.x, y: r.y + r.height });
40
+ p.moveTo(c0.x, c0.y);
41
+ p.lineTo(c1.x, c1.y);
42
+ p.lineTo(c2.x, c2.y);
43
+ p.lineTo(c3.x, c3.y);
44
+ p.close();
45
+ return p;
46
+ });
47
+
48
+ const corners = useDerivedValue<Vec2[]>(() => {
49
+ const r = cropRectSV.value;
50
+ const m = matrixSV.value;
51
+ return [
52
+ applyToPoint(m, { x: r.x, y: r.y }),
53
+ applyToPoint(m, { x: r.x + r.width, y: r.y }),
54
+ applyToPoint(m, { x: r.x + r.width, y: r.y + r.height }),
55
+ applyToPoint(m, { x: r.x, y: r.y + r.height }),
56
+ ];
57
+ });
58
+
59
+ // Per-corner x/y derived values (declared unconditionally — rules of hooks).
60
+ const c0x = useDerivedValue(() => corners.value[0]!.x);
61
+ const c0y = useDerivedValue(() => corners.value[0]!.y);
62
+ const c1x = useDerivedValue(() => corners.value[1]!.x);
63
+ const c1y = useDerivedValue(() => corners.value[1]!.y);
64
+ const c2x = useDerivedValue(() => corners.value[2]!.x);
65
+ const c2y = useDerivedValue(() => corners.value[2]!.y);
66
+ const c3x = useDerivedValue(() => corners.value[3]!.x);
67
+ const c3y = useDerivedValue(() => corners.value[3]!.y);
68
+
69
+ if (tool !== 'crop') {
70
+ return null;
71
+ }
72
+
73
+ const r = HANDLE_SIZE / 2;
74
+ return (
75
+ <Group>
76
+ {/* Dimmed scrim over the whole canvas. */}
77
+ <Rect
78
+ x={0}
79
+ y={0}
80
+ width={layout.width}
81
+ height={layout.height}
82
+ color="rgba(0,0,0,0.45)"
83
+ />
84
+ {/* Crop rectangle border. */}
85
+ <Path path={cropPath} color="#FFFFFF" style="stroke" strokeWidth={2} />
86
+ <Circle cx={c0x} cy={c0y} r={r} color="#FFFFFF" />
87
+ <Circle cx={c1x} cy={c1y} r={r} color="#FFFFFF" />
88
+ <Circle cx={c2x} cy={c2y} r={r} color="#FFFFFF" />
89
+ <Circle cx={c3x} cy={c3y} r={r} color="#FFFFFF" />
90
+ </Group>
91
+ );
92
+ }
@@ -0,0 +1,70 @@
1
+ import { useMemo } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import type { LayoutChangeEvent } from 'react-native';
4
+ import { Canvas, Group } from '@shopify/react-native-skia';
5
+ import type { SkImage } from '@shopify/react-native-skia';
6
+ import { GestureDetector, Gesture } from 'react-native-gesture-handler';
7
+
8
+ import { useEditor } from '../context/EditorContext';
9
+ import { sceneTransforms2d } from '../utils/math';
10
+ import { useEditorGestures } from '../gestures/useEditorGestures';
11
+ import { useCropGesture } from '../gestures/useCropGesture';
12
+ import { BaseImageLayer } from './BaseImageLayer';
13
+ import { AnnotationLayer } from './AnnotationLayer';
14
+ import { InFlightLayer } from './InFlightLayer';
15
+ import { SelectionOverlay } from './SelectionOverlay';
16
+ import { CropOverlay } from './CropOverlay';
17
+
18
+ /**
19
+ * The Skia canvas host. Layers, under one scene `<Group>` transform:
20
+ * base image → committed annotations → in-flight draft
21
+ * plus screen-space overlays (selection handles, crop UI). A single composed
22
+ * gesture drives all editing.
23
+ *
24
+ * IMPORTANT: React Context does NOT cross the Skia `<Canvas>` boundary — Skia
25
+ * renders Canvas children with its own reconciler, so a Provider mounted outside
26
+ * the Canvas is invisible inside it. Every Canvas child therefore receives the
27
+ * whole editor value via an `editor` PROP instead of calling `useEditor()`.
28
+ * Also, `<Canvas onLayout>` is unsupported on the New Architecture, so we measure
29
+ * on a wrapping `<View>`.
30
+ */
31
+ export function EditorCanvas({ image }: { image: SkImage }) {
32
+ const editor = useEditor();
33
+ const { doc, imageSize, setLayout, layout } = editor;
34
+
35
+ const pan = useEditorGestures();
36
+ const cropPan = useCropGesture();
37
+ const gesture = useMemo(() => Gesture.Race(cropPan, pan), [cropPan, pan]);
38
+
39
+ const transform = useMemo(
40
+ () => sceneTransforms2d(doc.scene, imageSize, layout),
41
+ [doc.scene, imageSize, layout]
42
+ );
43
+
44
+ const onLayout = (e: LayoutChangeEvent) => {
45
+ const { width, height } = e.nativeEvent.layout;
46
+ if (width > 0 && height > 0) {
47
+ setLayout({ width, height });
48
+ }
49
+ };
50
+
51
+ return (
52
+ <View style={styles.fill} onLayout={onLayout}>
53
+ <GestureDetector gesture={gesture}>
54
+ <Canvas style={styles.fill}>
55
+ <Group transform={transform as never}>
56
+ <BaseImageLayer image={image} size={imageSize} />
57
+ <AnnotationLayer editor={editor} />
58
+ <InFlightLayer editor={editor} />
59
+ </Group>
60
+ <SelectionOverlay editor={editor} />
61
+ <CropOverlay editor={editor} />
62
+ </Canvas>
63
+ </GestureDetector>
64
+ </View>
65
+ );
66
+ }
67
+
68
+ const styles = StyleSheet.create({
69
+ fill: { flex: 1 },
70
+ });
@@ -0,0 +1,140 @@
1
+ import { Circle, Group, Line, Path, Rect, Skia } from '@shopify/react-native-skia';
2
+ import type { SkPath } from '@shopify/react-native-skia';
3
+ import { useDerivedValue } from 'react-native-reanimated';
4
+
5
+ import type { DrawState, EditorContextValue } from '../context/EditorContext';
6
+ import { ARROW_HEAD_RATIO, MARKER_OPACITY } from '../constants';
7
+ import { withOpacity } from '../utils/color';
8
+
9
+ /**
10
+ * Renders the shape currently being drawn, bound to the `draw` shared values so
11
+ * it updates on the UI thread. The active tool is fixed for the duration of a
12
+ * draw, so switching on it here is safe. Opacity is 0 when not drawing.
13
+ *
14
+ * Receives `editor` as a prop (context does not cross the Skia Canvas boundary).
15
+ */
16
+ export function InFlightLayer({ editor }: { editor: EditorContextValue }) {
17
+ const { tool, draw, strokeColor, strokeWidth } = editor;
18
+
19
+ const opacity = useDerivedValue(() => (draw.active.value ? 1 : 0));
20
+
21
+ return (
22
+ <Group opacity={opacity}>
23
+ {tool === 'circle' && (
24
+ <CirclePreview draw={draw} color={strokeColor} width={strokeWidth} />
25
+ )}
26
+ {tool === 'arrow' && (
27
+ <ArrowPreview draw={draw} color={strokeColor} width={strokeWidth} />
28
+ )}
29
+ {tool === 'marker' && <MarkerPreview draw={draw} color={strokeColor} />}
30
+ {tool === 'freehand' && (
31
+ <FreehandPreview draw={draw} color={strokeColor} width={strokeWidth} />
32
+ )}
33
+ </Group>
34
+ );
35
+ }
36
+
37
+ function CirclePreview({
38
+ draw,
39
+ color,
40
+ width,
41
+ }: {
42
+ draw: DrawState;
43
+ color: string;
44
+ width: number;
45
+ }) {
46
+ const cx = useDerivedValue(() => draw.start.value.x);
47
+ const cy = useDerivedValue(() => draw.start.value.y);
48
+ const r = useDerivedValue(() =>
49
+ Math.hypot(
50
+ draw.current.value.x - draw.start.value.x,
51
+ draw.current.value.y - draw.start.value.y
52
+ )
53
+ );
54
+ return (
55
+ <Circle cx={cx} cy={cy} r={r} color={color} style="stroke" strokeWidth={width} />
56
+ );
57
+ }
58
+
59
+ function ArrowPreview({
60
+ draw,
61
+ color,
62
+ width,
63
+ }: {
64
+ draw: DrawState;
65
+ color: string;
66
+ width: number;
67
+ }) {
68
+ const headSize = width * ARROW_HEAD_RATIO;
69
+
70
+ const p1 = useDerivedValue(() => draw.start.value);
71
+ const p2 = useDerivedValue(() => draw.current.value);
72
+ const barb1 = useDerivedValue(() => {
73
+ const s = draw.start.value;
74
+ const e = draw.current.value;
75
+ const angle = Math.atan2(e.y - s.y, e.x - s.x);
76
+ const barb = (Math.PI * 5) / 6;
77
+ return {
78
+ x: e.x + headSize * Math.cos(angle + barb),
79
+ y: e.y + headSize * Math.sin(angle + barb),
80
+ };
81
+ });
82
+ const barb2 = useDerivedValue(() => {
83
+ const s = draw.start.value;
84
+ const e = draw.current.value;
85
+ const angle = Math.atan2(e.y - s.y, e.x - s.x);
86
+ const barb = (Math.PI * 5) / 6;
87
+ return {
88
+ x: e.x + headSize * Math.cos(angle - barb),
89
+ y: e.y + headSize * Math.sin(angle - barb),
90
+ };
91
+ });
92
+
93
+ return (
94
+ <Group color={color} style="stroke" strokeWidth={width} strokeCap="round">
95
+ <Line p1={p1} p2={p2} />
96
+ <Line p1={p2} p2={barb1} />
97
+ <Line p1={p2} p2={barb2} />
98
+ </Group>
99
+ );
100
+ }
101
+
102
+ function MarkerPreview({ draw, color }: { draw: DrawState; color: string }) {
103
+ const x = useDerivedValue(() => Math.min(draw.start.value.x, draw.current.value.x));
104
+ const y = useDerivedValue(() => Math.min(draw.start.value.y, draw.current.value.y));
105
+ const w = useDerivedValue(() => Math.abs(draw.current.value.x - draw.start.value.x));
106
+ const h = useDerivedValue(() => Math.abs(draw.current.value.y - draw.start.value.y));
107
+ return <Rect x={x} y={y} width={w} height={h} color={withOpacity(color, MARKER_OPACITY)} />;
108
+ }
109
+
110
+ function FreehandPreview({
111
+ draw,
112
+ color,
113
+ width,
114
+ }: {
115
+ draw: DrawState;
116
+ color: string;
117
+ width: number;
118
+ }) {
119
+ const path = useDerivedValue<SkPath>(() => {
120
+ const pts = draw.points.value;
121
+ const p = Skia.Path.Make();
122
+ if (pts.length > 0) {
123
+ p.moveTo(pts[0]!.x, pts[0]!.y);
124
+ for (let i = 1; i < pts.length; i++) {
125
+ p.lineTo(pts[i]!.x, pts[i]!.y);
126
+ }
127
+ }
128
+ return p;
129
+ });
130
+ return (
131
+ <Path
132
+ path={path}
133
+ color={color}
134
+ style="stroke"
135
+ strokeWidth={width}
136
+ strokeCap="round"
137
+ strokeJoin="round"
138
+ />
139
+ );
140
+ }
@@ -0,0 +1,92 @@
1
+ import React, { useMemo } from 'react';
2
+ import {
3
+ Circle,
4
+ Group,
5
+ Line,
6
+ Path,
7
+ Rect,
8
+ Skia,
9
+ } from '@shopify/react-native-skia';
10
+ import { useDerivedValue } from 'react-native-reanimated';
11
+
12
+ import type { EditorContextValue } from '../context/EditorContext';
13
+ import { getAnnotationById } from '../state/selectors';
14
+ import { selectionHandles } from '../gestures/handles';
15
+ import { HANDLE_SIZE } from '../constants';
16
+
17
+ const SELECTION_COLOR = '#1E90FF';
18
+
19
+ /**
20
+ * Draws the selection bounding box + resize corners + rotate handle in SCREEN
21
+ * space (outside the scene transform, so handle size is zoom-independent). The
22
+ * overlay hides itself during an active live transform to avoid a stale double.
23
+ */
24
+ export function SelectionOverlay({ editor }: { editor: EditorContextValue }) {
25
+ const { annotations, selectedId, matrix, live } = editor;
26
+ const selected = getAnnotationById(annotations, selectedId);
27
+
28
+ const opacity = useDerivedValue(() => (live.active.value ? 0 : 1));
29
+
30
+ const handles = useMemo(
31
+ () => (selected ? selectionHandles(selected, matrix) : null),
32
+ [selected, matrix]
33
+ );
34
+
35
+ const boxPath = useMemo(() => {
36
+ if (!handles) {
37
+ return null;
38
+ }
39
+ const p = Skia.Path.Make();
40
+ const c = handles.corners;
41
+ p.moveTo(c[0].x, c[0].y);
42
+ p.lineTo(c[1].x, c[1].y);
43
+ p.lineTo(c[2].x, c[2].y);
44
+ p.lineTo(c[3].x, c[3].y);
45
+ p.close();
46
+ return p;
47
+ }, [handles]);
48
+
49
+ if (!selected || !handles || !boxPath) {
50
+ return null;
51
+ }
52
+
53
+ const topMid = {
54
+ x: (handles.corners[0].x + handles.corners[1].x) / 2,
55
+ y: (handles.corners[0].y + handles.corners[1].y) / 2,
56
+ };
57
+
58
+ return (
59
+ <Group opacity={opacity}>
60
+ <Path
61
+ path={boxPath}
62
+ color={SELECTION_COLOR}
63
+ style="stroke"
64
+ strokeWidth={2}
65
+ />
66
+ {/* Rotate handle */}
67
+ <Line p1={topMid} p2={handles.rotate} color={SELECTION_COLOR} strokeWidth={2} />
68
+ <Circle cx={handles.rotate.x} cy={handles.rotate.y} r={HANDLE_SIZE / 2} color={SELECTION_COLOR} />
69
+ {/* Corner resize handles (white fill + blue border) */}
70
+ {handles.corners.map((corner, i) => (
71
+ <React.Fragment key={i}>
72
+ <Rect
73
+ x={corner.x - HANDLE_SIZE / 2}
74
+ y={corner.y - HANDLE_SIZE / 2}
75
+ width={HANDLE_SIZE}
76
+ height={HANDLE_SIZE}
77
+ color="#FFFFFF"
78
+ />
79
+ <Rect
80
+ x={corner.x - HANDLE_SIZE / 2}
81
+ y={corner.y - HANDLE_SIZE / 2}
82
+ width={HANDLE_SIZE}
83
+ height={HANDLE_SIZE}
84
+ color={SELECTION_COLOR}
85
+ style="stroke"
86
+ strokeWidth={2}
87
+ />
88
+ </React.Fragment>
89
+ ))}
90
+ </Group>
91
+ );
92
+ }
@@ -0,0 +1,46 @@
1
+ import type { ColorString } from './types';
2
+
3
+ /** Default stroke width (in image pixels) for new shapes. */
4
+ export const DEFAULT_STROKE_WIDTH = 8;
5
+
6
+ /** Default font size (in image pixels) for new text annotations. */
7
+ export const DEFAULT_FONT_SIZE = 48;
8
+
9
+ /** Default wrap width (in image pixels) for new text boxes. */
10
+ export const DEFAULT_TEXT_WIDTH = 400;
11
+
12
+ /** Semi-transparent opacity for the marker/highlighter tool. */
13
+ export const MARKER_OPACITY = 0.4;
14
+
15
+ /** Arrowhead barb length as a multiple of stroke width. */
16
+ export const ARROW_HEAD_RATIO = 4;
17
+
18
+ /** Touch slop (in screen points) added to hit-test radii for easier selection. */
19
+ export const HIT_SLOP = 12;
20
+
21
+ /** On-screen size (points) of selection handles. */
22
+ export const HANDLE_SIZE = 14;
23
+
24
+ /** Distance (points) the rotate handle sits above the selection's top edge. */
25
+ export const ROTATE_HANDLE_OFFSET = 36;
26
+
27
+ /** Max entries retained in the undo/redo history. */
28
+ export const MAX_HISTORY = 50;
29
+
30
+ /** Minimum spacing (image px) between sampled freehand points. */
31
+ export const FREEHAND_MIN_DISTANCE = 3;
32
+
33
+ export const DEFAULT_STROKE_COLOR: ColorString = '#FF3B30';
34
+ export const DEFAULT_TEXT_COLOR: ColorString = '#FFFFFF';
35
+
36
+ export const DEFAULT_PALETTE: ColorString[] = [
37
+ '#FF3B30', // red
38
+ '#FF9500', // orange
39
+ '#FFCC00', // yellow
40
+ '#34C759', // green
41
+ '#007AFF', // blue
42
+ '#5856D6', // indigo
43
+ '#AF52DE', // purple
44
+ '#FFFFFF', // white
45
+ '#000000', // black
46
+ ];