@umituz/react-native-photo-editor 1.0.1
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 +32 -0
- package/src/PhotoEditor.tsx +153 -0
- package/src/components/AIMagicSheet.tsx +174 -0
- package/src/components/DraggableSticker.tsx +100 -0
- package/src/components/DraggableText.tsx +143 -0
- package/src/components/EditorCanvas.tsx +80 -0
- package/src/components/EditorToolbar.tsx +54 -0
- package/src/components/FilterPicker.tsx +102 -0
- package/src/components/FontControls.tsx +82 -0
- package/src/components/LayerManager.tsx +111 -0
- package/src/components/StickerPicker.tsx +94 -0
- package/src/components/TextEditorSheet.tsx +63 -0
- package/src/core/HistoryManager.ts +75 -0
- package/src/hooks/usePhotoEditor.ts +165 -0
- package/src/hooks/usePhotoEditorUI.ts +126 -0
- package/src/index.ts +11 -0
- package/src/styles.ts +160 -0
- package/src/types.ts +50 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo } from "react";
|
|
2
|
+
import { Layer, TextLayer, ImageFilters } from "../types";
|
|
3
|
+
import { HistoryManager } from "../core/HistoryManager";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_FILTERS: ImageFilters = {
|
|
6
|
+
brightness: 1,
|
|
7
|
+
contrast: 1,
|
|
8
|
+
saturation: 1,
|
|
9
|
+
sepia: 0,
|
|
10
|
+
grayscale: 0,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const historyManager = new HistoryManager<Layer[]>();
|
|
14
|
+
|
|
15
|
+
import { DesignTokens } from "@umituz/react-native-design-system";
|
|
16
|
+
|
|
17
|
+
export const usePhotoEditor = (initialLayers: Layer[] = []) => {
|
|
18
|
+
const [history, setHistory] = useState(() =>
|
|
19
|
+
historyManager.createInitialState(initialLayers),
|
|
20
|
+
);
|
|
21
|
+
const [activeLayerId, setActiveLayerId] = useState<string | null>(
|
|
22
|
+
initialLayers[0]?.id || null,
|
|
23
|
+
);
|
|
24
|
+
const [filters, setFilters] = useState<ImageFilters>(DEFAULT_FILTERS);
|
|
25
|
+
|
|
26
|
+
const layers = history.present;
|
|
27
|
+
|
|
28
|
+
const canUndo = historyManager.canUndo(history);
|
|
29
|
+
const canRedo = historyManager.canRedo(history);
|
|
30
|
+
|
|
31
|
+
const pushState = useCallback((newLayers: Layer[]) => {
|
|
32
|
+
setHistory((prev) => historyManager.push(prev, newLayers));
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const addTextLayer = useCallback(
|
|
36
|
+
(defaultTokens: DesignTokens) => {
|
|
37
|
+
const id = `text-${Date.now()}`;
|
|
38
|
+
const newLayer: TextLayer = {
|
|
39
|
+
id,
|
|
40
|
+
type: "text",
|
|
41
|
+
text: "",
|
|
42
|
+
x: 50,
|
|
43
|
+
y: 50,
|
|
44
|
+
rotation: 0,
|
|
45
|
+
scale: 1,
|
|
46
|
+
opacity: 1,
|
|
47
|
+
zIndex: layers.length,
|
|
48
|
+
fontSize: 32,
|
|
49
|
+
fontFamily: "System",
|
|
50
|
+
color: defaultTokens.colors.onPrimary,
|
|
51
|
+
backgroundColor: "transparent",
|
|
52
|
+
textAlign: "center",
|
|
53
|
+
strokeWidth: 2,
|
|
54
|
+
strokeColor: defaultTokens.colors.onBackground,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const newLayers: Layer[] = [...layers, newLayer];
|
|
58
|
+
pushState(newLayers);
|
|
59
|
+
setActiveLayerId(newLayer.id);
|
|
60
|
+
return id;
|
|
61
|
+
},
|
|
62
|
+
[layers, pushState],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const addStickerLayer = useCallback(
|
|
66
|
+
(uri: string) => {
|
|
67
|
+
const id = `sticker-${Date.now()}`;
|
|
68
|
+
const newLayer: Layer = {
|
|
69
|
+
id,
|
|
70
|
+
type: "sticker",
|
|
71
|
+
uri: uri,
|
|
72
|
+
x: 100,
|
|
73
|
+
y: 100,
|
|
74
|
+
rotation: 0,
|
|
75
|
+
scale: 1,
|
|
76
|
+
opacity: 1,
|
|
77
|
+
zIndex: layers.length,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const newLayers: Layer[] = [...layers, newLayer];
|
|
81
|
+
pushState(newLayers);
|
|
82
|
+
setActiveLayerId(newLayer.id);
|
|
83
|
+
return id;
|
|
84
|
+
},
|
|
85
|
+
[layers, pushState],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const updateLayer = useCallback(
|
|
89
|
+
(id: string, updates: Partial<Layer>, silent = false) => {
|
|
90
|
+
const newLayers: Layer[] = layers.map((layer) =>
|
|
91
|
+
layer.id === id ? ({ ...layer, ...updates } as Layer) : layer,
|
|
92
|
+
);
|
|
93
|
+
if (silent) {
|
|
94
|
+
// Just update state without pushing to history (hacky but works for init)
|
|
95
|
+
setHistory((prev) => ({ ...prev, present: newLayers }));
|
|
96
|
+
} else {
|
|
97
|
+
pushState(newLayers);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
[layers, pushState],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const deleteLayer = useCallback(
|
|
104
|
+
(id: string) => {
|
|
105
|
+
if (layers.length <= 1) return;
|
|
106
|
+
const newLayers = layers.filter((layer) => layer.id !== id);
|
|
107
|
+
pushState(newLayers);
|
|
108
|
+
if (activeLayerId === id) {
|
|
109
|
+
setActiveLayerId(newLayers[0]?.id || null);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
[layers, activeLayerId, pushState],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const undo = useCallback(() => {
|
|
116
|
+
setHistory((prev) => historyManager.undo(prev));
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
const redo = useCallback(() => {
|
|
120
|
+
setHistory((prev) => historyManager.redo(prev));
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
const selectLayer = useCallback((id: string) => {
|
|
124
|
+
setActiveLayerId(id);
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
const bringToFront = useCallback(
|
|
128
|
+
(id: string) => {
|
|
129
|
+
const maxZ = Math.max(...layers.map((l) => l.zIndex), 0);
|
|
130
|
+
updateLayer(id, { zIndex: maxZ + 1 });
|
|
131
|
+
},
|
|
132
|
+
[layers, updateLayer],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const captureImage = useCallback(
|
|
136
|
+
async (_viewRef: unknown, backgroundUrl: string) => {
|
|
137
|
+
return backgroundUrl;
|
|
138
|
+
},
|
|
139
|
+
[],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const activeLayer = useMemo(
|
|
143
|
+
() => layers.find((l) => l.id === activeLayerId),
|
|
144
|
+
[layers, activeLayerId],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
layers: [...layers].sort((a, b) => a.zIndex - b.zIndex),
|
|
149
|
+
activeLayer,
|
|
150
|
+
activeLayerId,
|
|
151
|
+
addTextLayer,
|
|
152
|
+
addStickerLayer,
|
|
153
|
+
updateLayer,
|
|
154
|
+
deleteLayer,
|
|
155
|
+
selectLayer,
|
|
156
|
+
undo,
|
|
157
|
+
redo,
|
|
158
|
+
canUndo,
|
|
159
|
+
canRedo,
|
|
160
|
+
bringToFront,
|
|
161
|
+
filters,
|
|
162
|
+
updateFilters: setFilters,
|
|
163
|
+
captureImage,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useRef, useState, useCallback, useEffect } from "react";
|
|
2
|
+
import { BottomSheetModalRef, DesignTokens } from "@umituz/react-native-design-system";
|
|
3
|
+
import { usePhotoEditor } from "./usePhotoEditor";
|
|
4
|
+
|
|
5
|
+
export const usePhotoEditorUI = (
|
|
6
|
+
initialCaption: string | undefined,
|
|
7
|
+
tokens: DesignTokens,
|
|
8
|
+
) => {
|
|
9
|
+
const textEditorSheetRef = useRef<BottomSheetModalRef>(null);
|
|
10
|
+
const stickerSheetRef = useRef<BottomSheetModalRef>(null);
|
|
11
|
+
const filterSheetRef = useRef<BottomSheetModalRef>(null);
|
|
12
|
+
const layerSheetRef = useRef<BottomSheetModalRef>(null);
|
|
13
|
+
|
|
14
|
+
const [selectedFont, setSelectedFont] = useState<string>("Impact");
|
|
15
|
+
const [fontSize, setFontSize] = useState(48);
|
|
16
|
+
const [editingText, setEditingText] = useState("");
|
|
17
|
+
const [selectedFilter, setSelectedFilter] = useState("none");
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
layers,
|
|
21
|
+
activeLayerId,
|
|
22
|
+
addTextLayer,
|
|
23
|
+
addStickerLayer,
|
|
24
|
+
updateLayer,
|
|
25
|
+
deleteLayer,
|
|
26
|
+
selectLayer,
|
|
27
|
+
updateFilters,
|
|
28
|
+
filters,
|
|
29
|
+
} = usePhotoEditor([]);
|
|
30
|
+
|
|
31
|
+
// Handle initial caption
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (initialCaption) {
|
|
34
|
+
const id = addTextLayer(tokens);
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
updateLayer(id, { text: initialCaption } as any, true);
|
|
37
|
+
}, 0);
|
|
38
|
+
}
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const handleAddText = useCallback(() => {
|
|
43
|
+
addTextLayer(tokens);
|
|
44
|
+
setTimeout(() => textEditorSheetRef.current?.present(), 100);
|
|
45
|
+
}, [addTextLayer, tokens]);
|
|
46
|
+
|
|
47
|
+
const handleTextLayerTap = useCallback(
|
|
48
|
+
(layerId: string) => {
|
|
49
|
+
selectLayer(layerId);
|
|
50
|
+
const layer = layers.find((l) => l.id === layerId);
|
|
51
|
+
if (layer?.type === "text") {
|
|
52
|
+
setEditingText((layer as any).text || "");
|
|
53
|
+
setFontSize((layer as any).fontSize || 48);
|
|
54
|
+
textEditorSheetRef.current?.present();
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[selectLayer, layers],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const handleSaveText = useCallback(() => {
|
|
61
|
+
if (activeLayerId) {
|
|
62
|
+
updateLayer(activeLayerId, {
|
|
63
|
+
text: editingText,
|
|
64
|
+
fontSize,
|
|
65
|
+
fontFamily: selectedFont,
|
|
66
|
+
} as any);
|
|
67
|
+
}
|
|
68
|
+
textEditorSheetRef.current?.dismiss();
|
|
69
|
+
}, [activeLayerId, editingText, fontSize, selectedFont, updateLayer]);
|
|
70
|
+
|
|
71
|
+
const handleSelectFilter = useCallback(
|
|
72
|
+
(filterId: string, value: number) => {
|
|
73
|
+
setSelectedFilter(filterId);
|
|
74
|
+
const base = {
|
|
75
|
+
brightness: 1,
|
|
76
|
+
contrast: 1,
|
|
77
|
+
saturation: 1,
|
|
78
|
+
sepia: 0,
|
|
79
|
+
grayscale: 0,
|
|
80
|
+
};
|
|
81
|
+
if (filterId === "sepia") updateFilters({ ...base, sepia: value });
|
|
82
|
+
else if (filterId === "grayscale")
|
|
83
|
+
updateFilters({ ...base, grayscale: value });
|
|
84
|
+
else updateFilters(base);
|
|
85
|
+
},
|
|
86
|
+
[updateFilters],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const handleSelectSticker = useCallback(
|
|
90
|
+
(sticker: string) => {
|
|
91
|
+
addStickerLayer(sticker);
|
|
92
|
+
stickerSheetRef.current?.dismiss();
|
|
93
|
+
},
|
|
94
|
+
[addStickerLayer],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
// Refs
|
|
99
|
+
textEditorSheetRef,
|
|
100
|
+
stickerSheetRef,
|
|
101
|
+
filterSheetRef,
|
|
102
|
+
layerSheetRef,
|
|
103
|
+
// State
|
|
104
|
+
selectedFont,
|
|
105
|
+
setSelectedFont,
|
|
106
|
+
fontSize,
|
|
107
|
+
setFontSize,
|
|
108
|
+
editingText,
|
|
109
|
+
setEditingText,
|
|
110
|
+
selectedFilter,
|
|
111
|
+
// Domain State
|
|
112
|
+
layers,
|
|
113
|
+
activeLayerId,
|
|
114
|
+
filters,
|
|
115
|
+
// Domain Actions
|
|
116
|
+
updateLayer,
|
|
117
|
+
deleteLayer,
|
|
118
|
+
selectLayer,
|
|
119
|
+
// UI Actions
|
|
120
|
+
handleAddText,
|
|
121
|
+
handleTextLayerTap,
|
|
122
|
+
handleSaveText,
|
|
123
|
+
handleSelectFilter,
|
|
124
|
+
handleSelectSticker,
|
|
125
|
+
};
|
|
126
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./hooks/usePhotoEditor";
|
|
3
|
+
export * from "./core/HistoryManager";
|
|
4
|
+
export * from "./components/EditorCanvas";
|
|
5
|
+
export * from "./components/LayerManager";
|
|
6
|
+
export * from "./components/FontControls";
|
|
7
|
+
export * from "./components/FilterPicker";
|
|
8
|
+
export * from "./components/DraggableText";
|
|
9
|
+
export * from "./components/DraggableSticker";
|
|
10
|
+
export * from "./components/AIMagicSheet";
|
|
11
|
+
export * from "./PhotoEditor";
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { DesignTokens } from "@umituz/react-native-design-system";
|
|
3
|
+
import { EdgeInsets } from "react-native-safe-area-context";
|
|
4
|
+
|
|
5
|
+
export const createEditorStyles = (tokens: DesignTokens, insets: EdgeInsets) =>
|
|
6
|
+
StyleSheet.create({
|
|
7
|
+
container: { flex: 1, backgroundColor: tokens.colors.background },
|
|
8
|
+
header: {
|
|
9
|
+
flexDirection: "row",
|
|
10
|
+
alignItems: "center",
|
|
11
|
+
justifyContent: "space-between",
|
|
12
|
+
paddingHorizontal: tokens.spacing.md,
|
|
13
|
+
paddingTop: insets.top + tokens.spacing.sm,
|
|
14
|
+
paddingBottom: tokens.spacing.sm,
|
|
15
|
+
},
|
|
16
|
+
headerButton: {
|
|
17
|
+
width: 48,
|
|
18
|
+
height: 48,
|
|
19
|
+
borderRadius: 24,
|
|
20
|
+
alignItems: "center",
|
|
21
|
+
justifyContent: "center",
|
|
22
|
+
},
|
|
23
|
+
headerTitle: {
|
|
24
|
+
fontSize: 18,
|
|
25
|
+
fontWeight: "bold",
|
|
26
|
+
color: tokens.colors.textPrimary,
|
|
27
|
+
},
|
|
28
|
+
postButton: {
|
|
29
|
+
backgroundColor: tokens.colors.primary,
|
|
30
|
+
paddingHorizontal: 20,
|
|
31
|
+
paddingVertical: 10,
|
|
32
|
+
borderRadius: 999,
|
|
33
|
+
},
|
|
34
|
+
postButtonText: {
|
|
35
|
+
color: tokens.colors.onPrimary,
|
|
36
|
+
fontWeight: "bold",
|
|
37
|
+
fontSize: 14,
|
|
38
|
+
},
|
|
39
|
+
scrollContent: { paddingHorizontal: tokens.spacing.md, paddingBottom: 120 },
|
|
40
|
+
canvas: {
|
|
41
|
+
width: "100%",
|
|
42
|
+
aspectRatio: 4 / 5,
|
|
43
|
+
borderRadius: 16,
|
|
44
|
+
overflow: "hidden",
|
|
45
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
46
|
+
marginTop: tokens.spacing.sm,
|
|
47
|
+
},
|
|
48
|
+
canvasImage: { width: "100%", height: "100%" },
|
|
49
|
+
// Slider & Controls
|
|
50
|
+
controlsPanel: {
|
|
51
|
+
marginTop: tokens.spacing.md,
|
|
52
|
+
backgroundColor: tokens.colors.surfaceVariant + "80", // Opacity handled by token if possible, else hex
|
|
53
|
+
borderRadius: 16,
|
|
54
|
+
padding: tokens.spacing.md,
|
|
55
|
+
borderWidth: 1,
|
|
56
|
+
borderColor: tokens.colors.border,
|
|
57
|
+
},
|
|
58
|
+
sliderRow: {
|
|
59
|
+
flexDirection: "row",
|
|
60
|
+
alignItems: "center",
|
|
61
|
+
justifyContent: "space-between",
|
|
62
|
+
marginBottom: tokens.spacing.sm,
|
|
63
|
+
},
|
|
64
|
+
sliderLabel: {
|
|
65
|
+
flexDirection: "row",
|
|
66
|
+
alignItems: "center",
|
|
67
|
+
gap: tokens.spacing.xs,
|
|
68
|
+
},
|
|
69
|
+
sliderLabelText: { fontSize: 14, color: tokens.colors.textSecondary },
|
|
70
|
+
sliderValue: {
|
|
71
|
+
fontSize: 14,
|
|
72
|
+
fontWeight: "bold",
|
|
73
|
+
color: tokens.colors.primary,
|
|
74
|
+
},
|
|
75
|
+
sliderTrack: {
|
|
76
|
+
height: 6,
|
|
77
|
+
backgroundColor: tokens.colors.border,
|
|
78
|
+
borderRadius: 3,
|
|
79
|
+
marginBottom: tokens.spacing.lg,
|
|
80
|
+
},
|
|
81
|
+
sliderFill: {
|
|
82
|
+
height: "100%",
|
|
83
|
+
width: "65%",
|
|
84
|
+
backgroundColor: tokens.colors.primary,
|
|
85
|
+
borderRadius: 3,
|
|
86
|
+
},
|
|
87
|
+
fontLabel: {
|
|
88
|
+
fontSize: 14,
|
|
89
|
+
color: tokens.colors.textSecondary,
|
|
90
|
+
marginBottom: tokens.spacing.sm,
|
|
91
|
+
},
|
|
92
|
+
fontRow: { flexDirection: "row", gap: tokens.spacing.sm },
|
|
93
|
+
fontChip: {
|
|
94
|
+
paddingHorizontal: tokens.spacing.md,
|
|
95
|
+
paddingVertical: tokens.spacing.sm,
|
|
96
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
97
|
+
borderRadius: 8,
|
|
98
|
+
borderWidth: 1,
|
|
99
|
+
borderColor: tokens.colors.border,
|
|
100
|
+
},
|
|
101
|
+
fontChipActive: {
|
|
102
|
+
backgroundColor: tokens.colors.primary,
|
|
103
|
+
borderColor: tokens.colors.primary,
|
|
104
|
+
},
|
|
105
|
+
fontChipText: {
|
|
106
|
+
fontWeight: "bold",
|
|
107
|
+
fontSize: 14,
|
|
108
|
+
color: tokens.colors.textSecondary,
|
|
109
|
+
},
|
|
110
|
+
fontChipTextActive: { color: tokens.colors.onPrimary },
|
|
111
|
+
// Bottom Toolbar
|
|
112
|
+
bottomToolbar: {
|
|
113
|
+
position: "absolute",
|
|
114
|
+
bottom: insets.bottom + tokens.spacing.md,
|
|
115
|
+
left: "5%",
|
|
116
|
+
right: "5%",
|
|
117
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
118
|
+
borderRadius: 999,
|
|
119
|
+
padding: tokens.spacing.sm,
|
|
120
|
+
flexDirection: "row",
|
|
121
|
+
justifyContent: "space-around",
|
|
122
|
+
alignItems: "center",
|
|
123
|
+
borderWidth: 1,
|
|
124
|
+
borderColor: tokens.colors.border,
|
|
125
|
+
shadowColor: "#000",
|
|
126
|
+
shadowOffset: {
|
|
127
|
+
width: 0,
|
|
128
|
+
height: 2,
|
|
129
|
+
},
|
|
130
|
+
shadowOpacity: 0.1,
|
|
131
|
+
shadowRadius: 3.84,
|
|
132
|
+
elevation: 5,
|
|
133
|
+
},
|
|
134
|
+
toolButton: {
|
|
135
|
+
alignItems: "center",
|
|
136
|
+
justifyContent: "center",
|
|
137
|
+
width: 56,
|
|
138
|
+
height: 56,
|
|
139
|
+
borderRadius: 28,
|
|
140
|
+
},
|
|
141
|
+
toolButtonActive: { backgroundColor: tokens.colors.primary + "20" },
|
|
142
|
+
toolLabel: {
|
|
143
|
+
fontSize: 10,
|
|
144
|
+
fontWeight: "500",
|
|
145
|
+
color: tokens.colors.textSecondary,
|
|
146
|
+
marginTop: 2,
|
|
147
|
+
},
|
|
148
|
+
toolLabelActive: { color: tokens.colors.primary },
|
|
149
|
+
aiMagicButton: {
|
|
150
|
+
width: 64,
|
|
151
|
+
height: 64,
|
|
152
|
+
borderRadius: 32,
|
|
153
|
+
backgroundColor: tokens.colors.primary,
|
|
154
|
+
alignItems: "center",
|
|
155
|
+
justifyContent: "center",
|
|
156
|
+
marginTop: -32,
|
|
157
|
+
borderWidth: 4,
|
|
158
|
+
borderColor: tokens.colors.background,
|
|
159
|
+
},
|
|
160
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor Domain Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type TextAlign = "left" | "center" | "right";
|
|
6
|
+
|
|
7
|
+
export interface ImageFilters {
|
|
8
|
+
brightness: number;
|
|
9
|
+
contrast: number;
|
|
10
|
+
saturation: number;
|
|
11
|
+
sepia: number;
|
|
12
|
+
grayscale: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BaseLayer {
|
|
16
|
+
id: string;
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
rotation: number;
|
|
20
|
+
scale: number;
|
|
21
|
+
opacity: number;
|
|
22
|
+
zIndex: number;
|
|
23
|
+
type: "text" | "sticker" | "image";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TextLayer extends BaseLayer {
|
|
27
|
+
type: "text";
|
|
28
|
+
text: string;
|
|
29
|
+
fontSize: number;
|
|
30
|
+
fontFamily: string;
|
|
31
|
+
color: string;
|
|
32
|
+
backgroundColor: string;
|
|
33
|
+
textAlign: TextAlign;
|
|
34
|
+
strokeColor?: string;
|
|
35
|
+
strokeWidth?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface StickerLayer extends BaseLayer {
|
|
39
|
+
type: "sticker";
|
|
40
|
+
uri: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type Layer = TextLayer | StickerLayer;
|
|
44
|
+
|
|
45
|
+
export interface EditorState {
|
|
46
|
+
layers: Layer[];
|
|
47
|
+
activeLayerId: string | null;
|
|
48
|
+
canvasSize: { width: number; height: number };
|
|
49
|
+
filters: ImageFilters;
|
|
50
|
+
}
|