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
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
|
+
});
|