@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
package/package.json
CHANGED
package/src/PhotoEditor.tsx
CHANGED
|
@@ -3,31 +3,25 @@
|
|
|
3
3
|
* Main entry point for the photo editor
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, {
|
|
7
|
-
import { View
|
|
8
|
-
import { BottomSheetModal } from "@umituz/react-native-design-system/molecules";
|
|
9
|
-
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
10
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View } from "react-native";
|
|
11
8
|
import { useSafeAreaInsets } from "@umituz/react-native-design-system/safe-area";
|
|
12
|
-
|
|
13
|
-
import {
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
|
+
import { EditorHeader } from "./presentation/components/EditorHeader";
|
|
11
|
+
import { EditorContent } from "./presentation/components/EditorContent";
|
|
12
|
+
import { EditorSheets } from "./presentation/components/EditorSheets";
|
|
14
13
|
import { EditorToolbar } from "./presentation/components/EditorToolbar";
|
|
15
|
-
import { FontControls } from "./presentation/components/FontControls";
|
|
16
|
-
import { TextEditorSheet } from "./presentation/components/sheets/TextEditorSheet";
|
|
17
|
-
import { StickerPicker } from "./presentation/components/sheets/StickerPicker";
|
|
18
|
-
import { FilterSheet } from "./presentation/components/sheets/FilterSheet";
|
|
19
|
-
import { AdjustmentsSheet } from "./presentation/components/sheets/AdjustmentsSheet";
|
|
20
|
-
import { LayerManager } from "./presentation/components/sheets/LayerManager";
|
|
21
|
-
import { AIMagicSheet } from "./presentation/components/sheets/AIMagicSheet";
|
|
22
14
|
import { useEditorUI } from "./application/hooks/useEditorUI";
|
|
23
|
-
import { DEFAULT_FONTS } from "./constants";
|
|
15
|
+
import { DEFAULT_FONTS } from "./utils/constants";
|
|
16
|
+
import type { Layer } from "./domain/entities/Layer.entity";
|
|
17
|
+
import type { FilterSettings } from "./domain/value-objects/FilterSettings.vo";
|
|
24
18
|
|
|
25
19
|
export interface PhotoEditorProps {
|
|
26
20
|
imageUri: string;
|
|
27
|
-
onSave?: (uri: string, layers:
|
|
21
|
+
onSave?: (uri: string, layers: Layer[], filters: FilterSettings) => void;
|
|
28
22
|
onClose: () => void;
|
|
29
23
|
title?: string;
|
|
30
|
-
customTools?: React.ReactNode | ((ui:
|
|
24
|
+
customTools?: React.ReactNode | ((ui: unknown) => React.ReactNode);
|
|
31
25
|
initialCaption?: string;
|
|
32
26
|
t: (key: string) => string;
|
|
33
27
|
fonts?: readonly string[];
|
|
@@ -49,73 +43,42 @@ export function PhotoEditor({
|
|
|
49
43
|
const insets = useSafeAreaInsets();
|
|
50
44
|
const ui = useEditorUI(initialCaption);
|
|
51
45
|
|
|
52
|
-
const styles = useMemo(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
justifyContent: "space-between" as const,
|
|
62
|
-
paddingHorizontal: tokens.spacing.md,
|
|
63
|
-
paddingVertical: tokens.spacing.sm,
|
|
64
|
-
borderBottomWidth: 1,
|
|
65
|
-
borderBottomColor: tokens.colors.border,
|
|
66
|
-
},
|
|
67
|
-
headerTitle: {
|
|
68
|
-
flex: 1,
|
|
69
|
-
textAlign: "center",
|
|
70
|
-
},
|
|
71
|
-
scrollContent: {
|
|
72
|
-
padding: tokens.spacing.md,
|
|
73
|
-
gap: tokens.spacing.md,
|
|
74
|
-
},
|
|
75
|
-
}), [tokens, insets]);
|
|
76
|
-
|
|
77
|
-
const handleSave = useCallback(
|
|
78
|
-
() => onSave?.(imageUri, ui.layers.map(l => l.toJSON()), ui.filters),
|
|
79
|
-
[onSave, imageUri, ui.layers, ui.filters],
|
|
46
|
+
const styles = useMemo(
|
|
47
|
+
() => ({
|
|
48
|
+
container: {
|
|
49
|
+
flex: 1,
|
|
50
|
+
backgroundColor: tokens.colors.surface,
|
|
51
|
+
paddingTop: insets.top,
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
[tokens, insets]
|
|
80
55
|
);
|
|
81
56
|
|
|
57
|
+
const handleSave = () =>
|
|
58
|
+
onSave?.(imageUri, ui.layers, ui.filters);
|
|
59
|
+
|
|
82
60
|
return (
|
|
83
61
|
<View style={styles.container}>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
{title}
|
|
91
|
-
</AtomicText>
|
|
92
|
-
<TouchableOpacity onPress={handleSave} accessibilityLabel="Save" accessibilityRole="button">
|
|
93
|
-
<AtomicText fontWeight="bold" color="primary">
|
|
94
|
-
{t("common.save") || "Save"}
|
|
95
|
-
</AtomicText>
|
|
96
|
-
</TouchableOpacity>
|
|
97
|
-
</View>
|
|
98
|
-
|
|
99
|
-
<ScrollView contentContainerStyle={styles.scrollContent}>
|
|
100
|
-
<EditorCanvas
|
|
101
|
-
imageUrl={imageUri}
|
|
102
|
-
layers={ui.layers}
|
|
103
|
-
activeLayerId={ui.activeLayerId}
|
|
104
|
-
filters={ui.filters}
|
|
105
|
-
onLayerTap={ui.handleTextLayerTap}
|
|
106
|
-
onLayerTransform={ui.handleLayerTransform}
|
|
107
|
-
/>
|
|
108
|
-
|
|
109
|
-
{typeof customTools === "function" ? customTools(ui) : customTools}
|
|
62
|
+
<EditorHeader
|
|
63
|
+
title={title}
|
|
64
|
+
saveLabel={t("common.save") || "Save"}
|
|
65
|
+
onClose={onClose}
|
|
66
|
+
onSave={handleSave}
|
|
67
|
+
/>
|
|
110
68
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
69
|
+
<EditorContent
|
|
70
|
+
imageUrl={imageUri}
|
|
71
|
+
layers={ui.layers}
|
|
72
|
+
filters={ui.filters}
|
|
73
|
+
activeLayerId={ui.activeLayerId}
|
|
74
|
+
selectedFont={ui.selectedFont}
|
|
75
|
+
fontSize={ui.fontSize}
|
|
76
|
+
fonts={fonts}
|
|
77
|
+
customTools={customTools}
|
|
78
|
+
ui={ui}
|
|
79
|
+
onFontSizeChange={ui.setFontSize}
|
|
80
|
+
onFontSelect={ui.setSelectedFont}
|
|
81
|
+
/>
|
|
119
82
|
|
|
120
83
|
<EditorToolbar
|
|
121
84
|
onAddText={ui.handleAddText}
|
|
@@ -131,64 +94,7 @@ export function PhotoEditor({
|
|
|
131
94
|
t={t}
|
|
132
95
|
/>
|
|
133
96
|
|
|
134
|
-
{
|
|
135
|
-
<BottomSheetModal ref={ui.textEditorSheetRef} snapPoints={["55%"]}>
|
|
136
|
-
<TextEditorSheet
|
|
137
|
-
value={ui.editingText}
|
|
138
|
-
onChange={ui.setEditingText}
|
|
139
|
-
onSave={ui.handleSaveText}
|
|
140
|
-
t={t}
|
|
141
|
-
color={ui.editingColor}
|
|
142
|
-
onColorChange={ui.setEditingColor}
|
|
143
|
-
textAlign={ui.editingAlign}
|
|
144
|
-
onTextAlignChange={ui.setEditingAlign}
|
|
145
|
-
isBold={ui.editingBold}
|
|
146
|
-
onBoldChange={ui.setEditingBold}
|
|
147
|
-
isItalic={ui.editingItalic}
|
|
148
|
-
onItalicChange={ui.setEditingItalic}
|
|
149
|
-
/>
|
|
150
|
-
</BottomSheetModal>
|
|
151
|
-
|
|
152
|
-
<BottomSheetModal ref={ui.stickerSheetRef} snapPoints={["50%"]}>
|
|
153
|
-
<StickerPicker onSelectSticker={ui.handleSelectSticker} />
|
|
154
|
-
</BottomSheetModal>
|
|
155
|
-
|
|
156
|
-
<BottomSheetModal ref={ui.filterSheetRef} snapPoints={["40%"]}>
|
|
157
|
-
<FilterSheet
|
|
158
|
-
selectedFilter={ui.selectedFilter}
|
|
159
|
-
onSelectFilter={(option) => {
|
|
160
|
-
ui.setSelectedFilter(option.id);
|
|
161
|
-
ui.updateFilters(option.filters);
|
|
162
|
-
ui.filterSheetRef.current?.dismiss();
|
|
163
|
-
}}
|
|
164
|
-
/>
|
|
165
|
-
</BottomSheetModal>
|
|
166
|
-
|
|
167
|
-
<BottomSheetModal ref={ui.adjustmentsSheetRef} snapPoints={["55%"]}>
|
|
168
|
-
<AdjustmentsSheet
|
|
169
|
-
filters={ui.filters}
|
|
170
|
-
onFiltersChange={ui.updateFilters}
|
|
171
|
-
/>
|
|
172
|
-
</BottomSheetModal>
|
|
173
|
-
|
|
174
|
-
<BottomSheetModal ref={ui.layerSheetRef} snapPoints={["55%"]}>
|
|
175
|
-
<LayerManager
|
|
176
|
-
layers={ui.layers}
|
|
177
|
-
activeLayerId={ui.activeLayerId}
|
|
178
|
-
onSelectLayer={ui.selectLayer}
|
|
179
|
-
onDeleteLayer={ui.deleteLayer}
|
|
180
|
-
onDuplicateLayer={ui.duplicateLayer}
|
|
181
|
-
onMoveLayerUp={ui.moveLayerUp}
|
|
182
|
-
onMoveLayerDown={ui.moveLayerDown}
|
|
183
|
-
t={t}
|
|
184
|
-
/>
|
|
185
|
-
</BottomSheetModal>
|
|
186
|
-
|
|
187
|
-
{onAICaption && (
|
|
188
|
-
<BottomSheetModal ref={ui.aiSheetRef} snapPoints={["60%"]}>
|
|
189
|
-
<AIMagicSheet onGenerateCaption={onAICaption} />
|
|
190
|
-
</BottomSheetModal>
|
|
191
|
-
)}
|
|
97
|
+
<EditorSheets ui={ui} filters={ui.filters} t={t} onAICaption={onAICaption} />
|
|
192
98
|
</View>
|
|
193
99
|
);
|
|
194
100
|
}
|
|
@@ -5,10 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import { useMemo } from "react";
|
|
7
7
|
import { useEditorStore } from "../stores/EditorStore";
|
|
8
|
-
import { Layer } from "../../domain/entities/Layer";
|
|
9
|
-
import type { TextLayerData } from "../../domain/entities/Layer";
|
|
10
|
-
import type { Transform } from "../../domain/entities/Transform";
|
|
11
|
-
import type { FilterValues } from "../../domain/entities/Filters";
|
|
12
8
|
|
|
13
9
|
export function useEditor() {
|
|
14
10
|
const store = useEditorStore();
|
|
@@ -30,6 +26,7 @@ export function useEditor() {
|
|
|
30
26
|
addTextLayer: store.addTextLayer,
|
|
31
27
|
addStickerLayer: store.addStickerLayer,
|
|
32
28
|
updateLayer: store.updateLayer,
|
|
29
|
+
updateTextLayerContent: store.updateTextLayerContent,
|
|
33
30
|
deleteLayer: store.deleteLayer,
|
|
34
31
|
duplicateLayer: store.duplicateLayer,
|
|
35
32
|
moveLayerUp: store.moveLayerUp,
|
|
@@ -51,6 +48,7 @@ export function useEditor() {
|
|
|
51
48
|
store.addTextLayer,
|
|
52
49
|
store.addStickerLayer,
|
|
53
50
|
store.updateLayer,
|
|
51
|
+
store.updateTextLayerContent,
|
|
54
52
|
store.deleteLayer,
|
|
55
53
|
store.duplicateLayer,
|
|
56
54
|
store.moveLayerUp,
|
|
@@ -61,7 +59,7 @@ export function useEditor() {
|
|
|
61
59
|
]);
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
export type { Layer } from "../../domain/entities/Layer";
|
|
62
|
+
export type { Layer } from "../../domain/entities/Layer.entity";
|
|
65
63
|
export type { Transform } from "../../domain/entities/Transform";
|
|
66
64
|
export type { FilterValues } from "../../domain/entities/Filters";
|
|
67
|
-
export type { TextLayerData } from "../../domain/
|
|
65
|
+
export type { TextContent as TextLayerData } from "../../domain/types";
|
|
@@ -7,7 +7,8 @@ import { useState, useRef, useCallback, useEffect } from "react";
|
|
|
7
7
|
import type { BottomSheetModalRef } from "@umituz/react-native-design-system/molecules";
|
|
8
8
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
9
9
|
import { useEditor } from "./useEditor";
|
|
10
|
-
import type { TextAlign } from "../../domain/
|
|
10
|
+
import type { TextAlign } from "../../domain/types";
|
|
11
|
+
import type { Transform } from "../../domain/entities/Transform";
|
|
11
12
|
|
|
12
13
|
export function useEditorUI(initialCaption?: string) {
|
|
13
14
|
const tokens = useAppDesignTokens();
|
|
@@ -50,8 +51,8 @@ export function useEditorUI(initialCaption?: string) {
|
|
|
50
51
|
// Handle text layer tap
|
|
51
52
|
const handleTextLayerTap = useCallback((layerId: string) => {
|
|
52
53
|
editor.selectLayer(layerId);
|
|
53
|
-
const layer = editor.layers.find(l => l.id === layerId);
|
|
54
|
-
if (layer
|
|
54
|
+
const layer = editor.layers.find((l: { id: string }) => l.id === layerId);
|
|
55
|
+
if (layer && layer.isText()) {
|
|
55
56
|
setEditingText(layer.text ?? "");
|
|
56
57
|
setFontSize(layer.fontSize ?? 48);
|
|
57
58
|
setEditingColor(layer.color ?? tokens.colors.textPrimary);
|
|
@@ -64,8 +65,8 @@ export function useEditorUI(initialCaption?: string) {
|
|
|
64
65
|
|
|
65
66
|
// Handle save text
|
|
66
67
|
const handleSaveText = useCallback(() => {
|
|
67
|
-
if (editor.activeLayerId) {
|
|
68
|
-
editor.
|
|
68
|
+
if (editor.activeLayerId && editor.activeLayer?.isText()) {
|
|
69
|
+
editor.updateTextLayerContent(editor.activeLayerId, {
|
|
69
70
|
text: editingText,
|
|
70
71
|
fontSize,
|
|
71
72
|
fontFamily: selectedFont,
|
|
@@ -143,3 +144,5 @@ export function useEditorUI(initialCaption?: string) {
|
|
|
143
144
|
handleLayerTransform,
|
|
144
145
|
};
|
|
145
146
|
}
|
|
147
|
+
|
|
148
|
+
export type EditorUIState = ReturnType<typeof useEditorUI>;
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useCallback, useMemo } from "react";
|
|
7
|
-
import { Layer
|
|
8
|
-
import {
|
|
7
|
+
import { Layer } from "../../domain/entities/Layer.entity";
|
|
8
|
+
import type { TextContent as TextLayerData } from "../../domain/types";
|
|
9
|
+
import { FilterSettings } from "../../domain/value-objects/FilterSettings.vo";
|
|
9
10
|
import { HistoryService, HistoryState } from "../../domain/services/HistoryService";
|
|
10
11
|
import { LayerService } from "../../domain/services/LayerService";
|
|
11
12
|
import type { Transform } from "../../domain/entities/Transform";
|
|
@@ -19,27 +20,24 @@ export function useEditorStore() {
|
|
|
19
20
|
historyService.createInitialState([])
|
|
20
21
|
);
|
|
21
22
|
const [activeLayerId, setActiveLayerId] = useState<string | null>(null);
|
|
22
|
-
const [filters, setFilters] = useState<
|
|
23
|
+
const [filters, setFilters] = useState<FilterSettings>(FilterSettings.DEFAULT);
|
|
23
24
|
|
|
24
25
|
// History actions
|
|
25
26
|
const pushLayers = useCallback((layers: Layer[]) => {
|
|
26
|
-
setHistory((prev) => historyService.push(prev, layers));
|
|
27
|
+
setHistory((prev: HistoryState<Layer[]>) => historyService.push(prev, layers));
|
|
27
28
|
}, []);
|
|
28
29
|
|
|
29
30
|
const undo = useCallback(() => {
|
|
30
|
-
setHistory((prev) => historyService.undo(prev));
|
|
31
|
+
setHistory((prev: HistoryState<Layer[]>) => historyService.undo(prev));
|
|
31
32
|
}, []);
|
|
32
33
|
|
|
33
34
|
const redo = useCallback(() => {
|
|
34
|
-
setHistory((prev) => historyService.redo(prev));
|
|
35
|
+
setHistory((prev: HistoryState<Layer[]>) => historyService.redo(prev));
|
|
35
36
|
}, []);
|
|
36
37
|
|
|
37
38
|
// Layer actions
|
|
38
|
-
const addTextLayer = useCallback((overrides?: Partial<
|
|
39
|
-
const layer = layerService.createTextLayer({
|
|
40
|
-
...overrides,
|
|
41
|
-
zIndex: history.present.length,
|
|
42
|
-
});
|
|
39
|
+
const addTextLayer = useCallback((overrides?: Partial<TextLayerData>) => {
|
|
40
|
+
const layer = layerService.createTextLayer(overrides || {});
|
|
43
41
|
pushLayers([...history.present, layer]);
|
|
44
42
|
setActiveLayerId(layer.id);
|
|
45
43
|
return layer.id;
|
|
@@ -59,6 +57,16 @@ export function useEditorStore() {
|
|
|
59
57
|
pushLayers(layers);
|
|
60
58
|
}, [history.present, pushLayers]);
|
|
61
59
|
|
|
60
|
+
const updateTextLayerContent = useCallback((id: string, updates: Partial<Omit<TextLayerData, "id" | "type">>) => {
|
|
61
|
+
const layers = history.present.map(layer => {
|
|
62
|
+
if (layer.id === id && layer.isText()) {
|
|
63
|
+
return layer.withStyle(updates);
|
|
64
|
+
}
|
|
65
|
+
return layer;
|
|
66
|
+
});
|
|
67
|
+
pushLayers(layers);
|
|
68
|
+
}, [history.present, pushLayers]);
|
|
69
|
+
|
|
62
70
|
const deleteLayer = useCallback((id: string) => {
|
|
63
71
|
const layers = layerService.deleteLayer(history.present, id);
|
|
64
72
|
pushLayers(layers);
|
|
@@ -89,19 +97,35 @@ export function useEditorStore() {
|
|
|
89
97
|
}, []);
|
|
90
98
|
|
|
91
99
|
// Filter actions
|
|
92
|
-
const updateFilters = useCallback((updates: Partial<
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
const updateFilters = useCallback((updates: Partial<{ brightness: number; contrast: number; saturation: number; sepia: number; grayscale: number; hueRotate?: number }>) => {
|
|
101
|
+
if (updates.brightness !== undefined) {
|
|
102
|
+
setFilters(filters.withBrightness(updates.brightness));
|
|
103
|
+
}
|
|
104
|
+
if (updates.contrast !== undefined) {
|
|
105
|
+
setFilters(filters.withContrast(updates.contrast));
|
|
106
|
+
}
|
|
107
|
+
if (updates.saturation !== undefined) {
|
|
108
|
+
setFilters(filters.withSaturation(updates.saturation));
|
|
109
|
+
}
|
|
110
|
+
if (updates.sepia !== undefined) {
|
|
111
|
+
setFilters(filters.withSepia(updates.sepia));
|
|
112
|
+
}
|
|
113
|
+
if (updates.grayscale !== undefined) {
|
|
114
|
+
setFilters(filters.withGrayscale(updates.grayscale));
|
|
115
|
+
}
|
|
116
|
+
if (updates.hueRotate !== undefined) {
|
|
117
|
+
setFilters(filters.withHueRotate(updates.hueRotate));
|
|
118
|
+
}
|
|
95
119
|
}, [filters]);
|
|
96
120
|
|
|
97
121
|
const resetFilters = useCallback(() => {
|
|
98
|
-
setFilters(
|
|
122
|
+
setFilters(FilterSettings.DEFAULT);
|
|
99
123
|
}, []);
|
|
100
124
|
|
|
101
125
|
// Getters
|
|
102
126
|
const layers = useMemo(() => layerService.sortByZIndex(history.present), [history.present]);
|
|
103
127
|
const activeLayer = useMemo(() =>
|
|
104
|
-
history.present.find(l => l.id === activeLayerId) ?? null,
|
|
128
|
+
history.present.find((l: Layer) => l.id === activeLayerId) ?? null,
|
|
105
129
|
[history.present, activeLayerId]
|
|
106
130
|
);
|
|
107
131
|
const canUndo = useMemo(() => historyService.canUndo(history), [history]);
|
|
@@ -112,7 +136,7 @@ export function useEditorStore() {
|
|
|
112
136
|
layers,
|
|
113
137
|
activeLayerId,
|
|
114
138
|
activeLayer,
|
|
115
|
-
filters
|
|
139
|
+
filters,
|
|
116
140
|
canUndo,
|
|
117
141
|
canRedo,
|
|
118
142
|
|
|
@@ -124,6 +148,7 @@ export function useEditorStore() {
|
|
|
124
148
|
addTextLayer,
|
|
125
149
|
addStickerLayer,
|
|
126
150
|
updateLayer,
|
|
151
|
+
updateTextLayerContent,
|
|
127
152
|
deleteLayer,
|
|
128
153
|
duplicateLayer,
|
|
129
154
|
moveLayerUp,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Layer Entity
|
|
3
|
+
* Core layer abstraction with transform and appearance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LayerType, Position, Appearance, LayerContent } from "../types";
|
|
7
|
+
import type { TextLayer } from "./TextLayer.entity";
|
|
8
|
+
import type { StickerLayer } from "./StickerLayer.entity";
|
|
9
|
+
import { TextLayer as TextLayerClass } from "./TextLayer.entity";
|
|
10
|
+
import { StickerLayer as StickerLayerClass } from "./StickerLayer.entity";
|
|
11
|
+
|
|
12
|
+
export class Layer {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly type: LayerType;
|
|
15
|
+
readonly position: Position;
|
|
16
|
+
readonly rotation: number;
|
|
17
|
+
readonly scale: number;
|
|
18
|
+
readonly appearance: Appearance;
|
|
19
|
+
readonly content: LayerContent;
|
|
20
|
+
|
|
21
|
+
constructor(data: {
|
|
22
|
+
id: string;
|
|
23
|
+
type: LayerType;
|
|
24
|
+
position: Position;
|
|
25
|
+
rotation: number;
|
|
26
|
+
scale: number;
|
|
27
|
+
appearance: Appearance;
|
|
28
|
+
content: LayerContent;
|
|
29
|
+
}) {
|
|
30
|
+
this.id = data.id;
|
|
31
|
+
this.type = data.type;
|
|
32
|
+
this.position = data.position;
|
|
33
|
+
this.rotation = data.rotation;
|
|
34
|
+
this.scale = data.scale;
|
|
35
|
+
this.appearance = data.appearance;
|
|
36
|
+
this.content = data.content;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Convenience getters for backward compatibility
|
|
40
|
+
get x(): number { return this.position.x; }
|
|
41
|
+
get y(): number { return this.position.y; }
|
|
42
|
+
get zIndex(): number { return this.appearance.zIndex; }
|
|
43
|
+
get opacity(): number { return this.appearance.opacity; }
|
|
44
|
+
|
|
45
|
+
// Type guards
|
|
46
|
+
isText(): this is TextLayer {
|
|
47
|
+
return this.type === "text";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
isSticker(): this is StickerLayer {
|
|
51
|
+
return this.type === "sticker";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Immutable updates
|
|
55
|
+
withPosition(position: Partial<Position>): Layer {
|
|
56
|
+
return new Layer({
|
|
57
|
+
...this,
|
|
58
|
+
position: { ...this.position, ...position },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
withTransform(transform: { rotation?: number; scale?: number }): Layer {
|
|
63
|
+
return new Layer({
|
|
64
|
+
...this,
|
|
65
|
+
...transform,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
withAppearance(appearance: Partial<Appearance>): Layer {
|
|
70
|
+
return new Layer({
|
|
71
|
+
...this,
|
|
72
|
+
appearance: { ...this.appearance, ...appearance },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
withZIndex(zIndex: number): Layer {
|
|
77
|
+
return this.withAppearance({ zIndex });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
toJSON() {
|
|
81
|
+
return {
|
|
82
|
+
id: this.id,
|
|
83
|
+
type: this.type,
|
|
84
|
+
x: this.position.x,
|
|
85
|
+
y: this.position.y,
|
|
86
|
+
rotation: this.rotation,
|
|
87
|
+
scale: this.scale,
|
|
88
|
+
opacity: this.appearance.opacity,
|
|
89
|
+
zIndex: this.appearance.zIndex,
|
|
90
|
+
...this.content,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static from(data: ReturnType<Layer["toJSON"]>): Layer {
|
|
95
|
+
if (data.type === "text") {
|
|
96
|
+
return new TextLayerClass({
|
|
97
|
+
id: data.id,
|
|
98
|
+
position: { x: data.x, y: data.y },
|
|
99
|
+
rotation: data.rotation,
|
|
100
|
+
scale: data.scale,
|
|
101
|
+
appearance: { opacity: data.opacity, zIndex: data.zIndex },
|
|
102
|
+
content: {
|
|
103
|
+
text: (data as Record<string, unknown>).text as string || "",
|
|
104
|
+
fontSize: (data as Record<string, unknown>).fontSize as number || 32,
|
|
105
|
+
fontFamily: (data as Record<string, unknown>).fontFamily as string || "System",
|
|
106
|
+
color: (data as Record<string, unknown>).color as string || "#FFFFFF",
|
|
107
|
+
backgroundColor: (data as Record<string, unknown>).backgroundColor as string || "transparent",
|
|
108
|
+
textAlign: (data as Record<string, unknown>).textAlign as "left" | "center" | "right" || "center",
|
|
109
|
+
isBold: (data as Record<string, unknown>).isBold as boolean || undefined,
|
|
110
|
+
isItalic: (data as Record<string, unknown>).isItalic as boolean || undefined,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return new StickerLayerClass({
|
|
115
|
+
id: data.id,
|
|
116
|
+
position: { x: data.x, y: data.y },
|
|
117
|
+
rotation: data.rotation,
|
|
118
|
+
scale: data.scale,
|
|
119
|
+
appearance: { opacity: data.opacity, zIndex: data.zIndex },
|
|
120
|
+
content: { uri: (data as Record<string, unknown>).uri as string || "" },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Type exports for backward compatibility
|
|
126
|
+
export type { TextLayer } from "./TextLayer.entity";
|
|
127
|
+
export type { StickerLayer } from "./StickerLayer.entity";
|
|
128
|
+
export type { TextAlign } from "../types";
|
|
129
|
+
|
|
130
|
+
// Type guard functions for backward compatibility
|
|
131
|
+
export function isTextLayer(layer: Layer): layer is TextLayer {
|
|
132
|
+
return layer.isText();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function isStickerLayer(layer: Layer): layer is StickerLayer {
|
|
136
|
+
return layer.isSticker();
|
|
137
|
+
}
|
|
138
|
+
|
|
@@ -41,7 +41,7 @@ export interface StickerLayerData extends BaseLayerData {
|
|
|
41
41
|
export type LayerData = TextLayerData | StickerLayerData;
|
|
42
42
|
|
|
43
43
|
export class Layer {
|
|
44
|
-
constructor(
|
|
44
|
+
constructor(protected readonly data: LayerData) {}
|
|
45
45
|
|
|
46
46
|
get id(): string { return this.data.id; }
|
|
47
47
|
get type(): LayerType { return this.data.type; }
|
|
@@ -87,7 +87,7 @@ export class Layer {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export class TextLayer extends Layer {
|
|
90
|
-
declare readonly data: TextLayerData;
|
|
90
|
+
declare protected readonly data: TextLayerData;
|
|
91
91
|
|
|
92
92
|
get text(): string { return this.data.text; }
|
|
93
93
|
get fontSize(): number { return this.data.fontSize; }
|
|
@@ -108,7 +108,7 @@ export class TextLayer extends Layer {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export class StickerLayer extends Layer {
|
|
111
|
-
declare readonly data: StickerLayerData;
|
|
111
|
+
declare protected readonly data: StickerLayerData;
|
|
112
112
|
|
|
113
113
|
get uri(): string { return this.data.uri; }
|
|
114
114
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sticker Layer Entity
|
|
3
|
+
* Specialized layer for sticker/image content
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Layer } from "./Layer.entity";
|
|
7
|
+
import type { StickerContent, Position, Appearance } from "../types";
|
|
8
|
+
|
|
9
|
+
export class StickerLayer extends Layer {
|
|
10
|
+
readonly content: StickerContent;
|
|
11
|
+
|
|
12
|
+
constructor(data: {
|
|
13
|
+
id: string;
|
|
14
|
+
position: Position;
|
|
15
|
+
rotation: number;
|
|
16
|
+
scale: number;
|
|
17
|
+
appearance: Appearance;
|
|
18
|
+
content: StickerContent;
|
|
19
|
+
}) {
|
|
20
|
+
super({
|
|
21
|
+
...data,
|
|
22
|
+
type: "sticker",
|
|
23
|
+
});
|
|
24
|
+
this.content = data.content;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Convenience getters
|
|
28
|
+
get uri(): string { return this.content.uri; }
|
|
29
|
+
|
|
30
|
+
// Immutable updates
|
|
31
|
+
withUri(uri: string): StickerLayer {
|
|
32
|
+
return new StickerLayer({
|
|
33
|
+
...this,
|
|
34
|
+
content: { ...this.content, uri },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Layer Entity
|
|
3
|
+
* Specialized layer for text content with font and style properties
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Layer } from "./Layer.entity";
|
|
7
|
+
import type { TextContent, Position, Appearance, TextAlign } from "../types";
|
|
8
|
+
|
|
9
|
+
export class TextLayer extends Layer {
|
|
10
|
+
readonly content: TextContent;
|
|
11
|
+
|
|
12
|
+
constructor(data: {
|
|
13
|
+
id: string;
|
|
14
|
+
position: Position;
|
|
15
|
+
rotation: number;
|
|
16
|
+
scale: number;
|
|
17
|
+
appearance: Appearance;
|
|
18
|
+
content: TextContent;
|
|
19
|
+
}) {
|
|
20
|
+
super({
|
|
21
|
+
...data,
|
|
22
|
+
type: "text",
|
|
23
|
+
});
|
|
24
|
+
this.content = data.content;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Convenience getters
|
|
28
|
+
get text(): string { return this.content.text; }
|
|
29
|
+
get fontSize(): number { return this.content.fontSize; }
|
|
30
|
+
get fontFamily(): string { return this.content.fontFamily; }
|
|
31
|
+
get color(): string { return this.content.color; }
|
|
32
|
+
get backgroundColor(): string { return this.content.backgroundColor; }
|
|
33
|
+
get textAlign(): TextAlign { return this.content.textAlign; }
|
|
34
|
+
get isBold(): boolean { return this.content.isBold ?? false; }
|
|
35
|
+
get isItalic(): boolean { return this.content.isItalic ?? false; }
|
|
36
|
+
|
|
37
|
+
// Immutable updates
|
|
38
|
+
withText(text: string): TextLayer {
|
|
39
|
+
return new TextLayer({
|
|
40
|
+
...this,
|
|
41
|
+
content: { ...this.content, text },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
withStyle(styles: Partial<Omit<TextContent, "text">>): TextLayer {
|
|
46
|
+
return new TextLayer({
|
|
47
|
+
...this,
|
|
48
|
+
content: { ...this.content, ...styles },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
withContent(content: Partial<TextContent>): TextLayer {
|
|
53
|
+
return new TextLayer({
|
|
54
|
+
...this,
|
|
55
|
+
content: { ...this.content, ...content },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|