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
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ export { ImageEditor } from './ImageEditor';
2
+
3
+ export type {
4
+ ImageEditorProps,
5
+ ImageEditorRef,
6
+ ImageSource,
7
+ ExportOptions,
8
+ WriteFileFn,
9
+ OutputFormat,
10
+ Annotation,
11
+ AnnotationType,
12
+ CircleAnnotation,
13
+ ArrowAnnotation,
14
+ MarkerAnnotation,
15
+ FreehandAnnotation,
16
+ TextAnnotation,
17
+ SceneTransform,
18
+ ToolType,
19
+ ColorString,
20
+ Vec2,
21
+ Rect,
22
+ } from './types';
23
+
24
+ // Advanced: build a custom UI on top of the editor state/context.
25
+ export { EditorProvider, useEditor } from './context/EditorContext';
26
+ export { exportImage } from './export/exportImage';
27
+
28
+ export {
29
+ DEFAULT_PALETTE,
30
+ DEFAULT_STROKE_COLOR,
31
+ DEFAULT_TEXT_COLOR,
32
+ } from './constants';
@@ -0,0 +1,71 @@
1
+ import { MAX_HISTORY } from '../constants';
2
+
3
+ /**
4
+ * Generic, pure undo/redo history over an immutable document `T`.
5
+ * Snapshots are cheap here because the document is plain JSON (annotations +
6
+ * scene transform) — it never contains image bytes.
7
+ */
8
+ export interface History<T> {
9
+ past: T[];
10
+ present: T;
11
+ future: T[];
12
+ }
13
+
14
+ export function initHistory<T>(present: T): History<T> {
15
+ return { past: [], present, future: [] };
16
+ }
17
+
18
+ /** Replace the present, pushing the old present onto the undo stack. */
19
+ export function commit<T>(history: History<T>, next: T): History<T> {
20
+ if (next === history.present) {
21
+ return history;
22
+ }
23
+ const past = [...history.past, history.present];
24
+ // Cap the depth to bound memory.
25
+ if (past.length > MAX_HISTORY) {
26
+ past.shift();
27
+ }
28
+ return { past, present: next, future: [] };
29
+ }
30
+
31
+ /**
32
+ * Replace the present WITHOUT creating a history entry — used for live edits
33
+ * (e.g. typing in a text box) that should collapse into the eventual commit.
34
+ */
35
+ export function replacePresent<T>(history: History<T>, next: T): History<T> {
36
+ return { ...history, present: next };
37
+ }
38
+
39
+ export function undo<T>(history: History<T>): History<T> {
40
+ if (history.past.length === 0) {
41
+ return history;
42
+ }
43
+ const previous = history.past[history.past.length - 1]!;
44
+ const past = history.past.slice(0, -1);
45
+ return {
46
+ past,
47
+ present: previous,
48
+ future: [history.present, ...history.future],
49
+ };
50
+ }
51
+
52
+ export function redo<T>(history: History<T>): History<T> {
53
+ if (history.future.length === 0) {
54
+ return history;
55
+ }
56
+ const next = history.future[0]!;
57
+ const future = history.future.slice(1);
58
+ return {
59
+ past: [...history.past, history.present],
60
+ present: next,
61
+ future,
62
+ };
63
+ }
64
+
65
+ export function canUndo<T>(history: History<T>): boolean {
66
+ return history.past.length > 0;
67
+ }
68
+
69
+ export function canRedo<T>(history: History<T>): boolean {
70
+ return history.future.length > 0;
71
+ }
@@ -0,0 +1,16 @@
1
+ import type { Annotation } from '../types';
2
+
3
+ export function getAnnotationById(
4
+ annotations: Annotation[],
5
+ id: string | null
6
+ ): Annotation | undefined {
7
+ if (!id) {
8
+ return undefined;
9
+ }
10
+ return annotations.find((a) => a.id === id);
11
+ }
12
+
13
+ /** Annotations in paint order (ascending z). */
14
+ export function sortedByZ(annotations: Annotation[]): Annotation[] {
15
+ return [...annotations].sort((a, b) => a.z - b.z);
16
+ }
@@ -0,0 +1,93 @@
1
+ import { useReducer } from 'react';
2
+
3
+ import type { Annotation, EditorDocument, SceneTransform } from '../types';
4
+ import { IDENTITY_SCENE } from '../types';
5
+ import {
6
+ commit,
7
+ initHistory,
8
+ redo as redoHistory,
9
+ replacePresent,
10
+ undo as undoHistory,
11
+ } from './history';
12
+ import type { History } from './history';
13
+
14
+ export type EditorAction =
15
+ | { type: 'ADD_ANNOTATION'; annotation: Annotation }
16
+ | {
17
+ type: 'UPDATE_ANNOTATION';
18
+ id: string;
19
+ changes: Partial<Annotation>;
20
+ /** When true, does not create a history entry (live edit). */
21
+ transient?: boolean;
22
+ }
23
+ | { type: 'DELETE_ANNOTATION'; id: string }
24
+ | {
25
+ type: 'SET_SCENE';
26
+ changes: Partial<SceneTransform>;
27
+ /** When true, does not create a history entry (live slider drag). */
28
+ transient?: boolean;
29
+ }
30
+ | { type: 'REPLACE'; document: EditorDocument }
31
+ | { type: 'RESET' }
32
+ | { type: 'UNDO' }
33
+ | { type: 'REDO' };
34
+
35
+ export type EditorState = History<EditorDocument>;
36
+
37
+ const EMPTY_DOCUMENT: EditorDocument = {
38
+ annotations: [],
39
+ scene: IDENTITY_SCENE,
40
+ };
41
+
42
+ function nextZ(annotations: Annotation[]): number {
43
+ return annotations.reduce((max, a) => Math.max(max, a.z), 0) + 1;
44
+ }
45
+
46
+ function reducer(state: EditorState, action: EditorAction): EditorState {
47
+ const doc = state.present;
48
+ switch (action.type) {
49
+ case 'ADD_ANNOTATION': {
50
+ const annotation = { ...action.annotation, z: nextZ(doc.annotations) };
51
+ return commit(state, {
52
+ ...doc,
53
+ annotations: [...doc.annotations, annotation],
54
+ });
55
+ }
56
+ case 'UPDATE_ANNOTATION': {
57
+ const annotations = doc.annotations.map((a) =>
58
+ a.id === action.id ? ({ ...a, ...action.changes } as Annotation) : a
59
+ );
60
+ const next = { ...doc, annotations };
61
+ return action.transient ? replacePresent(state, next) : commit(state, next);
62
+ }
63
+ case 'DELETE_ANNOTATION': {
64
+ return commit(state, {
65
+ ...doc,
66
+ annotations: doc.annotations.filter((a) => a.id !== action.id),
67
+ });
68
+ }
69
+ case 'SET_SCENE': {
70
+ const next = { ...doc, scene: { ...doc.scene, ...action.changes } };
71
+ return action.transient ? replacePresent(state, next) : commit(state, next);
72
+ }
73
+ case 'REPLACE': {
74
+ return commit(state, action.document);
75
+ }
76
+ case 'RESET': {
77
+ return commit(state, EMPTY_DOCUMENT);
78
+ }
79
+ case 'UNDO':
80
+ return undoHistory(state);
81
+ case 'REDO':
82
+ return redoHistory(state);
83
+ default:
84
+ return state;
85
+ }
86
+ }
87
+
88
+ export function useEditorReducer(initial?: Partial<EditorDocument>) {
89
+ return useReducer(
90
+ reducer,
91
+ initHistory<EditorDocument>({ ...EMPTY_DOCUMENT, ...initial })
92
+ );
93
+ }
@@ -0,0 +1,72 @@
1
+ import { Pressable, ScrollView, StyleSheet, View } from 'react-native';
2
+
3
+ import { useEditor } from '../context/EditorContext';
4
+ import { getAnnotationById } from '../state/selectors';
5
+ import type { ColorString } from '../types';
6
+
7
+ /**
8
+ * Horizontal swatch strip. Picking a color sets the "current" color used for new
9
+ * shapes and, if an annotation is selected, recolors it (the right field per
10
+ * type). While editing text, it also updates the live text color.
11
+ */
12
+ export function ColorPicker() {
13
+ const {
14
+ palette,
15
+ strokeColor,
16
+ setStrokeColor,
17
+ setTextColor,
18
+ selectedId,
19
+ editingTextId,
20
+ annotations,
21
+ dispatch,
22
+ } = useEditor();
23
+
24
+ const onPick = (color: ColorString) => {
25
+ setStrokeColor(color);
26
+ setTextColor(color);
27
+ const target = getAnnotationById(annotations, editingTextId ?? selectedId);
28
+ if (target) {
29
+ if (target.type === 'text' || target.type === 'marker') {
30
+ dispatch({ type: 'UPDATE_ANNOTATION', id: target.id, changes: { color } });
31
+ } else {
32
+ dispatch({
33
+ type: 'UPDATE_ANNOTATION',
34
+ id: target.id,
35
+ changes: { strokeColor: color },
36
+ });
37
+ }
38
+ }
39
+ };
40
+
41
+ return (
42
+ <ScrollView
43
+ horizontal
44
+ showsHorizontalScrollIndicator={false}
45
+ contentContainerStyle={styles.row}
46
+ >
47
+ {palette.map((color) => (
48
+ <Pressable key={color} onPress={() => onPick(color)} hitSlop={4}>
49
+ <View
50
+ style={[
51
+ styles.swatch,
52
+ { backgroundColor: color },
53
+ color === strokeColor && styles.selected,
54
+ ]}
55
+ />
56
+ </Pressable>
57
+ ))}
58
+ </ScrollView>
59
+ );
60
+ }
61
+
62
+ const styles = StyleSheet.create({
63
+ row: { alignItems: 'center', paddingHorizontal: 8, gap: 8 },
64
+ swatch: {
65
+ width: 28,
66
+ height: 28,
67
+ borderRadius: 14,
68
+ borderWidth: 2,
69
+ borderColor: 'rgba(255,255,255,0.5)',
70
+ },
71
+ selected: { borderColor: '#FFFFFF', borderWidth: 3 },
72
+ });
@@ -0,0 +1,46 @@
1
+ import { StyleSheet, View } from 'react-native';
2
+
3
+ import { useEditor } from '../context/EditorContext';
4
+ import { ToolButton } from './ToolButton';
5
+
6
+ /** Apply / reset / cancel controls shown while the crop tool is active. */
7
+ export function CropControls() {
8
+ const { cropRectSV, dispatch, setTool, imageSize } = useEditor();
9
+
10
+ const apply = () => {
11
+ const r = cropRectSV.value;
12
+ // Ignore a degenerate crop.
13
+ const isFull =
14
+ r.x <= 0 &&
15
+ r.y <= 0 &&
16
+ r.width >= imageSize.width &&
17
+ r.height >= imageSize.height;
18
+ dispatch({
19
+ type: 'SET_SCENE',
20
+ changes: { cropRect: isFull ? null : { ...r } },
21
+ });
22
+ setTool('select');
23
+ };
24
+
25
+ const reset = () => {
26
+ dispatch({ type: 'SET_SCENE', changes: { cropRect: null } });
27
+ setTool('select');
28
+ };
29
+
30
+ return (
31
+ <View style={styles.row}>
32
+ <ToolButton label="✕ Cancel" onPress={() => setTool('select')} />
33
+ <ToolButton label="↺ Reset" onPress={reset} />
34
+ <ToolButton label="✓ Apply" active onPress={apply} />
35
+ </View>
36
+ );
37
+ }
38
+
39
+ const styles = StyleSheet.create({
40
+ row: {
41
+ flexDirection: 'row',
42
+ justifyContent: 'center',
43
+ alignItems: 'center',
44
+ paddingVertical: 6,
45
+ },
46
+ });
@@ -0,0 +1,56 @@
1
+ import { StyleSheet, Text, View } from 'react-native';
2
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
3
+ import { runOnJS, useSharedValue } from 'react-native-reanimated';
4
+
5
+ import { useEditor } from '../context/EditorContext';
6
+
7
+ const SENSITIVITY = 0.006; // radians per pixel dragged
8
+
9
+ /**
10
+ * Drag left/right anywhere on the bar to rotate the whole image to ANY angle.
11
+ * Updates are transient during the drag (no history spam) and committed on
12
+ * release. Double-nothing here is fine — the scene re-renders, but rotation is
13
+ * an occasional interaction.
14
+ */
15
+ export function RotationSlider() {
16
+ const { doc, dispatch } = useEditor();
17
+ const startRotation = useSharedValue(0);
18
+ const rotationDeg = Math.round((doc.scene.rotation * 180) / Math.PI);
19
+
20
+ const setScene = (rotation: number, transient: boolean) =>
21
+ dispatch({ type: 'SET_SCENE', changes: { rotation }, transient });
22
+
23
+ const pan = Gesture.Pan()
24
+ .onBegin(() => {
25
+ 'worklet';
26
+ startRotation.value = doc.scene.rotation;
27
+ })
28
+ .onChange((e) => {
29
+ 'worklet';
30
+ runOnJS(setScene)(startRotation.value + e.translationX * SENSITIVITY, true);
31
+ })
32
+ .onEnd((e) => {
33
+ 'worklet';
34
+ runOnJS(setScene)(startRotation.value + e.translationX * SENSITIVITY, false);
35
+ });
36
+
37
+ return (
38
+ <GestureDetector gesture={pan}>
39
+ <View style={styles.bar}>
40
+ <Text style={styles.label}>⟲ Rotate {rotationDeg}° ⟳</Text>
41
+ </View>
42
+ </GestureDetector>
43
+ );
44
+ }
45
+
46
+ const styles = StyleSheet.create({
47
+ bar: {
48
+ height: 36,
49
+ borderRadius: 8,
50
+ backgroundColor: 'rgba(255,255,255,0.12)',
51
+ alignItems: 'center',
52
+ justifyContent: 'center',
53
+ marginHorizontal: 8,
54
+ },
55
+ label: { color: '#FFFFFF', fontSize: 14, fontWeight: '600' },
56
+ });
@@ -0,0 +1,104 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { StyleSheet, TextInput } from 'react-native';
3
+
4
+ import { useEditor } from '../context/EditorContext';
5
+ import { getAnnotationById } from '../state/selectors';
6
+ import { applyToPoint } from '../utils/math';
7
+
8
+ /**
9
+ * Native TextInput overlaid on the text annotation being edited. Skia can't
10
+ * accept keyboard input, so we edit here and mirror the string back into the
11
+ * annotation (transient updates while typing; a committed update on blur). The
12
+ * Skia text for this annotation is hidden while editing (see AnnotationLayer).
13
+ * Positioning ignores rotation for simplicity.
14
+ */
15
+ export function TextInputOverlay() {
16
+ const {
17
+ editingTextId,
18
+ setEditingTextId,
19
+ annotations,
20
+ dispatch,
21
+ matrix,
22
+ setSelectedId,
23
+ } = useEditor();
24
+
25
+ const target = getAnnotationById(annotations, editingTextId);
26
+ const isText = target?.type === 'text' ? target : null;
27
+ const inputRef = useRef<TextInput>(null);
28
+ const [value, setValue] = useState('');
29
+
30
+ useEffect(() => {
31
+ if (isText) {
32
+ setValue(isText.text);
33
+ const t = setTimeout(() => inputRef.current?.focus(), 50);
34
+ return () => clearTimeout(t);
35
+ }
36
+ return undefined;
37
+ // eslint-disable-next-line react-hooks/exhaustive-deps
38
+ }, [editingTextId]);
39
+
40
+ if (!isText) {
41
+ return null;
42
+ }
43
+
44
+ const scaleFactor = Math.hypot(matrix.a, matrix.b) || 1;
45
+ const screen = applyToPoint(matrix, isText.origin);
46
+ const fontSize = isText.fontSize * scaleFactor;
47
+ const width = isText.width * scaleFactor;
48
+
49
+ const onChange = (text: string) => {
50
+ setValue(text);
51
+ dispatch({
52
+ type: 'UPDATE_ANNOTATION',
53
+ id: isText.id,
54
+ changes: { text },
55
+ transient: true,
56
+ });
57
+ };
58
+
59
+ const onDone = () => {
60
+ const trimmed = value.trim();
61
+ if (trimmed.length === 0) {
62
+ dispatch({ type: 'DELETE_ANNOTATION', id: isText.id });
63
+ setSelectedId(null);
64
+ } else {
65
+ // Promote the accumulated transient edits into a single history entry.
66
+ dispatch({ type: 'UPDATE_ANNOTATION', id: isText.id, changes: { text: value } });
67
+ }
68
+ setEditingTextId(null);
69
+ };
70
+
71
+ return (
72
+ <TextInput
73
+ ref={inputRef}
74
+ value={value}
75
+ onChangeText={onChange}
76
+ onBlur={onDone}
77
+ onSubmitEditing={onDone}
78
+ multiline
79
+ blurOnSubmit
80
+ style={[
81
+ styles.input,
82
+ {
83
+ left: screen.x,
84
+ top: screen.y,
85
+ width: Math.max(width, 80),
86
+ fontSize,
87
+ color: isText.color,
88
+ lineHeight: fontSize * 1.2,
89
+ },
90
+ ]}
91
+ placeholder="Text"
92
+ placeholderTextColor="rgba(255,255,255,0.5)"
93
+ />
94
+ );
95
+ }
96
+
97
+ const styles = StyleSheet.create({
98
+ input: {
99
+ position: 'absolute',
100
+ padding: 0,
101
+ margin: 0,
102
+ textAlignVertical: 'top',
103
+ },
104
+ });
@@ -0,0 +1,46 @@
1
+ import { Pressable, StyleSheet, Text } from 'react-native';
2
+
3
+ export function ToolButton({
4
+ label,
5
+ active,
6
+ disabled,
7
+ onPress,
8
+ }: {
9
+ label: string;
10
+ active?: boolean;
11
+ disabled?: boolean;
12
+ onPress: () => void;
13
+ }) {
14
+ return (
15
+ <Pressable
16
+ onPress={onPress}
17
+ disabled={disabled}
18
+ style={({ pressed }) => [
19
+ styles.button,
20
+ active && styles.active,
21
+ disabled && styles.disabled,
22
+ pressed && styles.pressed,
23
+ ]}
24
+ hitSlop={6}
25
+ >
26
+ <Text style={[styles.label, active && styles.activeLabel]}>{label}</Text>
27
+ </Pressable>
28
+ );
29
+ }
30
+
31
+ const styles = StyleSheet.create({
32
+ button: {
33
+ minWidth: 44,
34
+ height: 44,
35
+ paddingHorizontal: 8,
36
+ alignItems: 'center',
37
+ justifyContent: 'center',
38
+ borderRadius: 10,
39
+ marginHorizontal: 2,
40
+ },
41
+ active: { backgroundColor: '#1E90FF' },
42
+ disabled: { opacity: 0.35 },
43
+ pressed: { opacity: 0.6 },
44
+ label: { color: '#FFFFFF', fontSize: 18 },
45
+ activeLabel: { color: '#FFFFFF', fontWeight: '700' },
46
+ });
@@ -0,0 +1,110 @@
1
+ import { StyleSheet, View } from 'react-native';
2
+
3
+ import { useEditor } from '../context/EditorContext';
4
+ import type { ToolType } from '../types';
5
+ import { ToolButton } from './ToolButton';
6
+ import { ColorPicker } from './ColorPicker';
7
+ import { CropControls } from './CropControls';
8
+ import { RotationSlider } from './RotationSlider';
9
+
10
+ const TOOLS: { tool: ToolType; label: string }[] = [
11
+ { tool: 'select', label: '⇱' },
12
+ { tool: 'circle', label: '◯' },
13
+ { tool: 'arrow', label: '↗' },
14
+ { tool: 'marker', label: '▬' },
15
+ { tool: 'freehand', label: '✎' },
16
+ { tool: 'text', label: 'T' },
17
+ { tool: 'crop', label: '⛶' },
18
+ ];
19
+
20
+ export function Toolbar({ onDone }: { onDone?: () => void }) {
21
+ const {
22
+ tool,
23
+ setTool,
24
+ selectedId,
25
+ setSelectedId,
26
+ canUndo,
27
+ canRedo,
28
+ dispatch,
29
+ doc,
30
+ } = useEditor();
31
+
32
+ const cropping = tool === 'crop';
33
+
34
+ const deleteSelected = () => {
35
+ if (selectedId) {
36
+ dispatch({ type: 'DELETE_ANNOTATION', id: selectedId });
37
+ setSelectedId(null);
38
+ }
39
+ };
40
+
41
+ const bumpScale = (delta: number) => {
42
+ const next = Math.max(0.1, Math.min(4, doc.scene.scale + delta));
43
+ dispatch({ type: 'SET_SCENE', changes: { scale: next } });
44
+ };
45
+
46
+ return (
47
+ <View style={styles.container}>
48
+ {cropping ? (
49
+ <>
50
+ <RotationSlider />
51
+ <CropControls />
52
+ </>
53
+ ) : (
54
+ <>
55
+ <View style={styles.topRow}>
56
+ <ToolButton label="↶" disabled={!canUndo} onPress={() => dispatch({ type: 'UNDO' })} />
57
+ <ToolButton label="↷" disabled={!canRedo} onPress={() => dispatch({ type: 'REDO' })} />
58
+ <ToolButton label="−" onPress={() => bumpScale(-0.1)} />
59
+ <ToolButton label="+" onPress={() => bumpScale(0.1)} />
60
+ <ToolButton
61
+ label="🗑"
62
+ disabled={!selectedId}
63
+ onPress={deleteSelected}
64
+ />
65
+ {onDone ? <ToolButton label="Done" active onPress={onDone} /> : null}
66
+ </View>
67
+
68
+ <RotationSlider />
69
+ <ColorPicker />
70
+
71
+ <View style={styles.toolRow}>
72
+ {TOOLS.map((t) => (
73
+ <ToolButton
74
+ key={t.tool}
75
+ label={t.label}
76
+ active={tool === t.tool}
77
+ onPress={() => {
78
+ setTool(t.tool);
79
+ if (t.tool !== 'select') {
80
+ setSelectedId(null);
81
+ }
82
+ }}
83
+ />
84
+ ))}
85
+ </View>
86
+ </>
87
+ )}
88
+ </View>
89
+ );
90
+ }
91
+
92
+ const styles = StyleSheet.create({
93
+ container: {
94
+ backgroundColor: '#1C1C1E',
95
+ paddingVertical: 8,
96
+ gap: 8,
97
+ },
98
+ topRow: {
99
+ flexDirection: 'row',
100
+ justifyContent: 'center',
101
+ alignItems: 'center',
102
+ flexWrap: 'wrap',
103
+ },
104
+ toolRow: {
105
+ flexDirection: 'row',
106
+ justifyContent: 'center',
107
+ alignItems: 'center',
108
+ flexWrap: 'wrap',
109
+ },
110
+ });