@umituz/react-native-photo-editor 2.0.24 → 2.0.26
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 +42 -17
- package/src/domain/entities/Layer.entity.ts +138 -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 +90 -0
- package/src/domain/services/LayerService.ts +21 -23
- 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 +6 -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 +9 -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/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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History Service
|
|
3
|
+
* Undo/redo functionality for any state type
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface HistoryState<T> {
|
|
7
|
+
past: T[][];
|
|
8
|
+
present: T[];
|
|
9
|
+
future: T[][];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class HistoryService<T> {
|
|
13
|
+
constructor(private readonly maxSize: number = 20) {}
|
|
14
|
+
|
|
15
|
+
createInitialState(initialValue: T[]): HistoryState<T> {
|
|
16
|
+
return {
|
|
17
|
+
past: [],
|
|
18
|
+
present: initialValue,
|
|
19
|
+
future: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
push(history: HistoryState<T>, newValue: T[]): HistoryState<T> {
|
|
24
|
+
const past = [...history.past, history.present];
|
|
25
|
+
const trimmedPast = past.slice(-this.maxSize);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
past: trimmedPast,
|
|
29
|
+
present: newValue,
|
|
30
|
+
future: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
undo(history: HistoryState<T>): HistoryState<T> {
|
|
35
|
+
if (history.past.length === 0) return history;
|
|
36
|
+
|
|
37
|
+
const previous = history.past[history.past.length - 1];
|
|
38
|
+
const newPast = history.past.slice(0, -1);
|
|
39
|
+
const newFuture = [history.present, ...history.future];
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
past: newPast,
|
|
43
|
+
present: previous,
|
|
44
|
+
future: newFuture,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
redo(history: HistoryState<T>): HistoryState<T> {
|
|
49
|
+
if (history.future.length === 0) return history;
|
|
50
|
+
|
|
51
|
+
const next = history.future[0];
|
|
52
|
+
const newPast = [...history.past, history.present];
|
|
53
|
+
const newFuture = history.future.slice(1);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
past: newPast,
|
|
57
|
+
present: next,
|
|
58
|
+
future: newFuture,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
canUndo(history: HistoryState<T>): boolean {
|
|
63
|
+
return history.past.length > 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
canRedo(history: HistoryState<T>): boolean {
|
|
67
|
+
return history.future.length > 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -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 } 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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer Repository Service
|
|
3
|
+
* CRUD operations for layers with history support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Layer } from "../entities/Layer.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
|
+
// Type narrowing - only TextLayer and StickerLayer can be duplicated
|
|
52
|
+
if (!layer.isText() && !layer.isSticker()) {
|
|
53
|
+
return layers;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const duplicate = this.factory.duplicateLayer(layer, layers);
|
|
57
|
+
return [...layers, duplicate];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
reorderLayers(layers: Layer[], layerId: string, direction: "up" | "down"): Layer[] {
|
|
61
|
+
const sorted = this.sortByZIndex(layers);
|
|
62
|
+
const idx = sorted.findIndex((l) => l.id === layerId);
|
|
63
|
+
|
|
64
|
+
if (idx === -1) return layers;
|
|
65
|
+
if (direction === "up" && idx >= sorted.length - 1) return layers;
|
|
66
|
+
if (direction === "down" && idx <= 0) return layers;
|
|
67
|
+
|
|
68
|
+
const reordered = [...sorted];
|
|
69
|
+
const targetIdx = direction === "up" ? idx + 1 : idx - 1;
|
|
70
|
+
[reordered[idx], reordered[targetIdx]] = [reordered[targetIdx], reordered[idx]];
|
|
71
|
+
|
|
72
|
+
return this.reassignZIndex(reordered);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
sortByZIndex(layers: Layer[]): Layer[] {
|
|
76
|
+
return [...layers].sort((a, b) => a.appearance.zIndex - b.appearance.zIndex);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private getNextZIndex(layers: Layer[]): number {
|
|
80
|
+
return layers.length > 0 ? Math.max(...layers.map((l) => l.appearance.zIndex)) + 1 : 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private reassignZIndex(layers: Layer[]): Layer[] {
|
|
84
|
+
return layers.map((layer, i) => layer.withAppearance({ zIndex: i }));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Singleton instance
|
|
89
|
+
import { layerFactory } from "./LayerFactory.service";
|
|
90
|
+
export const layerRepository = new LayerRepository(layerFactory);
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
* Business logic for layer operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Layer
|
|
6
|
+
import { Layer } from "../entities/Layer.entity";
|
|
7
|
+
import { TextLayer } from "../entities/TextLayer.entity";
|
|
8
|
+
import { StickerLayer } from "../entities/StickerLayer.entity";
|
|
9
|
+
import type { TextContent } from "../types";
|
|
7
10
|
import type { Transform } from "../entities/Transform";
|
|
8
11
|
|
|
9
12
|
export class LayerService {
|
|
@@ -11,18 +14,10 @@ export class LayerService {
|
|
|
11
14
|
return `${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
createTextLayer(overrides: Partial<
|
|
17
|
+
createTextLayer(overrides: Partial<TextContent> = {}): TextLayer {
|
|
15
18
|
const id = this.generateId("text");
|
|
16
|
-
const
|
|
17
|
-
id,
|
|
18
|
-
type: "text",
|
|
19
|
+
const contentDefaults: TextContent = {
|
|
19
20
|
text: "",
|
|
20
|
-
x: 50,
|
|
21
|
-
y: 50,
|
|
22
|
-
rotation: 0,
|
|
23
|
-
scale: 1,
|
|
24
|
-
opacity: 1,
|
|
25
|
-
zIndex: 0,
|
|
26
21
|
fontSize: 32,
|
|
27
22
|
fontFamily: "System",
|
|
28
23
|
color: "#FFFFFF",
|
|
@@ -32,24 +27,27 @@ export class LayerService {
|
|
|
32
27
|
isItalic: false,
|
|
33
28
|
};
|
|
34
29
|
|
|
35
|
-
return new TextLayer({
|
|
30
|
+
return new TextLayer({
|
|
31
|
+
id,
|
|
32
|
+
position: { x: 50, y: 50 },
|
|
33
|
+
rotation: 0,
|
|
34
|
+
scale: 1,
|
|
35
|
+
appearance: { opacity: 1, zIndex: 0 },
|
|
36
|
+
content: { ...contentDefaults, ...overrides },
|
|
37
|
+
});
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
createStickerLayer(uri: string,
|
|
40
|
+
createStickerLayer(uri: string, _overrides: Record<string, never> = {}): StickerLayer {
|
|
39
41
|
const id = this.generateId("sticker");
|
|
40
|
-
|
|
42
|
+
|
|
43
|
+
return new StickerLayer({
|
|
41
44
|
id,
|
|
42
|
-
|
|
43
|
-
uri,
|
|
44
|
-
x: 100,
|
|
45
|
-
y: 100,
|
|
45
|
+
position: { x: 100, y: 100 },
|
|
46
46
|
rotation: 0,
|
|
47
47
|
scale: 1,
|
|
48
|
-
opacity: 1,
|
|
49
|
-
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return new StickerLayer({ ...defaults, ...overrides });
|
|
48
|
+
appearance: { opacity: 1, zIndex: 0 },
|
|
49
|
+
content: { uri },
|
|
50
|
+
});
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
updateLayer(layers: Layer[], layerId: string, updates: Partial<Transform>): Layer[] {
|
|
@@ -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 LayerDefaultsType = 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,20 @@ export { PhotoEditor } from "./PhotoEditor";
|
|
|
10
10
|
export type { PhotoEditorProps } from "./PhotoEditor";
|
|
11
11
|
|
|
12
12
|
// Domain entities
|
|
13
|
-
export type { Layer, TextLayer, StickerLayer } from "./domain/entities/Layer";
|
|
13
|
+
export type { Layer, TextLayer, StickerLayer } from "./domain/entities/Layer.entity";
|
|
14
|
+
export type { TextContent as TextLayerData, StickerContent as StickerLayerData } from "./domain/types";
|
|
15
|
+
export type { TextAlign } from "./domain/types";
|
|
14
16
|
export type { Transform } from "./domain/entities/Transform";
|
|
15
17
|
export type { FilterValues, FiltersVO } from "./domain/entities/Filters";
|
|
16
|
-
export { isTextLayer, isStickerLayer } from "./domain/entities/Layer";
|
|
18
|
+
export { isTextLayer, isStickerLayer } from "./domain/entities/Layer.entity";
|
|
17
19
|
|
|
18
20
|
// Application hooks
|
|
19
21
|
export { useEditor } from "./application/hooks/useEditor";
|
|
20
22
|
export { useEditorUI } from "./application/hooks/useEditorUI";
|
|
21
23
|
|
|
22
24
|
// Types & constants
|
|
23
|
-
export type {
|
|
24
|
-
export { DEFAULT_IMAGE_FILTERS } from "./
|
|
25
|
+
export type { TransformGestureState, TransformGestureConfig } from "./infrastructure/gesture/types";
|
|
26
|
+
export { DEFAULT_IMAGE_FILTERS } from "./types";
|
|
25
27
|
export { DEFAULT_FONTS, DEFAULT_TEXT_COLORS, DEFAULT_STICKERS, DEFAULT_AI_STYLES } from "./constants";
|
|
26
28
|
export type { FilterOption } from "./presentation/components/sheets/FilterSheet";
|
|
27
29
|
|