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,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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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
|
+
];
|