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.
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/lib/commonjs/ImageEditor.js +141 -0
- package/lib/commonjs/ImageEditor.js.map +1 -0
- package/lib/commonjs/annotations/AnnotationView.js +42 -0
- package/lib/commonjs/annotations/AnnotationView.js.map +1 -0
- package/lib/commonjs/annotations/ArrowAnnotation.js +29 -0
- package/lib/commonjs/annotations/ArrowAnnotation.js.map +1 -0
- package/lib/commonjs/annotations/CircleAnnotation.js +31 -0
- package/lib/commonjs/annotations/CircleAnnotation.js.map +1 -0
- package/lib/commonjs/annotations/FreehandAnnotation.js +29 -0
- package/lib/commonjs/annotations/FreehandAnnotation.js.map +1 -0
- package/lib/commonjs/annotations/MarkerAnnotation.js +27 -0
- package/lib/commonjs/annotations/MarkerAnnotation.js.map +1 -0
- package/lib/commonjs/annotations/RotatedGroup.js +34 -0
- package/lib/commonjs/annotations/RotatedGroup.js.map +1 -0
- package/lib/commonjs/annotations/TextAnnotation.js +40 -0
- package/lib/commonjs/annotations/TextAnnotation.js.map +1 -0
- package/lib/commonjs/annotations/geometry.js +73 -0
- package/lib/commonjs/annotations/geometry.js.map +1 -0
- package/lib/commonjs/annotations/geometryPure.js +104 -0
- package/lib/commonjs/annotations/geometryPure.js.map +1 -0
- package/lib/commonjs/canvas/AnnotationLayer.js +58 -0
- package/lib/commonjs/canvas/AnnotationLayer.js.map +1 -0
- package/lib/commonjs/canvas/BaseImageLayer.js +27 -0
- package/lib/commonjs/canvas/BaseImageLayer.js.map +1 -0
- package/lib/commonjs/canvas/CropOverlay.js +135 -0
- package/lib/commonjs/canvas/CropOverlay.js.map +1 -0
- package/lib/commonjs/canvas/EditorCanvas.js +91 -0
- package/lib/commonjs/canvas/EditorCanvas.js.map +1 -0
- package/lib/commonjs/canvas/InFlightLayer.js +152 -0
- package/lib/commonjs/canvas/InFlightLayer.js.map +1 -0
- package/lib/commonjs/canvas/SelectionOverlay.js +90 -0
- package/lib/commonjs/canvas/SelectionOverlay.js.map +1 -0
- package/lib/commonjs/constants.js +56 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/context/EditorContext.js +132 -0
- package/lib/commonjs/context/EditorContext.js.map +1 -0
- package/lib/commonjs/export/drawScene.js +97 -0
- package/lib/commonjs/export/drawScene.js.map +1 -0
- package/lib/commonjs/export/exportImage.js +92 -0
- package/lib/commonjs/export/exportImage.js.map +1 -0
- package/lib/commonjs/gestures/applyTransform.js +79 -0
- package/lib/commonjs/gestures/applyTransform.js.map +1 -0
- package/lib/commonjs/gestures/createAnnotation.js +73 -0
- package/lib/commonjs/gestures/createAnnotation.js.map +1 -0
- package/lib/commonjs/gestures/handles.js +53 -0
- package/lib/commonjs/gestures/handles.js.map +1 -0
- package/lib/commonjs/gestures/hitTest.js +72 -0
- package/lib/commonjs/gestures/hitTest.js.map +1 -0
- package/lib/commonjs/gestures/useCropGesture.js +149 -0
- package/lib/commonjs/gestures/useCropGesture.js.map +1 -0
- package/lib/commonjs/gestures/useEditorGestures.js +289 -0
- package/lib/commonjs/gestures/useEditorGestures.js.map +1 -0
- package/lib/commonjs/image/disposeRegistry.js +63 -0
- package/lib/commonjs/image/disposeRegistry.js.map +1 -0
- package/lib/commonjs/image/useLoadedImage.js +121 -0
- package/lib/commonjs/image/useLoadedImage.js.map +1 -0
- package/lib/commonjs/index.js +52 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/state/history.js +85 -0
- package/lib/commonjs/state/history.js.map +1 -0
- package/lib/commonjs/state/selectors.js +19 -0
- package/lib/commonjs/state/selectors.js.map +1 -0
- package/lib/commonjs/state/useEditorReducer.js +83 -0
- package/lib/commonjs/state/useEditorReducer.js.map +1 -0
- package/lib/commonjs/toolbar/ColorPicker.js +84 -0
- package/lib/commonjs/toolbar/ColorPicker.js.map +1 -0
- package/lib/commonjs/toolbar/CropControls.js +65 -0
- package/lib/commonjs/toolbar/CropControls.js.map +1 -0
- package/lib/commonjs/toolbar/RotationSlider.js +73 -0
- package/lib/commonjs/toolbar/RotationSlider.js.map +1 -0
- package/lib/commonjs/toolbar/TextInputOverlay.js +108 -0
- package/lib/commonjs/toolbar/TextInputOverlay.js.map +1 -0
- package/lib/commonjs/toolbar/ToolButton.js +56 -0
- package/lib/commonjs/toolbar/ToolButton.js.map +1 -0
- package/lib/commonjs/toolbar/Toolbar.js +137 -0
- package/lib/commonjs/toolbar/Toolbar.js.map +1 -0
- package/lib/commonjs/types.js +47 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/utils/color.js +37 -0
- package/lib/commonjs/utils/color.js.map +1 -0
- package/lib/commonjs/utils/id.js +14 -0
- package/lib/commonjs/utils/id.js.map +1 -0
- package/lib/commonjs/utils/math.js +277 -0
- package/lib/commonjs/utils/math.js.map +1 -0
- package/lib/module/ImageEditor.js +138 -0
- package/lib/module/ImageEditor.js.map +1 -0
- package/lib/module/annotations/AnnotationView.js +39 -0
- package/lib/module/annotations/AnnotationView.js.map +1 -0
- package/lib/module/annotations/ArrowAnnotation.js +26 -0
- package/lib/module/annotations/ArrowAnnotation.js.map +1 -0
- package/lib/module/annotations/CircleAnnotation.js +27 -0
- package/lib/module/annotations/CircleAnnotation.js.map +1 -0
- package/lib/module/annotations/FreehandAnnotation.js +25 -0
- package/lib/module/annotations/FreehandAnnotation.js.map +1 -0
- package/lib/module/annotations/MarkerAnnotation.js +23 -0
- package/lib/module/annotations/MarkerAnnotation.js.map +1 -0
- package/lib/module/annotations/RotatedGroup.js +29 -0
- package/lib/module/annotations/RotatedGroup.js.map +1 -0
- package/lib/module/annotations/TextAnnotation.js +37 -0
- package/lib/module/annotations/TextAnnotation.js.map +1 -0
- package/lib/module/annotations/geometry.js +56 -0
- package/lib/module/annotations/geometry.js.map +1 -0
- package/lib/module/annotations/geometryPure.js +100 -0
- package/lib/module/annotations/geometryPure.js.map +1 -0
- package/lib/module/canvas/AnnotationLayer.js +55 -0
- package/lib/module/canvas/AnnotationLayer.js.map +1 -0
- package/lib/module/canvas/BaseImageLayer.js +23 -0
- package/lib/module/canvas/BaseImageLayer.js.map +1 -0
- package/lib/module/canvas/CropOverlay.js +131 -0
- package/lib/module/canvas/CropOverlay.js.map +1 -0
- package/lib/module/canvas/EditorCanvas.js +88 -0
- package/lib/module/canvas/EditorCanvas.js.map +1 -0
- package/lib/module/canvas/InFlightLayer.js +149 -0
- package/lib/module/canvas/InFlightLayer.js.map +1 -0
- package/lib/module/canvas/SelectionOverlay.js +85 -0
- package/lib/module/canvas/SelectionOverlay.js.map +1 -0
- package/lib/module/constants.js +52 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/context/EditorContext.js +126 -0
- package/lib/module/context/EditorContext.js.map +1 -0
- package/lib/module/export/drawScene.js +93 -0
- package/lib/module/export/drawScene.js.map +1 -0
- package/lib/module/export/exportImage.js +88 -0
- package/lib/module/export/exportImage.js.map +1 -0
- package/lib/module/gestures/applyTransform.js +75 -0
- package/lib/module/gestures/applyTransform.js.map +1 -0
- package/lib/module/gestures/createAnnotation.js +65 -0
- package/lib/module/gestures/createAnnotation.js.map +1 -0
- package/lib/module/gestures/handles.js +49 -0
- package/lib/module/gestures/handles.js.map +1 -0
- package/lib/module/gestures/hitTest.js +69 -0
- package/lib/module/gestures/hitTest.js.map +1 -0
- package/lib/module/gestures/useCropGesture.js +145 -0
- package/lib/module/gestures/useCropGesture.js.map +1 -0
- package/lib/module/gestures/useEditorGestures.js +285 -0
- package/lib/module/gestures/useEditorGestures.js.map +1 -0
- package/lib/module/image/disposeRegistry.js +57 -0
- package/lib/module/image/disposeRegistry.js.map +1 -0
- package/lib/module/image/useLoadedImage.js +117 -0
- package/lib/module/image/useLoadedImage.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/state/history.js +76 -0
- package/lib/module/state/history.js.map +1 -0
- package/lib/module/state/selectors.js +14 -0
- package/lib/module/state/selectors.js.map +1 -0
- package/lib/module/state/useEditorReducer.js +79 -0
- package/lib/module/state/useEditorReducer.js.map +1 -0
- package/lib/module/toolbar/ColorPicker.js +80 -0
- package/lib/module/toolbar/ColorPicker.js.map +1 -0
- package/lib/module/toolbar/CropControls.js +62 -0
- package/lib/module/toolbar/CropControls.js.map +1 -0
- package/lib/module/toolbar/RotationSlider.js +69 -0
- package/lib/module/toolbar/RotationSlider.js.map +1 -0
- package/lib/module/toolbar/TextInputOverlay.js +105 -0
- package/lib/module/toolbar/TextInputOverlay.js.map +1 -0
- package/lib/module/toolbar/ToolButton.js +52 -0
- package/lib/module/toolbar/ToolButton.js.map +1 -0
- package/lib/module/toolbar/Toolbar.js +133 -0
- package/lib/module/toolbar/Toolbar.js.map +1 -0
- package/lib/module/types.js +43 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/color.js +33 -0
- package/lib/module/utils/color.js.map +1 -0
- package/lib/module/utils/id.js +10 -0
- package/lib/module/utils/id.js.map +1 -0
- package/lib/module/utils/math.js +258 -0
- package/lib/module/utils/math.js.map +1 -0
- package/lib/typescript/src/ImageEditor.d.ts +9 -0
- package/lib/typescript/src/ImageEditor.d.ts.map +1 -0
- package/lib/typescript/src/annotations/AnnotationView.d.ts +6 -0
- package/lib/typescript/src/annotations/AnnotationView.d.ts.map +1 -0
- package/lib/typescript/src/annotations/ArrowAnnotation.d.ts +5 -0
- package/lib/typescript/src/annotations/ArrowAnnotation.d.ts.map +1 -0
- package/lib/typescript/src/annotations/CircleAnnotation.d.ts +5 -0
- package/lib/typescript/src/annotations/CircleAnnotation.d.ts.map +1 -0
- package/lib/typescript/src/annotations/FreehandAnnotation.d.ts +5 -0
- package/lib/typescript/src/annotations/FreehandAnnotation.d.ts.map +1 -0
- package/lib/typescript/src/annotations/MarkerAnnotation.d.ts +5 -0
- package/lib/typescript/src/annotations/MarkerAnnotation.d.ts.map +1 -0
- package/lib/typescript/src/annotations/RotatedGroup.d.ts +13 -0
- package/lib/typescript/src/annotations/RotatedGroup.d.ts.map +1 -0
- package/lib/typescript/src/annotations/TextAnnotation.d.ts +10 -0
- package/lib/typescript/src/annotations/TextAnnotation.d.ts.map +1 -0
- package/lib/typescript/src/annotations/geometry.d.ts +11 -0
- package/lib/typescript/src/annotations/geometry.d.ts.map +1 -0
- package/lib/typescript/src/annotations/geometryPure.d.ts +14 -0
- package/lib/typescript/src/annotations/geometryPure.d.ts.map +1 -0
- package/lib/typescript/src/canvas/AnnotationLayer.d.ts +11 -0
- package/lib/typescript/src/canvas/AnnotationLayer.d.ts.map +1 -0
- package/lib/typescript/src/canvas/BaseImageLayer.d.ts +12 -0
- package/lib/typescript/src/canvas/BaseImageLayer.d.ts.map +1 -0
- package/lib/typescript/src/canvas/CropOverlay.d.ts +10 -0
- package/lib/typescript/src/canvas/CropOverlay.d.ts.map +1 -0
- package/lib/typescript/src/canvas/EditorCanvas.d.ts +18 -0
- package/lib/typescript/src/canvas/EditorCanvas.d.ts.map +1 -0
- package/lib/typescript/src/canvas/InFlightLayer.d.ts +12 -0
- package/lib/typescript/src/canvas/InFlightLayer.d.ts.map +1 -0
- package/lib/typescript/src/canvas/SelectionOverlay.d.ts +11 -0
- package/lib/typescript/src/canvas/SelectionOverlay.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +25 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/context/EditorContext.d.ts +66 -0
- package/lib/typescript/src/context/EditorContext.d.ts.map +1 -0
- package/lib/typescript/src/export/drawScene.d.ts +10 -0
- package/lib/typescript/src/export/drawScene.d.ts.map +1 -0
- package/lib/typescript/src/export/exportImage.d.ts +23 -0
- package/lib/typescript/src/export/exportImage.d.ts.map +1 -0
- package/lib/typescript/src/gestures/applyTransform.d.ts +17 -0
- package/lib/typescript/src/gestures/applyTransform.d.ts.map +1 -0
- package/lib/typescript/src/gestures/createAnnotation.d.ts +11 -0
- package/lib/typescript/src/gestures/createAnnotation.d.ts.map +1 -0
- package/lib/typescript/src/gestures/handles.d.ts +17 -0
- package/lib/typescript/src/gestures/handles.d.ts.map +1 -0
- package/lib/typescript/src/gestures/hitTest.d.ts +9 -0
- package/lib/typescript/src/gestures/hitTest.d.ts.map +1 -0
- package/lib/typescript/src/gestures/useCropGesture.d.ts +7 -0
- package/lib/typescript/src/gestures/useCropGesture.d.ts.map +1 -0
- package/lib/typescript/src/gestures/useEditorGestures.d.ts +8 -0
- package/lib/typescript/src/gestures/useEditorGestures.d.ts.map +1 -0
- package/lib/typescript/src/image/disposeRegistry.d.ts +25 -0
- package/lib/typescript/src/image/disposeRegistry.d.ts.map +1 -0
- package/lib/typescript/src/image/useLoadedImage.d.ts +23 -0
- package/lib/typescript/src/image/useLoadedImage.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/state/history.d.ts +23 -0
- package/lib/typescript/src/state/history.d.ts.map +1 -0
- package/lib/typescript/src/state/selectors.d.ts +5 -0
- package/lib/typescript/src/state/selectors.d.ts.map +1 -0
- package/lib/typescript/src/state/useEditorReducer.d.ts +32 -0
- package/lib/typescript/src/state/useEditorReducer.d.ts.map +1 -0
- package/lib/typescript/src/toolbar/ColorPicker.d.ts +7 -0
- package/lib/typescript/src/toolbar/ColorPicker.d.ts.map +1 -0
- package/lib/typescript/src/toolbar/CropControls.d.ts +3 -0
- package/lib/typescript/src/toolbar/CropControls.d.ts.map +1 -0
- package/lib/typescript/src/toolbar/RotationSlider.d.ts +8 -0
- package/lib/typescript/src/toolbar/RotationSlider.d.ts.map +1 -0
- package/lib/typescript/src/toolbar/TextInputOverlay.d.ts +9 -0
- package/lib/typescript/src/toolbar/TextInputOverlay.d.ts.map +1 -0
- package/lib/typescript/src/toolbar/ToolButton.d.ts +7 -0
- package/lib/typescript/src/toolbar/ToolButton.d.ts.map +1 -0
- package/lib/typescript/src/toolbar/Toolbar.d.ts +4 -0
- package/lib/typescript/src/toolbar/Toolbar.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +170 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/color.d.ts +8 -0
- package/lib/typescript/src/utils/color.d.ts.map +1 -0
- package/lib/typescript/src/utils/id.d.ts +3 -0
- package/lib/typescript/src/utils/id.d.ts.map +1 -0
- package/lib/typescript/src/utils/math.d.ts +68 -0
- package/lib/typescript/src/utils/math.d.ts.map +1 -0
- package/package.json +90 -0
- package/src/ImageEditor.tsx +133 -0
- package/src/annotations/AnnotationView.tsx +24 -0
- package/src/annotations/ArrowAnnotation.tsx +26 -0
- package/src/annotations/CircleAnnotation.tsx +22 -0
- package/src/annotations/FreehandAnnotation.tsx +22 -0
- package/src/annotations/MarkerAnnotation.tsx +20 -0
- package/src/annotations/RotatedGroup.tsx +28 -0
- package/src/annotations/TextAnnotation.tsx +42 -0
- package/src/annotations/geometry.ts +62 -0
- package/src/annotations/geometryPure.ts +73 -0
- package/src/canvas/AnnotationLayer.tsx +43 -0
- package/src/canvas/BaseImageLayer.tsx +28 -0
- package/src/canvas/CropOverlay.tsx +92 -0
- package/src/canvas/EditorCanvas.tsx +70 -0
- package/src/canvas/InFlightLayer.tsx +140 -0
- package/src/canvas/SelectionOverlay.tsx +92 -0
- package/src/constants.ts +46 -0
- package/src/context/EditorContext.tsx +229 -0
- package/src/export/drawScene.ts +120 -0
- package/src/export/exportImage.ts +111 -0
- package/src/gestures/applyTransform.ts +76 -0
- package/src/gestures/createAnnotation.ts +92 -0
- package/src/gestures/handles.ts +50 -0
- package/src/gestures/hitTest.ts +79 -0
- package/src/gestures/useCropGesture.ts +123 -0
- package/src/gestures/useEditorGestures.ts +308 -0
- package/src/image/disposeRegistry.ts +59 -0
- package/src/image/useLoadedImage.ts +131 -0
- package/src/index.ts +32 -0
- package/src/state/history.ts +71 -0
- package/src/state/selectors.ts +16 -0
- package/src/state/useEditorReducer.ts +93 -0
- package/src/toolbar/ColorPicker.tsx +72 -0
- package/src/toolbar/CropControls.tsx +46 -0
- package/src/toolbar/RotationSlider.tsx +56 -0
- package/src/toolbar/TextInputOverlay.tsx +104 -0
- package/src/toolbar/ToolButton.tsx +46 -0
- package/src/toolbar/Toolbar.tsx +110 -0
- package/src/types.ts +203 -0
- package/src/utils/color.ts +34 -0
- package/src/utils/id.ts +7 -0
- package/src/utils/math.ts +222 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { useSharedValue } from 'react-native-reanimated';
|
|
9
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
Annotation,
|
|
13
|
+
ColorString,
|
|
14
|
+
EditorDocument,
|
|
15
|
+
Rect,
|
|
16
|
+
ToolType,
|
|
17
|
+
Vec2,
|
|
18
|
+
} from '../types';
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_PALETTE,
|
|
21
|
+
DEFAULT_STROKE_COLOR,
|
|
22
|
+
DEFAULT_STROKE_WIDTH,
|
|
23
|
+
DEFAULT_TEXT_COLOR,
|
|
24
|
+
} from '../constants';
|
|
25
|
+
import { useEditorReducer } from '../state/useEditorReducer';
|
|
26
|
+
import type { EditorAction, EditorState } from '../state/useEditorReducer';
|
|
27
|
+
import { canRedo, canUndo } from '../state/history';
|
|
28
|
+
import { identity, imageToScreenMatrix } from '../utils/math';
|
|
29
|
+
import type { Mat } from '../utils/math';
|
|
30
|
+
|
|
31
|
+
export interface Size {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Live geometry of a shape currently being drawn (UI-thread shared values). */
|
|
37
|
+
export interface DrawState {
|
|
38
|
+
active: SharedValue<boolean>;
|
|
39
|
+
start: SharedValue<Vec2>;
|
|
40
|
+
current: SharedValue<Vec2>;
|
|
41
|
+
points: SharedValue<Vec2[]>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Live transform applied to the selected annotation during move/resize/rotate. */
|
|
45
|
+
export interface LiveTransformState {
|
|
46
|
+
active: SharedValue<boolean>;
|
|
47
|
+
tx: SharedValue<number>;
|
|
48
|
+
ty: SharedValue<number>;
|
|
49
|
+
rotate: SharedValue<number>;
|
|
50
|
+
scale: SharedValue<number>;
|
|
51
|
+
origin: SharedValue<Vec2>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface EditorContextValue {
|
|
55
|
+
// Document + history
|
|
56
|
+
state: EditorState;
|
|
57
|
+
dispatch: React.Dispatch<EditorAction>;
|
|
58
|
+
doc: EditorDocument;
|
|
59
|
+
annotations: Annotation[];
|
|
60
|
+
canUndo: boolean;
|
|
61
|
+
canRedo: boolean;
|
|
62
|
+
|
|
63
|
+
// Tool + selection
|
|
64
|
+
tool: ToolType;
|
|
65
|
+
setTool: (t: ToolType) => void;
|
|
66
|
+
selectedId: string | null;
|
|
67
|
+
setSelectedId: (id: string | null) => void;
|
|
68
|
+
|
|
69
|
+
// Style
|
|
70
|
+
strokeColor: ColorString;
|
|
71
|
+
setStrokeColor: (c: ColorString) => void;
|
|
72
|
+
textColor: ColorString;
|
|
73
|
+
setTextColor: (c: ColorString) => void;
|
|
74
|
+
strokeWidth: number;
|
|
75
|
+
setStrokeWidth: (w: number) => void;
|
|
76
|
+
palette: ColorString[];
|
|
77
|
+
|
|
78
|
+
// Text editing overlay
|
|
79
|
+
editingTextId: string | null;
|
|
80
|
+
setEditingTextId: (id: string | null) => void;
|
|
81
|
+
|
|
82
|
+
// Geometry
|
|
83
|
+
layout: Size;
|
|
84
|
+
setLayout: (s: Size) => void;
|
|
85
|
+
imageSize: Size;
|
|
86
|
+
/** image→screen affine matrix (JS thread). */
|
|
87
|
+
matrix: Mat;
|
|
88
|
+
/** image→screen affine matrix mirrored for worklet/UI-thread reads. */
|
|
89
|
+
matrixSV: SharedValue<Mat>;
|
|
90
|
+
|
|
91
|
+
// Live gesture shared values
|
|
92
|
+
draw: DrawState;
|
|
93
|
+
live: LiveTransformState;
|
|
94
|
+
|
|
95
|
+
/** In-progress crop rectangle (image space) while the crop tool is active. */
|
|
96
|
+
cropRectSV: SharedValue<Rect>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const EditorContext = createContext<EditorContextValue | null>(null);
|
|
100
|
+
|
|
101
|
+
export function useEditor(): EditorContextValue {
|
|
102
|
+
const ctx = useContext(EditorContext);
|
|
103
|
+
if (!ctx) {
|
|
104
|
+
throw new Error('useEditor must be used within <EditorProvider>');
|
|
105
|
+
}
|
|
106
|
+
return ctx;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function EditorProvider({
|
|
110
|
+
imageSize,
|
|
111
|
+
initialStrokeColor = DEFAULT_STROKE_COLOR,
|
|
112
|
+
initialTextColor = DEFAULT_TEXT_COLOR,
|
|
113
|
+
palette = DEFAULT_PALETTE,
|
|
114
|
+
children,
|
|
115
|
+
}: {
|
|
116
|
+
imageSize: Size;
|
|
117
|
+
initialStrokeColor?: ColorString;
|
|
118
|
+
initialTextColor?: ColorString;
|
|
119
|
+
palette?: ColorString[];
|
|
120
|
+
children: React.ReactNode;
|
|
121
|
+
}) {
|
|
122
|
+
const [state, dispatch] = useEditorReducer();
|
|
123
|
+
const [tool, setTool] = useState<ToolType>('select');
|
|
124
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
125
|
+
const [strokeColor, setStrokeColor] = useState(initialStrokeColor);
|
|
126
|
+
const [textColor, setTextColor] = useState(initialTextColor);
|
|
127
|
+
const [strokeWidth, setStrokeWidth] = useState(DEFAULT_STROKE_WIDTH);
|
|
128
|
+
const [editingTextId, setEditingTextId] = useState<string | null>(null);
|
|
129
|
+
const [layout, setLayout] = useState<Size>({ width: 1, height: 1 });
|
|
130
|
+
|
|
131
|
+
const doc = state.present;
|
|
132
|
+
|
|
133
|
+
// Draw shared values.
|
|
134
|
+
const drawActive = useSharedValue(false);
|
|
135
|
+
const drawStart = useSharedValue<Vec2>({ x: 0, y: 0 });
|
|
136
|
+
const drawCurrent = useSharedValue<Vec2>({ x: 0, y: 0 });
|
|
137
|
+
const drawPoints = useSharedValue<Vec2[]>([]);
|
|
138
|
+
|
|
139
|
+
// Live transform shared values.
|
|
140
|
+
const liveActive = useSharedValue(false);
|
|
141
|
+
const liveTx = useSharedValue(0);
|
|
142
|
+
const liveTy = useSharedValue(0);
|
|
143
|
+
const liveRotate = useSharedValue(0);
|
|
144
|
+
const liveScale = useSharedValue(1);
|
|
145
|
+
const liveOrigin = useSharedValue<Vec2>({ x: 0, y: 0 });
|
|
146
|
+
|
|
147
|
+
const matrixSV = useSharedValue<Mat>(identity());
|
|
148
|
+
|
|
149
|
+
const cropRectSV = useSharedValue<Rect>({
|
|
150
|
+
x: 0,
|
|
151
|
+
y: 0,
|
|
152
|
+
width: imageSize.width,
|
|
153
|
+
height: imageSize.height,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const matrix = useMemo(
|
|
157
|
+
() => imageToScreenMatrix(doc.scene, imageSize, layout),
|
|
158
|
+
[doc.scene, imageSize, layout]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Mirror the JS-thread matrix into the shared value for worklet reads.
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
matrixSV.value = matrix;
|
|
164
|
+
}, [matrix, matrixSV]);
|
|
165
|
+
|
|
166
|
+
const value = useMemo<EditorContextValue>(
|
|
167
|
+
() => ({
|
|
168
|
+
state,
|
|
169
|
+
dispatch,
|
|
170
|
+
doc,
|
|
171
|
+
annotations: doc.annotations,
|
|
172
|
+
canUndo: canUndo(state),
|
|
173
|
+
canRedo: canRedo(state),
|
|
174
|
+
tool,
|
|
175
|
+
setTool,
|
|
176
|
+
selectedId,
|
|
177
|
+
setSelectedId,
|
|
178
|
+
strokeColor,
|
|
179
|
+
setStrokeColor,
|
|
180
|
+
textColor,
|
|
181
|
+
setTextColor,
|
|
182
|
+
strokeWidth,
|
|
183
|
+
setStrokeWidth,
|
|
184
|
+
palette,
|
|
185
|
+
editingTextId,
|
|
186
|
+
setEditingTextId,
|
|
187
|
+
layout,
|
|
188
|
+
setLayout,
|
|
189
|
+
imageSize,
|
|
190
|
+
matrix,
|
|
191
|
+
matrixSV,
|
|
192
|
+
draw: {
|
|
193
|
+
active: drawActive,
|
|
194
|
+
start: drawStart,
|
|
195
|
+
current: drawCurrent,
|
|
196
|
+
points: drawPoints,
|
|
197
|
+
},
|
|
198
|
+
live: {
|
|
199
|
+
active: liveActive,
|
|
200
|
+
tx: liveTx,
|
|
201
|
+
ty: liveTy,
|
|
202
|
+
rotate: liveRotate,
|
|
203
|
+
scale: liveScale,
|
|
204
|
+
origin: liveOrigin,
|
|
205
|
+
},
|
|
206
|
+
cropRectSV,
|
|
207
|
+
}),
|
|
208
|
+
// Shared values are stable refs; only re-memoize on JS-state changes.
|
|
209
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
210
|
+
[
|
|
211
|
+
state,
|
|
212
|
+
doc,
|
|
213
|
+
tool,
|
|
214
|
+
selectedId,
|
|
215
|
+
strokeColor,
|
|
216
|
+
textColor,
|
|
217
|
+
strokeWidth,
|
|
218
|
+
palette,
|
|
219
|
+
editingTextId,
|
|
220
|
+
layout,
|
|
221
|
+
imageSize,
|
|
222
|
+
matrix,
|
|
223
|
+
]
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<EditorContext.Provider value={value}>{children}</EditorContext.Provider>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PaintStyle,
|
|
3
|
+
Skia,
|
|
4
|
+
StrokeCap,
|
|
5
|
+
StrokeJoin,
|
|
6
|
+
matchFont,
|
|
7
|
+
} from '@shopify/react-native-skia';
|
|
8
|
+
import type { SkCanvas, SkImage } from '@shopify/react-native-skia';
|
|
9
|
+
|
|
10
|
+
import type { Annotation } from '../types';
|
|
11
|
+
import { sortedByZ } from '../state/selectors';
|
|
12
|
+
import { annotationCenter, buildArrowPath, buildFreehandPath } from '../annotations/geometry';
|
|
13
|
+
import { withOpacity } from '../utils/color';
|
|
14
|
+
|
|
15
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Imperative draw of the base image + all annotations onto `canvas`, in IMAGE
|
|
19
|
+
* space. Shared by the off-screen export path. Mirrors the declarative on-screen
|
|
20
|
+
* renderers (same geometry, same rotation-about-center), so exports match the
|
|
21
|
+
* preview. The caller sets up the output transform + clip before calling this.
|
|
22
|
+
*/
|
|
23
|
+
export function drawScene(
|
|
24
|
+
canvas: SkCanvas,
|
|
25
|
+
image: SkImage,
|
|
26
|
+
annotations: Annotation[]
|
|
27
|
+
): void {
|
|
28
|
+
canvas.drawImage(image, 0, 0);
|
|
29
|
+
for (const a of sortedByZ(annotations)) {
|
|
30
|
+
drawAnnotation(canvas, a);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function strokePaint(color: string, width: number) {
|
|
35
|
+
const paint = Skia.Paint();
|
|
36
|
+
paint.setColor(Skia.Color(color));
|
|
37
|
+
paint.setStyle(PaintStyle.Stroke);
|
|
38
|
+
paint.setStrokeWidth(width);
|
|
39
|
+
paint.setAntiAlias(true);
|
|
40
|
+
paint.setStrokeCap(StrokeCap.Round);
|
|
41
|
+
paint.setStrokeJoin(StrokeJoin.Round);
|
|
42
|
+
return paint;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fillPaint(color: string) {
|
|
46
|
+
const paint = Skia.Paint();
|
|
47
|
+
paint.setColor(Skia.Color(color));
|
|
48
|
+
paint.setStyle(PaintStyle.Fill);
|
|
49
|
+
paint.setAntiAlias(true);
|
|
50
|
+
return paint;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function drawAnnotation(canvas: SkCanvas, a: Annotation): void {
|
|
54
|
+
const center = annotationCenter(a);
|
|
55
|
+
canvas.save();
|
|
56
|
+
if (a.rotation) {
|
|
57
|
+
canvas.translate(center.x, center.y);
|
|
58
|
+
canvas.rotate(a.rotation * RAD_TO_DEG, 0, 0);
|
|
59
|
+
canvas.translate(-center.x, -center.y);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
switch (a.type) {
|
|
63
|
+
case 'circle': {
|
|
64
|
+
if (a.fill) {
|
|
65
|
+
canvas.drawCircle(a.center.x, a.center.y, a.radius, fillPaint(a.fill));
|
|
66
|
+
}
|
|
67
|
+
canvas.drawCircle(
|
|
68
|
+
a.center.x,
|
|
69
|
+
a.center.y,
|
|
70
|
+
a.radius,
|
|
71
|
+
strokePaint(a.strokeColor, a.strokeWidth)
|
|
72
|
+
);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'arrow': {
|
|
76
|
+
const path = buildArrowPath(a.start, a.end, a.headSize);
|
|
77
|
+
canvas.drawPath(path, strokePaint(a.strokeColor, a.strokeWidth));
|
|
78
|
+
path.dispose?.();
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'marker': {
|
|
82
|
+
const rect = Skia.XYWHRect(
|
|
83
|
+
a.rect.x,
|
|
84
|
+
a.rect.y,
|
|
85
|
+
a.rect.width,
|
|
86
|
+
a.rect.height
|
|
87
|
+
);
|
|
88
|
+
canvas.drawRect(rect, fillPaint(withOpacity(a.color, a.opacity)));
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case 'freehand': {
|
|
92
|
+
const path = buildFreehandPath(a.points);
|
|
93
|
+
canvas.drawPath(path, strokePaint(a.strokeColor, a.strokeWidth));
|
|
94
|
+
path.dispose?.();
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case 'text': {
|
|
98
|
+
const font = matchFont({
|
|
99
|
+
fontFamily: 'sans-serif',
|
|
100
|
+
fontSize: a.fontSize,
|
|
101
|
+
fontStyle: 'normal',
|
|
102
|
+
fontWeight: 'normal',
|
|
103
|
+
});
|
|
104
|
+
const paint = fillPaint(a.color);
|
|
105
|
+
const lineHeight = a.fontSize * 1.2;
|
|
106
|
+
a.text.split('\n').forEach((line, i) => {
|
|
107
|
+
canvas.drawText(
|
|
108
|
+
line,
|
|
109
|
+
a.origin.x,
|
|
110
|
+
a.origin.y + a.fontSize + i * lineHeight,
|
|
111
|
+
paint,
|
|
112
|
+
font
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
canvas.restore();
|
|
120
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ClipOp, ImageFormat, Skia } from '@shopify/react-native-skia';
|
|
2
|
+
import type { SkImage } from '@shopify/react-native-skia';
|
|
3
|
+
|
|
4
|
+
import type { Annotation, ExportOptions, Rect, SceneTransform } from '../types';
|
|
5
|
+
import { clampSizeToMax } from '../utils/math';
|
|
6
|
+
import { drawScene } from './drawScene';
|
|
7
|
+
import { safeDispose } from '../image/disposeRegistry';
|
|
8
|
+
|
|
9
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
10
|
+
|
|
11
|
+
export interface ExportParams {
|
|
12
|
+
image: SkImage;
|
|
13
|
+
annotations: Annotation[];
|
|
14
|
+
scene: SceneTransform;
|
|
15
|
+
imageWidth: number;
|
|
16
|
+
imageHeight: number;
|
|
17
|
+
options?: ExportOptions;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Render the scene OFF-SCREEN at full resolution and return base64.
|
|
22
|
+
*
|
|
23
|
+
* Output = the (optionally cropped) image + annotations, scaled by `scene.scale`
|
|
24
|
+
* and rotated by `scene.rotation`, on a surface sized to the rotated content's
|
|
25
|
+
* bounding box (transparent corners for PNG). Because annotations live in image
|
|
26
|
+
* space, no per-annotation rescaling is needed — the exact preview geometry is
|
|
27
|
+
* drawn at native resolution.
|
|
28
|
+
*
|
|
29
|
+
* All Skia objects created here (surface + snapshot) are disposed before return.
|
|
30
|
+
*/
|
|
31
|
+
export async function exportImage(params: ExportParams): Promise<string> {
|
|
32
|
+
const { image, annotations, scene, imageWidth, imageHeight, options } = params;
|
|
33
|
+
const format = options?.format ?? 'png';
|
|
34
|
+
const quality = options?.quality ?? 100;
|
|
35
|
+
const dataUri = options?.dataUri ?? true;
|
|
36
|
+
|
|
37
|
+
const src: Rect = scene.cropRect ?? {
|
|
38
|
+
x: 0,
|
|
39
|
+
y: 0,
|
|
40
|
+
width: imageWidth,
|
|
41
|
+
height: imageHeight,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const contentW = src.width * scene.scale;
|
|
45
|
+
const contentH = src.height * scene.scale;
|
|
46
|
+
const absCos = Math.abs(Math.cos(scene.rotation));
|
|
47
|
+
const absSin = Math.abs(Math.sin(scene.rotation));
|
|
48
|
+
const rotatedW = contentW * absCos + contentH * absSin;
|
|
49
|
+
const rotatedH = contentW * absSin + contentH * absCos;
|
|
50
|
+
|
|
51
|
+
const clamped = clampSizeToMax(
|
|
52
|
+
Math.max(1, Math.ceil(rotatedW)),
|
|
53
|
+
Math.max(1, Math.ceil(rotatedH)),
|
|
54
|
+
options?.maxExportSize
|
|
55
|
+
);
|
|
56
|
+
// Reduce the drawing scale by the same factor the clamp shrank the surface.
|
|
57
|
+
const clampFactor = clamped.width / Math.max(1, Math.ceil(rotatedW));
|
|
58
|
+
const drawScale = scene.scale * clampFactor;
|
|
59
|
+
|
|
60
|
+
const surface = Skia.Surface.MakeOffscreen(clamped.width, clamped.height);
|
|
61
|
+
if (!surface) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'Skia.Surface.MakeOffscreen returned null — offscreen export unavailable on this device/version.'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let snapshot: SkImage | null = null;
|
|
68
|
+
try {
|
|
69
|
+
const canvas = surface.getCanvas();
|
|
70
|
+
const srcCenter = {
|
|
71
|
+
x: src.x + src.width / 2,
|
|
72
|
+
y: src.y + src.height / 2,
|
|
73
|
+
};
|
|
74
|
+
canvas.save();
|
|
75
|
+
canvas.translate(clamped.width / 2, clamped.height / 2);
|
|
76
|
+
if (scene.rotation) {
|
|
77
|
+
canvas.rotate(scene.rotation * RAD_TO_DEG, 0, 0);
|
|
78
|
+
}
|
|
79
|
+
canvas.scale(drawScale, drawScale);
|
|
80
|
+
canvas.translate(-srcCenter.x, -srcCenter.y);
|
|
81
|
+
// Clip to the crop region so only the selected area is drawn.
|
|
82
|
+
canvas.clipRect(
|
|
83
|
+
Skia.XYWHRect(src.x, src.y, src.width, src.height),
|
|
84
|
+
ClipOp.Intersect,
|
|
85
|
+
true
|
|
86
|
+
);
|
|
87
|
+
drawScene(canvas, image, annotations);
|
|
88
|
+
canvas.restore();
|
|
89
|
+
|
|
90
|
+
surface.flush();
|
|
91
|
+
snapshot = surface.makeImageSnapshot();
|
|
92
|
+
const skFormat = format === 'jpeg' ? ImageFormat.JPEG : ImageFormat.PNG;
|
|
93
|
+
const base64 = snapshot.encodeToBase64(skFormat, quality);
|
|
94
|
+
|
|
95
|
+
// File output: write the raw base64 via the caller-supplied writer.
|
|
96
|
+
if (options?.output === 'file') {
|
|
97
|
+
if (!options.filePath || !options.writeFile) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"exportImage: output:'file' requires both `filePath` and `writeFile`."
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
await options.writeFile(options.filePath, base64);
|
|
103
|
+
return options.filePath;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return dataUri ? `data:image/${format};base64,${base64}` : base64;
|
|
107
|
+
} finally {
|
|
108
|
+
safeDispose(snapshot);
|
|
109
|
+
safeDispose(surface);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Annotation, Vec2 } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface LiveValues {
|
|
4
|
+
tx: number;
|
|
5
|
+
ty: number;
|
|
6
|
+
rotate: number;
|
|
7
|
+
scale: number;
|
|
8
|
+
origin: Vec2;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Bake a live move/resize/rotate transform into an annotation's stored geometry.
|
|
13
|
+
* This MUST match the on-screen preview (AnnotationLayer applies, in order:
|
|
14
|
+
* translate → rotate(about origin) → scale(about origin)). Rotation is folded
|
|
15
|
+
* into the annotation's `rotation` field (also about its center), so only scale
|
|
16
|
+
* and translate touch geometry here.
|
|
17
|
+
*/
|
|
18
|
+
export function applyTransformToAnnotation(
|
|
19
|
+
a: Annotation,
|
|
20
|
+
live: LiveValues
|
|
21
|
+
): Annotation {
|
|
22
|
+
const { scale: s, origin: o, tx, ty, rotate } = live;
|
|
23
|
+
const map = (p: Vec2): Vec2 => ({
|
|
24
|
+
x: o.x + (p.x - o.x) * s + tx,
|
|
25
|
+
y: o.y + (p.y - o.y) * s + ty,
|
|
26
|
+
});
|
|
27
|
+
const rotation = a.rotation + rotate;
|
|
28
|
+
|
|
29
|
+
switch (a.type) {
|
|
30
|
+
case 'circle':
|
|
31
|
+
return {
|
|
32
|
+
...a,
|
|
33
|
+
center: map(a.center),
|
|
34
|
+
radius: a.radius * s,
|
|
35
|
+
strokeWidth: a.strokeWidth * s,
|
|
36
|
+
rotation,
|
|
37
|
+
};
|
|
38
|
+
case 'arrow':
|
|
39
|
+
return {
|
|
40
|
+
...a,
|
|
41
|
+
start: map(a.start),
|
|
42
|
+
end: map(a.end),
|
|
43
|
+
headSize: a.headSize * s,
|
|
44
|
+
strokeWidth: a.strokeWidth * s,
|
|
45
|
+
rotation,
|
|
46
|
+
};
|
|
47
|
+
case 'marker': {
|
|
48
|
+
const tl = map({ x: a.rect.x, y: a.rect.y });
|
|
49
|
+
return {
|
|
50
|
+
...a,
|
|
51
|
+
rect: {
|
|
52
|
+
x: tl.x,
|
|
53
|
+
y: tl.y,
|
|
54
|
+
width: a.rect.width * s,
|
|
55
|
+
height: a.rect.height * s,
|
|
56
|
+
},
|
|
57
|
+
rotation,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
case 'freehand':
|
|
61
|
+
return {
|
|
62
|
+
...a,
|
|
63
|
+
points: a.points.map(map),
|
|
64
|
+
strokeWidth: a.strokeWidth * s,
|
|
65
|
+
rotation,
|
|
66
|
+
};
|
|
67
|
+
case 'text':
|
|
68
|
+
return {
|
|
69
|
+
...a,
|
|
70
|
+
origin: map(a.origin),
|
|
71
|
+
fontSize: a.fontSize * s,
|
|
72
|
+
width: a.width * s,
|
|
73
|
+
rotation,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Annotation,
|
|
3
|
+
ColorString,
|
|
4
|
+
Rect,
|
|
5
|
+
TextAnnotation,
|
|
6
|
+
Vec2,
|
|
7
|
+
} from '../types';
|
|
8
|
+
import {
|
|
9
|
+
ARROW_HEAD_RATIO,
|
|
10
|
+
DEFAULT_FONT_SIZE,
|
|
11
|
+
DEFAULT_TEXT_WIDTH,
|
|
12
|
+
MARKER_OPACITY,
|
|
13
|
+
} from '../constants';
|
|
14
|
+
import { genId } from '../utils/id';
|
|
15
|
+
|
|
16
|
+
export interface DrawStyle {
|
|
17
|
+
strokeColor: ColorString;
|
|
18
|
+
strokeWidth: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function makeCircle(
|
|
22
|
+
center: Vec2,
|
|
23
|
+
radius: number,
|
|
24
|
+
style: DrawStyle
|
|
25
|
+
): Annotation {
|
|
26
|
+
return {
|
|
27
|
+
id: genId('circle'),
|
|
28
|
+
type: 'circle',
|
|
29
|
+
rotation: 0,
|
|
30
|
+
z: 0,
|
|
31
|
+
center,
|
|
32
|
+
radius,
|
|
33
|
+
strokeColor: style.strokeColor,
|
|
34
|
+
strokeWidth: style.strokeWidth,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function makeArrow(start: Vec2, end: Vec2, style: DrawStyle): Annotation {
|
|
39
|
+
return {
|
|
40
|
+
id: genId('arrow'),
|
|
41
|
+
type: 'arrow',
|
|
42
|
+
rotation: 0,
|
|
43
|
+
z: 0,
|
|
44
|
+
start,
|
|
45
|
+
end,
|
|
46
|
+
headSize: style.strokeWidth * ARROW_HEAD_RATIO,
|
|
47
|
+
strokeColor: style.strokeColor,
|
|
48
|
+
strokeWidth: style.strokeWidth,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function makeMarker(rect: Rect, color: ColorString): Annotation {
|
|
53
|
+
return {
|
|
54
|
+
id: genId('marker'),
|
|
55
|
+
type: 'marker',
|
|
56
|
+
rotation: 0,
|
|
57
|
+
z: 0,
|
|
58
|
+
rect,
|
|
59
|
+
color,
|
|
60
|
+
opacity: MARKER_OPACITY,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function makeFreehand(points: Vec2[], style: DrawStyle): Annotation {
|
|
65
|
+
return {
|
|
66
|
+
id: genId('freehand'),
|
|
67
|
+
type: 'freehand',
|
|
68
|
+
rotation: 0,
|
|
69
|
+
z: 0,
|
|
70
|
+
points,
|
|
71
|
+
strokeColor: style.strokeColor,
|
|
72
|
+
strokeWidth: style.strokeWidth,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function makeText(
|
|
77
|
+
origin: Vec2,
|
|
78
|
+
color: ColorString,
|
|
79
|
+
fontSize = DEFAULT_FONT_SIZE
|
|
80
|
+
): TextAnnotation {
|
|
81
|
+
return {
|
|
82
|
+
id: genId('text'),
|
|
83
|
+
type: 'text',
|
|
84
|
+
rotation: 0,
|
|
85
|
+
z: 0,
|
|
86
|
+
origin,
|
|
87
|
+
text: '',
|
|
88
|
+
color,
|
|
89
|
+
fontSize,
|
|
90
|
+
width: DEFAULT_TEXT_WIDTH,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Annotation, Vec2 } from '../types';
|
|
2
|
+
import { annotationBounds, annotationCenter } from '../annotations/geometryPure';
|
|
3
|
+
import { ROTATE_HANDLE_OFFSET } from '../constants';
|
|
4
|
+
import { applyToPoint, rotatePoint } from '../utils/math';
|
|
5
|
+
import type { Mat } from '../utils/math';
|
|
6
|
+
|
|
7
|
+
export interface SelectionHandles {
|
|
8
|
+
/** Corner handles in SCREEN space: top-left, top-right, bottom-right, bottom-left. */
|
|
9
|
+
corners: [Vec2, Vec2, Vec2, Vec2];
|
|
10
|
+
/** Rotate handle in screen space (above the top edge). */
|
|
11
|
+
rotate: Vec2;
|
|
12
|
+
/** Annotation center in screen space. */
|
|
13
|
+
center: Vec2;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compute selection-box handle positions in SCREEN space for annotation `a`,
|
|
18
|
+
* accounting for its rotation and the current image→screen matrix. Worklet-safe
|
|
19
|
+
* (used by the transform gesture) and also callable from JS (SelectionOverlay).
|
|
20
|
+
*/
|
|
21
|
+
export function selectionHandles(a: Annotation, matrix: Mat): SelectionHandles {
|
|
22
|
+
'worklet';
|
|
23
|
+
const b = annotationBounds(a);
|
|
24
|
+
const center = annotationCenter(a);
|
|
25
|
+
const localCorners: Vec2[] = [
|
|
26
|
+
{ x: b.x, y: b.y },
|
|
27
|
+
{ x: b.x + b.width, y: b.y },
|
|
28
|
+
{ x: b.x + b.width, y: b.y + b.height },
|
|
29
|
+
{ x: b.x, y: b.y + b.height },
|
|
30
|
+
];
|
|
31
|
+
const screenCorners = localCorners.map((c) =>
|
|
32
|
+
applyToPoint(matrix, rotatePoint(c, center, a.rotation))
|
|
33
|
+
) as [Vec2, Vec2, Vec2, Vec2];
|
|
34
|
+
|
|
35
|
+
const centerScreen = applyToPoint(matrix, center);
|
|
36
|
+
const topMid = {
|
|
37
|
+
x: (screenCorners[0].x + screenCorners[1].x) / 2,
|
|
38
|
+
y: (screenCorners[0].y + screenCorners[1].y) / 2,
|
|
39
|
+
};
|
|
40
|
+
// Push the rotate handle outward from center along the top-edge direction.
|
|
41
|
+
const dx = topMid.x - centerScreen.x;
|
|
42
|
+
const dy = topMid.y - centerScreen.y;
|
|
43
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
44
|
+
const rotate = {
|
|
45
|
+
x: topMid.x + (dx / len) * ROTATE_HANDLE_OFFSET,
|
|
46
|
+
y: topMid.y + (dy / len) * ROTATE_HANDLE_OFFSET,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return { corners: screenCorners, rotate, center: centerScreen };
|
|
50
|
+
}
|