@umituz/react-native-photo-editor 2.0.23 → 2.0.25
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/package.json +1 -1
- package/src/PhotoEditor.tsx +43 -137
- package/src/application/hooks/useEditor.ts +4 -6
- package/src/application/hooks/useEditorUI.ts +8 -5
- package/src/application/stores/EditorStore.ts +17 -6
- package/src/domain/entities/Layer.entity.ts +86 -0
- package/src/domain/entities/{Layer.ts → Layer.legacy.ts} +3 -3
- package/src/domain/entities/StickerLayer.entity.ts +37 -0
- package/src/domain/entities/TextLayer.entity.ts +58 -0
- package/src/domain/entities/index.ts +9 -0
- package/src/domain/services/History.service.ts +69 -0
- package/src/domain/services/LayerFactory.service.ts +81 -0
- package/src/domain/services/LayerRepository.service.ts +85 -0
- package/src/domain/services/LayerService.ts +1 -1
- package/src/domain/types.ts +39 -0
- package/src/domain/value-objects/FilterSettings.vo.ts +89 -0
- package/src/domain/value-objects/LayerDefaults.vo.ts +56 -0
- package/src/domain/value-objects/Transform.vo.ts +61 -0
- package/src/domain/value-objects/index.ts +13 -0
- package/src/index.ts +4 -4
- package/src/infrastructure/gesture/createTransformGesture.ts +127 -0
- package/src/infrastructure/gesture/useTransformGesture.ts +7 -13
- package/src/presentation/components/DraggableLayer.tsx +13 -13
- package/src/presentation/components/EditorCanvas.tsx +5 -5
- package/src/presentation/components/EditorContent.tsx +72 -0
- package/src/presentation/components/EditorHeader.tsx +48 -0
- package/src/presentation/components/EditorSheets.tsx +85 -0
- package/src/presentation/components/FontControls.tsx +2 -2
- package/src/presentation/components/sheets/AdjustmentsSheet.tsx +4 -4
- package/src/presentation/components/sheets/FilterSheet.tsx +1 -1
- package/src/presentation/components/sheets/LayerManager.tsx +3 -4
- package/src/presentation/components/sheets/TextEditorSheet.tsx +1 -1
- package/src/types.ts +8 -18
- package/src/utils/constants.ts +84 -0
- package/src/utils/formatters.ts +29 -0
- package/src/utils/helpers.ts +51 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/validators.ts +38 -0
- package/ARCHITECTURE.md +0 -104
- package/MIGRATION.md +0 -100
- package/src/components/AIMagicSheet.tsx +0 -107
- package/src/components/AdjustmentsSheet.tsx +0 -108
- package/src/components/ColorPicker.tsx +0 -77
- package/src/components/DraggableSticker.tsx +0 -161
- package/src/components/DraggableText.tsx +0 -181
- package/src/components/EditorCanvas.tsx +0 -106
- package/src/components/EditorToolbar.tsx +0 -155
- package/src/components/FilterPicker.tsx +0 -73
- package/src/components/FontControls.tsx +0 -132
- package/src/components/LayerManager.tsx +0 -164
- package/src/components/Slider.tsx +0 -112
- package/src/components/StickerPicker.tsx +0 -47
- package/src/components/TextEditorSheet.tsx +0 -160
- package/src/core/HistoryManager.ts +0 -53
- package/src/hooks/usePhotoEditor.ts +0 -172
- package/src/hooks/usePhotoEditorUI.ts +0 -162
- package/src/infrastructure/history/HistoryManager.ts +0 -38
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Factory Service
|
|
3
|
+
* Centralized layer creation with defaults and validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TextLayer } from "../entities/TextLayer.entity";
|
|
7
|
+
import { StickerLayer } from "../entities/StickerLayer.entity";
|
|
8
|
+
import { LayerDefaults } from "../value-objects/LayerDefaults.vo";
|
|
9
|
+
import type { Position, Appearance, TextContent, StickerContent } from "../types";
|
|
10
|
+
|
|
11
|
+
export class LayerFactory {
|
|
12
|
+
createTextLayer(overrides?: Partial<Position & Appearance & TextContent>): TextLayer {
|
|
13
|
+
const id = LayerDefaults.createId("text");
|
|
14
|
+
|
|
15
|
+
return new TextLayer({
|
|
16
|
+
id,
|
|
17
|
+
position: { ...LayerDefaults.position, ...overrides },
|
|
18
|
+
rotation: LayerDefaults.transform.rotation,
|
|
19
|
+
scale: LayerDefaults.transform.scale,
|
|
20
|
+
appearance: { ...LayerDefaults.appearance, ...overrides },
|
|
21
|
+
content: {
|
|
22
|
+
...LayerDefaults.text,
|
|
23
|
+
...overrides,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
createStickerLayer(uri: string, overrides?: Partial<Position & Appearance>): StickerLayer {
|
|
29
|
+
const id = LayerDefaults.createId("sticker");
|
|
30
|
+
|
|
31
|
+
return new StickerLayer({
|
|
32
|
+
id,
|
|
33
|
+
position: { ...LayerDefaults.position, ...overrides },
|
|
34
|
+
rotation: LayerDefaults.transform.rotation,
|
|
35
|
+
scale: LayerDefaults.transform.scale,
|
|
36
|
+
appearance: { ...LayerDefaults.appearance, ...overrides },
|
|
37
|
+
content: {
|
|
38
|
+
uri,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
duplicateLayer<T extends TextLayer | StickerLayer>(
|
|
44
|
+
layer: T,
|
|
45
|
+
currentLayers: { zIndex: number }[]
|
|
46
|
+
): T {
|
|
47
|
+
const nextZIndex = LayerDefaults.getNextZIndex(currentLayers);
|
|
48
|
+
const offset = { x: 20, y: 20 };
|
|
49
|
+
|
|
50
|
+
if (layer instanceof TextLayer) {
|
|
51
|
+
return new TextLayer({
|
|
52
|
+
...layer,
|
|
53
|
+
id: LayerDefaults.createId("text"),
|
|
54
|
+
position: {
|
|
55
|
+
x: layer.position.x + offset.x,
|
|
56
|
+
y: layer.position.y + offset.y,
|
|
57
|
+
},
|
|
58
|
+
appearance: {
|
|
59
|
+
...layer.appearance,
|
|
60
|
+
zIndex: nextZIndex,
|
|
61
|
+
},
|
|
62
|
+
}) as T;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return new StickerLayer({
|
|
66
|
+
...layer,
|
|
67
|
+
id: LayerDefaults.createId("sticker"),
|
|
68
|
+
position: {
|
|
69
|
+
x: layer.position.x + offset.x,
|
|
70
|
+
y: layer.position.y + offset.y,
|
|
71
|
+
},
|
|
72
|
+
appearance: {
|
|
73
|
+
...layer.appearance,
|
|
74
|
+
zIndex: nextZIndex,
|
|
75
|
+
},
|
|
76
|
+
}) as T;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Singleton instance
|
|
81
|
+
export const layerFactory = new LayerFactory();
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Repository Service
|
|
3
|
+
* CRUD operations for layers with history support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Layer } from "../entities/Layer.entity".entity";
|
|
7
|
+
import { LayerFactory } from "./LayerFactory.service";
|
|
8
|
+
|
|
9
|
+
export class LayerRepository {
|
|
10
|
+
constructor(private factory: LayerFactory) {}
|
|
11
|
+
|
|
12
|
+
createLayer(
|
|
13
|
+
type: "text" | "sticker",
|
|
14
|
+
content: string,
|
|
15
|
+
currentLayers: Layer[]
|
|
16
|
+
): Layer {
|
|
17
|
+
if (type === "text") {
|
|
18
|
+
return this.factory.createTextLayer({
|
|
19
|
+
zIndex: this.getNextZIndex(currentLayers),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return this.factory.createStickerLayer(content || "", {
|
|
24
|
+
zIndex: this.getNextZIndex(currentLayers),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
updateLayer(
|
|
29
|
+
layers: Layer[],
|
|
30
|
+
layerId: string,
|
|
31
|
+
updates: Partial<{ position: { x: number; y: number }; rotation: number; scale: number }>
|
|
32
|
+
): Layer[] {
|
|
33
|
+
return layers.map((layer) =>
|
|
34
|
+
layer.id === layerId
|
|
35
|
+
? layer.withPosition(updates.position || {}).withTransform({
|
|
36
|
+
rotation: updates.rotation,
|
|
37
|
+
scale: updates.scale,
|
|
38
|
+
})
|
|
39
|
+
: layer
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
deleteLayer(layers: Layer[], layerId: string): Layer[] {
|
|
44
|
+
return layers.filter((layer) => layer.id !== layerId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
duplicateLayer(layers: Layer[], layerId: string): Layer[] {
|
|
48
|
+
const layer = layers.find((l) => l.id === layerId);
|
|
49
|
+
if (!layer) return layers;
|
|
50
|
+
|
|
51
|
+
const duplicate = this.factory.duplicateLayer(layer, layers);
|
|
52
|
+
return [...layers, duplicate];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
reorderLayers(layers: Layer[], layerId: string, direction: "up" | "down"): Layer[] {
|
|
56
|
+
const sorted = this.sortByZIndex(layers);
|
|
57
|
+
const idx = sorted.findIndex((l) => l.id === layerId);
|
|
58
|
+
|
|
59
|
+
if (idx === -1) return layers;
|
|
60
|
+
if (direction === "up" && idx >= sorted.length - 1) return layers;
|
|
61
|
+
if (direction === "down" && idx <= 0) return layers;
|
|
62
|
+
|
|
63
|
+
const reordered = [...sorted];
|
|
64
|
+
const targetIdx = direction === "up" ? idx + 1 : idx - 1;
|
|
65
|
+
[reordered[idx], reordered[targetIdx]] = [reordered[targetIdx], reordered[idx]];
|
|
66
|
+
|
|
67
|
+
return this.reassignZIndex(reordered);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
sortByZIndex(layers: Layer[]): Layer[] {
|
|
71
|
+
return [...layers].sort((a, b) => a.appearance.zIndex - b.appearance.zIndex);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private getNextZIndex(layers: Layer[]): number {
|
|
75
|
+
return layers.length > 0 ? Math.max(...layers.map((l) => l.appearance.zIndex)) + 1 : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private reassignZIndex(layers: Layer[]): Layer[] {
|
|
79
|
+
return layers.map((layer, i) => layer.withAppearance({ zIndex: i }));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Singleton instance
|
|
84
|
+
import { layerFactory } from "./LayerFactory.service";
|
|
85
|
+
export const layerRepository = new LayerRepository(layerFactory);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Business logic for layer operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Layer, TextLayer, StickerLayer,
|
|
6
|
+
import { Layer, TextLayer, StickerLayer, type TextLayerData, type StickerLayerData } from "../entities/Layer.entity"";
|
|
7
7
|
import type { Transform } from "../entities/Transform";
|
|
8
8
|
|
|
9
9
|
export class LayerService {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Domain Types
|
|
3
|
+
* Central type definitions for the photo editor domain
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LayerType = "text" | "sticker";
|
|
7
|
+
export type TextAlign = "left" | "center" | "right";
|
|
8
|
+
|
|
9
|
+
export interface Position {
|
|
10
|
+
readonly x: number;
|
|
11
|
+
readonly y: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Dimensions {
|
|
15
|
+
readonly width?: number;
|
|
16
|
+
readonly height?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Appearance {
|
|
20
|
+
readonly opacity: number;
|
|
21
|
+
readonly zIndex: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TextContent {
|
|
25
|
+
readonly text: string;
|
|
26
|
+
readonly fontSize: number;
|
|
27
|
+
readonly fontFamily: string;
|
|
28
|
+
readonly color: string;
|
|
29
|
+
readonly backgroundColor: string;
|
|
30
|
+
readonly textAlign: TextAlign;
|
|
31
|
+
readonly isBold?: boolean;
|
|
32
|
+
readonly isItalic?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StickerContent {
|
|
36
|
+
readonly uri: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type LayerContent = TextContent | StickerContent;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter Settings Value Object
|
|
3
|
+
* Immutable image filter adjustments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface FilterData {
|
|
7
|
+
readonly brightness: number;
|
|
8
|
+
readonly contrast: number;
|
|
9
|
+
readonly saturation: number;
|
|
10
|
+
readonly sepia: number;
|
|
11
|
+
readonly grayscale: number;
|
|
12
|
+
readonly hueRotate?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class FilterSettings {
|
|
16
|
+
readonly brightness: number;
|
|
17
|
+
readonly contrast: number;
|
|
18
|
+
readonly saturation: number;
|
|
19
|
+
readonly sepia: number;
|
|
20
|
+
readonly grayscale: number;
|
|
21
|
+
readonly hueRotate?: number;
|
|
22
|
+
|
|
23
|
+
constructor(data: FilterData) {
|
|
24
|
+
this.brightness = data.brightness;
|
|
25
|
+
this.contrast = data.contrast;
|
|
26
|
+
this.saturation = data.saturation;
|
|
27
|
+
this.sepia = data.sepia;
|
|
28
|
+
this.grayscale = data.grayscale;
|
|
29
|
+
this.hueRotate = data.hueRotate;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static readonly DEFAULT = new FilterSettings({
|
|
33
|
+
brightness: 1,
|
|
34
|
+
contrast: 1,
|
|
35
|
+
saturation: 1,
|
|
36
|
+
sepia: 0,
|
|
37
|
+
grayscale: 0,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
withBrightness(brightness: number): FilterSettings {
|
|
41
|
+
return new FilterSettings({ ...this, brightness });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
withContrast(contrast: number): FilterSettings {
|
|
45
|
+
return new FilterSettings({ ...this, contrast });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
withSaturation(saturation: number): FilterSettings {
|
|
49
|
+
return new FilterSettings({ ...this, saturation });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
withSepia(sepia: number): FilterSettings {
|
|
53
|
+
return new FilterSettings({ ...this, sepia });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
withGrayscale(grayscale: number): FilterSettings {
|
|
57
|
+
return new FilterSettings({ ...this, grayscale });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
withHueRotate(hueRotate: number): FilterSettings {
|
|
61
|
+
return new FilterSettings({ ...this, hueRotate });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
reset(): FilterSettings {
|
|
65
|
+
return FilterSettings.DEFAULT;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
toJSON(): FilterData {
|
|
69
|
+
return {
|
|
70
|
+
brightness: this.brightness,
|
|
71
|
+
contrast: this.contrast,
|
|
72
|
+
saturation: this.saturation,
|
|
73
|
+
sepia: this.sepia,
|
|
74
|
+
grayscale: this.grayscale,
|
|
75
|
+
hueRotate: this.hueRotate,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
toRecord(): Record<string, number> {
|
|
80
|
+
return {
|
|
81
|
+
brightness: this.brightness,
|
|
82
|
+
contrast: this.contrast,
|
|
83
|
+
saturation: this.saturation,
|
|
84
|
+
sepia: this.sepia,
|
|
85
|
+
grayscale: this.grayscale,
|
|
86
|
+
...(this.hueRotate !== undefined && { hueRotate: this.hueRotate }),
|
|
87
|
+
} as Record<string, number>;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Defaults Value Object
|
|
3
|
+
* Centralized default values for layer creation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TextAlign } from "../types";
|
|
7
|
+
|
|
8
|
+
export const LayerDefaults = {
|
|
9
|
+
// Position defaults
|
|
10
|
+
position: {
|
|
11
|
+
x: 50,
|
|
12
|
+
y: 50,
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
// Transform defaults
|
|
16
|
+
transform: {
|
|
17
|
+
rotation: 0,
|
|
18
|
+
scale: 1,
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// Appearance defaults
|
|
22
|
+
appearance: {
|
|
23
|
+
opacity: 1,
|
|
24
|
+
zIndex: 0,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// Text layer defaults
|
|
28
|
+
text: {
|
|
29
|
+
text: "",
|
|
30
|
+
fontSize: 32,
|
|
31
|
+
fontFamily: "System",
|
|
32
|
+
color: "#FFFFFF",
|
|
33
|
+
backgroundColor: "transparent",
|
|
34
|
+
textAlign: "center" as TextAlign,
|
|
35
|
+
isBold: false,
|
|
36
|
+
isItalic: false,
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Sticker layer defaults
|
|
40
|
+
sticker: {
|
|
41
|
+
uri: "",
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// Utility functions
|
|
45
|
+
createId(type: "text" | "sticker"): string {
|
|
46
|
+
return `${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
getNextZIndex(currentLayers: { zIndex: number }[]): number {
|
|
50
|
+
return currentLayers.length > 0
|
|
51
|
+
? Math.max(...currentLayers.map((l) => l.zIndex)) + 1
|
|
52
|
+
: 0;
|
|
53
|
+
},
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
export type LayerDefaults = typeof LayerDefaults;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Value Object
|
|
3
|
+
* Immutable position, scale, and rotation state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface TransformData {
|
|
7
|
+
readonly x: number;
|
|
8
|
+
readonly y: number;
|
|
9
|
+
readonly rotation: number;
|
|
10
|
+
readonly scale: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Transform {
|
|
14
|
+
readonly x: number;
|
|
15
|
+
readonly y: number;
|
|
16
|
+
readonly rotation: number;
|
|
17
|
+
readonly scale: number;
|
|
18
|
+
|
|
19
|
+
constructor(data: TransformData) {
|
|
20
|
+
this.x = data.x;
|
|
21
|
+
this.y = data.y;
|
|
22
|
+
this.rotation = data.rotation;
|
|
23
|
+
this.scale = data.scale;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static readonly DEFAULT = new Transform({
|
|
27
|
+
x: 50,
|
|
28
|
+
y: 50,
|
|
29
|
+
rotation: 0,
|
|
30
|
+
scale: 1,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
withX(x: number): Transform {
|
|
34
|
+
return new Transform({ ...this, x });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
withY(y: number): Transform {
|
|
38
|
+
return new Transform({ ...this, y });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
withPosition(position: { x: number; y: number }): Transform {
|
|
42
|
+
return new Transform({ ...this, ...position });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
withRotation(rotation: number): Transform {
|
|
46
|
+
return new Transform({ ...this, rotation });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
withScale(scale: number): Transform {
|
|
50
|
+
return new Transform({ ...this, scale });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toJSON(): TransformData {
|
|
54
|
+
return {
|
|
55
|
+
x: this.x,
|
|
56
|
+
y: this.y,
|
|
57
|
+
rotation: this.rotation,
|
|
58
|
+
scale: this.scale,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value Objects Export
|
|
3
|
+
* Immutable value objects for domain modeling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { Transform } from "./Transform.vo";
|
|
7
|
+
export type { TransformData } from "./Transform.vo";
|
|
8
|
+
|
|
9
|
+
export { FilterSettings } from "./FilterSettings.vo";
|
|
10
|
+
export type { FilterData } from "./FilterSettings.vo";
|
|
11
|
+
|
|
12
|
+
export { LayerDefaults } from "./LayerDefaults.vo";
|
|
13
|
+
export type { LayerDefaults as LayerDefaultsType } from "./LayerDefaults.vo";
|
package/src/index.ts
CHANGED
|
@@ -10,18 +10,18 @@ export { PhotoEditor } from "./PhotoEditor";
|
|
|
10
10
|
export type { PhotoEditorProps } from "./PhotoEditor";
|
|
11
11
|
|
|
12
12
|
// Domain entities
|
|
13
|
-
export type { Layer, TextLayer, StickerLayer } from "
|
|
13
|
+
export type { Layer, TextLayer, StickerLayer } from "../entities/Layer.entity"";
|
|
14
14
|
export type { Transform } from "./domain/entities/Transform";
|
|
15
15
|
export type { FilterValues, FiltersVO } from "./domain/entities/Filters";
|
|
16
|
-
export { isTextLayer, isStickerLayer } from "
|
|
16
|
+
export { isTextLayer, isStickerLayer } from "../entities/Layer.entity"";
|
|
17
17
|
|
|
18
18
|
// Application hooks
|
|
19
19
|
export { useEditor } from "./application/hooks/useEditor";
|
|
20
20
|
export { useEditorUI } from "./application/hooks/useEditorUI";
|
|
21
21
|
|
|
22
22
|
// Types & constants
|
|
23
|
-
export type {
|
|
24
|
-
export { DEFAULT_IMAGE_FILTERS } from "./
|
|
23
|
+
export type { TransformGestureState, TransformGestureConfig } from "./infrastructure/gesture/types";
|
|
24
|
+
export { DEFAULT_IMAGE_FILTERS } from "./types";
|
|
25
25
|
export { DEFAULT_FONTS, DEFAULT_TEXT_COLORS, DEFAULT_STICKERS, DEFAULT_AI_STYLES } from "./constants";
|
|
26
26
|
export type { FilterOption } from "./presentation/components/sheets/FilterSheet";
|
|
27
27
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Gesture Utility
|
|
3
|
+
* Reusable gesture logic for draggable layers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
7
|
+
import { Gesture } from "react-native-gesture-handler";
|
|
8
|
+
import type { Layer } from "../entities/Layer.entity".entity";
|
|
9
|
+
|
|
10
|
+
export interface TransformGestureState {
|
|
11
|
+
position: { x: number; y: number };
|
|
12
|
+
scale: number;
|
|
13
|
+
rotation: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TransformGestureConfig {
|
|
17
|
+
minScale?: number;
|
|
18
|
+
maxScale?: number;
|
|
19
|
+
onTransformEnd: (transform: { x: number; y: number; scale: number; rotation: number }) => void;
|
|
20
|
+
onPress?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useTransformGesture(
|
|
24
|
+
initialTransform: Pick<Layer, "position" | "rotation" | "scale">,
|
|
25
|
+
config: TransformGestureConfig
|
|
26
|
+
) {
|
|
27
|
+
const {
|
|
28
|
+
minScale = 0.2,
|
|
29
|
+
maxScale = 6,
|
|
30
|
+
onTransformEnd,
|
|
31
|
+
onPress,
|
|
32
|
+
} = config;
|
|
33
|
+
|
|
34
|
+
const [state, setState] = useState<TransformGestureState>(() => ({
|
|
35
|
+
position: { x: initialTransform.position.x, y: initialTransform.position.y },
|
|
36
|
+
scale: initialTransform.scale,
|
|
37
|
+
rotation: initialTransform.rotation,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const stateRef = useRef(state);
|
|
41
|
+
stateRef.current = state;
|
|
42
|
+
|
|
43
|
+
const onTransformEndRef = useRef(onTransformEnd);
|
|
44
|
+
onTransformEndRef.current = onTransformEnd;
|
|
45
|
+
const onPressRef = useRef(onPress);
|
|
46
|
+
onPressRef.current = onPress;
|
|
47
|
+
|
|
48
|
+
const offsetRef = useRef(state.position);
|
|
49
|
+
const scaleStartRef = useRef(state.scale);
|
|
50
|
+
const rotationStartRef = useRef(state.rotation);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
setState((prev) => ({
|
|
54
|
+
...prev,
|
|
55
|
+
position: {
|
|
56
|
+
x: initialTransform.position.x ?? prev.position.x,
|
|
57
|
+
y: initialTransform.position.y ?? prev.position.y,
|
|
58
|
+
},
|
|
59
|
+
scale: initialTransform.scale ?? prev.scale,
|
|
60
|
+
rotation: initialTransform.rotation ?? prev.rotation,
|
|
61
|
+
}));
|
|
62
|
+
}, [initialTransform.position.x, initialTransform.position.y, initialTransform.scale, initialTransform.rotation]);
|
|
63
|
+
|
|
64
|
+
const emitTransform = useCallback(() => {
|
|
65
|
+
onTransformEndRef.current({
|
|
66
|
+
x: stateRef.current.position.x,
|
|
67
|
+
y: stateRef.current.position.y,
|
|
68
|
+
scale: stateRef.current.scale,
|
|
69
|
+
rotation: stateRef.current.rotation,
|
|
70
|
+
});
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const panGesture = Gesture.Pan()
|
|
74
|
+
.runOnJS(true)
|
|
75
|
+
.averageTouches(true)
|
|
76
|
+
.onStart(() => {
|
|
77
|
+
offsetRef.current = stateRef.current.position;
|
|
78
|
+
})
|
|
79
|
+
.onUpdate((e: { translationX: number; translationY: number }) => {
|
|
80
|
+
setState({
|
|
81
|
+
...stateRef.current,
|
|
82
|
+
position: {
|
|
83
|
+
x: offsetRef.current.x + e.translationX,
|
|
84
|
+
y: offsetRef.current.y + e.translationY,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
})
|
|
88
|
+
.onEnd(emitTransform);
|
|
89
|
+
|
|
90
|
+
const pinchGesture = Gesture.Pinch()
|
|
91
|
+
.runOnJS(true)
|
|
92
|
+
.onStart(() => {
|
|
93
|
+
scaleStartRef.current = stateRef.current.scale;
|
|
94
|
+
})
|
|
95
|
+
.onUpdate((e: { scale: number }) => {
|
|
96
|
+
setState({
|
|
97
|
+
...stateRef.current,
|
|
98
|
+
scale: Math.max(minScale, Math.min(maxScale, scaleStartRef.current * e.scale)),
|
|
99
|
+
});
|
|
100
|
+
})
|
|
101
|
+
.onEnd(emitTransform);
|
|
102
|
+
|
|
103
|
+
const rotationGesture = Gesture.Rotation()
|
|
104
|
+
.runOnJS(true)
|
|
105
|
+
.onStart(() => {
|
|
106
|
+
rotationStartRef.current = stateRef.current.rotation;
|
|
107
|
+
})
|
|
108
|
+
.onUpdate((e: { rotation: number }) => {
|
|
109
|
+
setState({
|
|
110
|
+
...stateRef.current,
|
|
111
|
+
rotation: rotationStartRef.current + (e.rotation * 180) / Math.PI,
|
|
112
|
+
});
|
|
113
|
+
})
|
|
114
|
+
.onEnd(emitTransform);
|
|
115
|
+
|
|
116
|
+
const tapGesture = Gesture.Tap()
|
|
117
|
+
.runOnJS(true)
|
|
118
|
+
.onEnd(() => onPressRef.current?.());
|
|
119
|
+
|
|
120
|
+
const composed = Gesture.Exclusive(
|
|
121
|
+
Gesture.Simultaneous(panGesture, pinchGesture, rotationGesture),
|
|
122
|
+
tapGesture
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return { state, gestures: { pan: panGesture, pinch: pinchGesture, rotation: rotationGesture, tap: tapGesture, composed } };
|
|
126
|
+
}
|
|
127
|
+
|
|
@@ -9,12 +9,6 @@ import { Gesture } from "react-native-gesture-handler";
|
|
|
9
9
|
import type { Transform } from "../../domain/entities/Transform";
|
|
10
10
|
import type { TransformGestureConfig, TransformGestureState } from "./types";
|
|
11
11
|
|
|
12
|
-
const DEFAULT_STATE: TransformGestureState = {
|
|
13
|
-
position: { x: 50, y: 50 },
|
|
14
|
-
scale: 1,
|
|
15
|
-
rotation: 0,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
12
|
export function useTransformGesture(
|
|
19
13
|
initialTransform: Partial<Transform>,
|
|
20
14
|
config: TransformGestureConfig
|
|
@@ -35,7 +29,7 @@ export function useTransformGesture(
|
|
|
35
29
|
|
|
36
30
|
// Sync state when props change (undo/redo)
|
|
37
31
|
useEffect(() => {
|
|
38
|
-
setState(prev => ({
|
|
32
|
+
setState((prev: TransformGestureState) => ({
|
|
39
33
|
...prev,
|
|
40
34
|
position: { x: initialTransform.x ?? prev.position.x, y: initialTransform.y ?? prev.position.y },
|
|
41
35
|
scale: initialTransform.scale ?? prev.scale,
|
|
@@ -73,8 +67,8 @@ export function useTransformGesture(
|
|
|
73
67
|
.onStart(() => {
|
|
74
68
|
offsetRef.current = stateRef.current.position;
|
|
75
69
|
})
|
|
76
|
-
.onUpdate((e) => {
|
|
77
|
-
setState(prev => ({
|
|
70
|
+
.onUpdate((e: { translationX: number; translationY: number }) => {
|
|
71
|
+
setState((prev: TransformGestureState) => ({
|
|
78
72
|
...prev,
|
|
79
73
|
position: {
|
|
80
74
|
x: offsetRef.current.x + e.translationX,
|
|
@@ -90,8 +84,8 @@ export function useTransformGesture(
|
|
|
90
84
|
.onStart(() => {
|
|
91
85
|
scaleStartRef.current = stateRef.current.scale;
|
|
92
86
|
})
|
|
93
|
-
.onUpdate((e) => {
|
|
94
|
-
setState(prev => ({
|
|
87
|
+
.onUpdate((e: { scale: number }) => {
|
|
88
|
+
setState((prev: TransformGestureState) => ({
|
|
95
89
|
...prev,
|
|
96
90
|
scale: Math.max(minScale, Math.min(maxScale, scaleStartRef.current * e.scale)),
|
|
97
91
|
}));
|
|
@@ -104,8 +98,8 @@ export function useTransformGesture(
|
|
|
104
98
|
.onStart(() => {
|
|
105
99
|
rotationStartRef.current = stateRef.current.rotation;
|
|
106
100
|
})
|
|
107
|
-
.onUpdate((e) => {
|
|
108
|
-
setState(prev => ({
|
|
101
|
+
.onUpdate((e: { rotation: number }) => {
|
|
102
|
+
setState((prev: TransformGestureState) => ({
|
|
109
103
|
...prev,
|
|
110
104
|
rotation: rotationStartRef.current + (e.rotation * 180) / Math.PI,
|
|
111
105
|
}));
|