@umituz/react-native-video-editor 1.1.38 → 1.1.39
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/index.ts +3 -0
- package/src/infrastructure/constants/collage.constants.ts +74 -0
- package/src/infrastructure/constants/index.ts +1 -0
- package/src/presentation/components/CollageEditorCanvas.tsx +285 -0
- package/src/presentation/components/index.ts +1 -0
- package/src/presentation/hooks/useCollageEditor.ts +73 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-video-editor",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.39",
|
|
4
4
|
"description": "Professional video editor with layer-based timeline, text/image/shape/audio/animation layers, and export functionality",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,7 @@ export {
|
|
|
76
76
|
ExportDialog,
|
|
77
77
|
SpeedControlPanel,
|
|
78
78
|
VideoFilterPicker,
|
|
79
|
+
CollageEditorCanvas,
|
|
79
80
|
} from "./presentation/components";
|
|
80
81
|
|
|
81
82
|
export { useEditorLayers } from "./presentation/hooks/useEditorLayers";
|
|
@@ -100,6 +101,8 @@ export { useLayerActions } from "./presentation/hooks/useLayerActions";
|
|
|
100
101
|
export { useSceneActions } from "./presentation/hooks/useSceneActions";
|
|
101
102
|
export { useMenuActions } from "./presentation/hooks/useMenuActions";
|
|
102
103
|
export { useExportActions } from "./presentation/hooks/useExportActions";
|
|
104
|
+
export { useCollageEditor } from "./presentation/hooks/useCollageEditor";
|
|
105
|
+
export type { UseCollageEditorReturn } from "./presentation/hooks/useCollageEditor";
|
|
103
106
|
|
|
104
107
|
// =============================================================================
|
|
105
108
|
// VIDEO PLAYER MODULE
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collage Layouts
|
|
3
|
+
* Grid definitions for collage editor
|
|
4
|
+
* Each cell: [x, y, width, height] as fractions of canvas (0–1)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface CollageLayout {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly count: number;
|
|
10
|
+
readonly grid: readonly [number, number, number, number][];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const COLLAGE_LAYOUTS: CollageLayout[] = [
|
|
14
|
+
{
|
|
15
|
+
id: "2h",
|
|
16
|
+
count: 2,
|
|
17
|
+
grid: [
|
|
18
|
+
[0, 0, 0.5, 1],
|
|
19
|
+
[0.5, 0, 0.5, 1],
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "2v",
|
|
24
|
+
count: 2,
|
|
25
|
+
grid: [
|
|
26
|
+
[0, 0, 1, 0.5],
|
|
27
|
+
[0, 0.5, 1, 0.5],
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "3a",
|
|
32
|
+
count: 3,
|
|
33
|
+
grid: [
|
|
34
|
+
[0, 0, 0.5, 1],
|
|
35
|
+
[0.5, 0, 0.5, 0.5],
|
|
36
|
+
[0.5, 0.5, 0.5, 0.5],
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "3b",
|
|
41
|
+
count: 3,
|
|
42
|
+
grid: [
|
|
43
|
+
[0, 0, 1, 0.5],
|
|
44
|
+
[0, 0.5, 0.5, 0.5],
|
|
45
|
+
[0.5, 0.5, 0.5, 0.5],
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "4",
|
|
50
|
+
count: 4,
|
|
51
|
+
grid: [
|
|
52
|
+
[0, 0, 0.5, 0.5],
|
|
53
|
+
[0.5, 0, 0.5, 0.5],
|
|
54
|
+
[0, 0.5, 0.5, 0.5],
|
|
55
|
+
[0.5, 0.5, 0.5, 0.5],
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "6",
|
|
60
|
+
count: 6,
|
|
61
|
+
grid: [
|
|
62
|
+
[0, 0, 0.333, 0.5],
|
|
63
|
+
[0.333, 0, 0.334, 0.5],
|
|
64
|
+
[0.667, 0, 0.333, 0.5],
|
|
65
|
+
[0, 0.5, 0.333, 0.5],
|
|
66
|
+
[0.333, 0.5, 0.334, 0.5],
|
|
67
|
+
[0.667, 0.5, 0.333, 0.5],
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export const DEFAULT_COLLAGE_LAYOUT = COLLAGE_LAYOUTS[0];
|
|
73
|
+
export const DEFAULT_COLLAGE_SPACING = 4;
|
|
74
|
+
export const DEFAULT_COLLAGE_BORDER_RADIUS = 8;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CollageEditorCanvas Component
|
|
3
|
+
* Collage layout canvas with image cells, layout picker, spacing and border radius controls
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
ScrollView,
|
|
10
|
+
TouchableOpacity,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
Dimensions,
|
|
13
|
+
} from "react-native";
|
|
14
|
+
import { Image } from "expo-image";
|
|
15
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
16
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
17
|
+
import { COLLAGE_LAYOUTS } from "../../infrastructure/constants/collage.constants";
|
|
18
|
+
import type { CollageLayout } from "../../infrastructure/constants/collage.constants";
|
|
19
|
+
|
|
20
|
+
const SCREEN_WIDTH = Dimensions.get("window").width;
|
|
21
|
+
|
|
22
|
+
interface CollageEditorCanvasProps {
|
|
23
|
+
layout: CollageLayout;
|
|
24
|
+
images: (string | null)[];
|
|
25
|
+
spacing: number;
|
|
26
|
+
borderRadius: number;
|
|
27
|
+
onSelectLayout: (layout: CollageLayout) => void;
|
|
28
|
+
onCellPress: (index: number) => void;
|
|
29
|
+
onSpacingChange: (value: number) => void;
|
|
30
|
+
onBorderRadiusChange: (value: number) => void;
|
|
31
|
+
t: (key: string) => string;
|
|
32
|
+
canvasSize?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const CollageEditorCanvas: React.FC<CollageEditorCanvasProps> = ({
|
|
36
|
+
layout,
|
|
37
|
+
images,
|
|
38
|
+
spacing,
|
|
39
|
+
borderRadius,
|
|
40
|
+
onSelectLayout,
|
|
41
|
+
onCellPress,
|
|
42
|
+
onSpacingChange,
|
|
43
|
+
onBorderRadiusChange,
|
|
44
|
+
t,
|
|
45
|
+
canvasSize,
|
|
46
|
+
}) => {
|
|
47
|
+
const tokens = useAppDesignTokens();
|
|
48
|
+
const size = canvasSize ?? SCREEN_WIDTH - tokens.spacing.md * 2;
|
|
49
|
+
|
|
50
|
+
const styles = useMemo(
|
|
51
|
+
() =>
|
|
52
|
+
StyleSheet.create({
|
|
53
|
+
canvas: {
|
|
54
|
+
width: size,
|
|
55
|
+
height: size,
|
|
56
|
+
alignSelf: "center",
|
|
57
|
+
position: "relative",
|
|
58
|
+
backgroundColor: tokens.colors.surface,
|
|
59
|
+
borderRadius: tokens.borders.radius.md,
|
|
60
|
+
overflow: "hidden",
|
|
61
|
+
},
|
|
62
|
+
cell: {
|
|
63
|
+
position: "absolute",
|
|
64
|
+
overflow: "hidden",
|
|
65
|
+
},
|
|
66
|
+
cellImage: {
|
|
67
|
+
width: "100%",
|
|
68
|
+
height: "100%",
|
|
69
|
+
},
|
|
70
|
+
cellEmpty: {
|
|
71
|
+
flex: 1,
|
|
72
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
73
|
+
alignItems: "center",
|
|
74
|
+
justifyContent: "center",
|
|
75
|
+
},
|
|
76
|
+
controls: {
|
|
77
|
+
paddingHorizontal: tokens.spacing.md,
|
|
78
|
+
paddingTop: tokens.spacing.md,
|
|
79
|
+
gap: tokens.spacing.sm,
|
|
80
|
+
},
|
|
81
|
+
controlRow: {
|
|
82
|
+
flexDirection: "row",
|
|
83
|
+
alignItems: "center",
|
|
84
|
+
justifyContent: "space-between",
|
|
85
|
+
},
|
|
86
|
+
stepper: {
|
|
87
|
+
flexDirection: "row",
|
|
88
|
+
alignItems: "center",
|
|
89
|
+
gap: tokens.spacing.sm,
|
|
90
|
+
},
|
|
91
|
+
stepBtn: {
|
|
92
|
+
width: 32,
|
|
93
|
+
height: 32,
|
|
94
|
+
borderRadius: 16,
|
|
95
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
96
|
+
alignItems: "center",
|
|
97
|
+
justifyContent: "center",
|
|
98
|
+
},
|
|
99
|
+
stepValue: {
|
|
100
|
+
minWidth: 28,
|
|
101
|
+
textAlign: "center",
|
|
102
|
+
},
|
|
103
|
+
layoutSection: {
|
|
104
|
+
paddingTop: tokens.spacing.sm,
|
|
105
|
+
},
|
|
106
|
+
layoutScroll: {
|
|
107
|
+
paddingHorizontal: tokens.spacing.md,
|
|
108
|
+
gap: tokens.spacing.sm,
|
|
109
|
+
},
|
|
110
|
+
layoutCard: {
|
|
111
|
+
width: 64,
|
|
112
|
+
alignItems: "center",
|
|
113
|
+
gap: tokens.spacing.xs,
|
|
114
|
+
},
|
|
115
|
+
layoutPreview: {
|
|
116
|
+
width: 52,
|
|
117
|
+
height: 52,
|
|
118
|
+
borderRadius: tokens.borders.radius.sm,
|
|
119
|
+
overflow: "hidden",
|
|
120
|
+
position: "relative",
|
|
121
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
122
|
+
borderWidth: 2,
|
|
123
|
+
borderColor: "transparent",
|
|
124
|
+
},
|
|
125
|
+
layoutPreviewActive: {
|
|
126
|
+
borderColor: tokens.colors.primary,
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
[tokens, size],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<View>
|
|
134
|
+
{/* Canvas */}
|
|
135
|
+
<View style={styles.canvas}>
|
|
136
|
+
{layout.grid.map((cell, index) => {
|
|
137
|
+
const [cx, cy, cw, ch] = cell;
|
|
138
|
+
const cellStyle = {
|
|
139
|
+
left: cx * size + spacing,
|
|
140
|
+
top: cy * size + spacing,
|
|
141
|
+
width: cw * size - spacing * 2,
|
|
142
|
+
height: ch * size - spacing * 2,
|
|
143
|
+
borderRadius,
|
|
144
|
+
};
|
|
145
|
+
return (
|
|
146
|
+
<TouchableOpacity
|
|
147
|
+
key={index}
|
|
148
|
+
style={[styles.cell, cellStyle]}
|
|
149
|
+
onPress={() => onCellPress(index)}
|
|
150
|
+
accessibilityLabel={`Cell ${index + 1}`}
|
|
151
|
+
accessibilityRole="button"
|
|
152
|
+
>
|
|
153
|
+
{images[index] ? (
|
|
154
|
+
<Image
|
|
155
|
+
source={{ uri: images[index]! }}
|
|
156
|
+
style={[styles.cellImage, { borderRadius }]}
|
|
157
|
+
contentFit="cover"
|
|
158
|
+
/>
|
|
159
|
+
) : (
|
|
160
|
+
<View style={styles.cellEmpty}>
|
|
161
|
+
<AtomicIcon name="add" size="md" color="textSecondary" />
|
|
162
|
+
</View>
|
|
163
|
+
)}
|
|
164
|
+
</TouchableOpacity>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</View>
|
|
168
|
+
|
|
169
|
+
{/* Spacing + Border Radius */}
|
|
170
|
+
<View style={styles.controls}>
|
|
171
|
+
<View style={styles.controlRow}>
|
|
172
|
+
<AtomicText type="labelSmall" color="textSecondary">
|
|
173
|
+
{t("editor.collage.spacing") || "Spacing"}
|
|
174
|
+
</AtomicText>
|
|
175
|
+
<View style={styles.stepper}>
|
|
176
|
+
<TouchableOpacity
|
|
177
|
+
style={styles.stepBtn}
|
|
178
|
+
onPress={() => onSpacingChange(Math.max(0, spacing - 2))}
|
|
179
|
+
accessibilityLabel="Decrease spacing"
|
|
180
|
+
accessibilityRole="button"
|
|
181
|
+
>
|
|
182
|
+
<AtomicIcon name="chevron-back" size="sm" color="textSecondary" />
|
|
183
|
+
</TouchableOpacity>
|
|
184
|
+
<AtomicText fontWeight="bold" style={styles.stepValue}>
|
|
185
|
+
{spacing}
|
|
186
|
+
</AtomicText>
|
|
187
|
+
<TouchableOpacity
|
|
188
|
+
style={styles.stepBtn}
|
|
189
|
+
onPress={() => onSpacingChange(Math.min(16, spacing + 2))}
|
|
190
|
+
accessibilityLabel="Increase spacing"
|
|
191
|
+
accessibilityRole="button"
|
|
192
|
+
>
|
|
193
|
+
<AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
|
|
194
|
+
</TouchableOpacity>
|
|
195
|
+
</View>
|
|
196
|
+
</View>
|
|
197
|
+
|
|
198
|
+
<View style={styles.controlRow}>
|
|
199
|
+
<AtomicText type="labelSmall" color="textSecondary">
|
|
200
|
+
{t("editor.collage.corners") || "Corners"}
|
|
201
|
+
</AtomicText>
|
|
202
|
+
<View style={styles.stepper}>
|
|
203
|
+
<TouchableOpacity
|
|
204
|
+
style={styles.stepBtn}
|
|
205
|
+
onPress={() => onBorderRadiusChange(Math.max(0, borderRadius - 4))}
|
|
206
|
+
accessibilityLabel="Decrease corner radius"
|
|
207
|
+
accessibilityRole="button"
|
|
208
|
+
>
|
|
209
|
+
<AtomicIcon name="chevron-back" size="sm" color="textSecondary" />
|
|
210
|
+
</TouchableOpacity>
|
|
211
|
+
<AtomicText fontWeight="bold" style={styles.stepValue}>
|
|
212
|
+
{borderRadius}
|
|
213
|
+
</AtomicText>
|
|
214
|
+
<TouchableOpacity
|
|
215
|
+
style={styles.stepBtn}
|
|
216
|
+
onPress={() => onBorderRadiusChange(Math.min(24, borderRadius + 4))}
|
|
217
|
+
accessibilityLabel="Increase corner radius"
|
|
218
|
+
accessibilityRole="button"
|
|
219
|
+
>
|
|
220
|
+
<AtomicIcon name="chevron-forward" size="sm" color="textSecondary" />
|
|
221
|
+
</TouchableOpacity>
|
|
222
|
+
</View>
|
|
223
|
+
</View>
|
|
224
|
+
</View>
|
|
225
|
+
|
|
226
|
+
{/* Layout Picker */}
|
|
227
|
+
<View style={styles.layoutSection}>
|
|
228
|
+
<AtomicText
|
|
229
|
+
type="labelSmall"
|
|
230
|
+
color="textSecondary"
|
|
231
|
+
style={{ paddingHorizontal: tokens.spacing.md, marginBottom: tokens.spacing.xs }}
|
|
232
|
+
>
|
|
233
|
+
{t("editor.collage.layout") || "Layout"}
|
|
234
|
+
</AtomicText>
|
|
235
|
+
<ScrollView
|
|
236
|
+
horizontal
|
|
237
|
+
showsHorizontalScrollIndicator={false}
|
|
238
|
+
contentContainerStyle={styles.layoutScroll}
|
|
239
|
+
>
|
|
240
|
+
{COLLAGE_LAYOUTS.map((l) => {
|
|
241
|
+
const isActive = layout.id === l.id;
|
|
242
|
+
return (
|
|
243
|
+
<TouchableOpacity
|
|
244
|
+
key={l.id}
|
|
245
|
+
style={styles.layoutCard}
|
|
246
|
+
onPress={() => onSelectLayout(l)}
|
|
247
|
+
accessibilityLabel={`Layout ${l.count} cells`}
|
|
248
|
+
accessibilityRole="button"
|
|
249
|
+
accessibilityState={{ selected: isActive }}
|
|
250
|
+
>
|
|
251
|
+
<View style={[styles.layoutPreview, isActive && styles.layoutPreviewActive]}>
|
|
252
|
+
{l.grid.map((cell, i) => {
|
|
253
|
+
const [lx, ly, lw, lh] = cell;
|
|
254
|
+
return (
|
|
255
|
+
<View
|
|
256
|
+
key={i}
|
|
257
|
+
style={{
|
|
258
|
+
position: "absolute",
|
|
259
|
+
left: lx * 52 + 2,
|
|
260
|
+
top: ly * 52 + 2,
|
|
261
|
+
width: lw * 52 - 4,
|
|
262
|
+
height: lh * 52 - 4,
|
|
263
|
+
backgroundColor: isActive
|
|
264
|
+
? tokens.colors.primary
|
|
265
|
+
: tokens.colors.surfaceVariant,
|
|
266
|
+
borderRadius: 2,
|
|
267
|
+
}}
|
|
268
|
+
/>
|
|
269
|
+
);
|
|
270
|
+
})}
|
|
271
|
+
</View>
|
|
272
|
+
<AtomicText
|
|
273
|
+
type="labelSmall"
|
|
274
|
+
color={isActive ? "primary" : "textSecondary"}
|
|
275
|
+
>
|
|
276
|
+
{l.count}
|
|
277
|
+
</AtomicText>
|
|
278
|
+
</TouchableOpacity>
|
|
279
|
+
);
|
|
280
|
+
})}
|
|
281
|
+
</ScrollView>
|
|
282
|
+
</View>
|
|
283
|
+
</View>
|
|
284
|
+
);
|
|
285
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCollageEditor Hook
|
|
3
|
+
* State management for collage editor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback } from "react";
|
|
7
|
+
import {
|
|
8
|
+
COLLAGE_LAYOUTS,
|
|
9
|
+
DEFAULT_COLLAGE_LAYOUT,
|
|
10
|
+
DEFAULT_COLLAGE_SPACING,
|
|
11
|
+
DEFAULT_COLLAGE_BORDER_RADIUS,
|
|
12
|
+
} from "../../infrastructure/constants/collage.constants";
|
|
13
|
+
import type { CollageLayout } from "../../infrastructure/constants/collage.constants";
|
|
14
|
+
|
|
15
|
+
export interface UseCollageEditorReturn {
|
|
16
|
+
layout: CollageLayout;
|
|
17
|
+
images: (string | null)[];
|
|
18
|
+
spacing: number;
|
|
19
|
+
borderRadius: number;
|
|
20
|
+
setLayout: (layout: CollageLayout) => void;
|
|
21
|
+
setImage: (index: number, uri: string) => void;
|
|
22
|
+
clearImage: (index: number) => void;
|
|
23
|
+
setSpacing: (value: number) => void;
|
|
24
|
+
setBorderRadius: (value: number) => void;
|
|
25
|
+
filledCount: number;
|
|
26
|
+
allLayouts: CollageLayout[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useCollageEditor(): UseCollageEditorReturn {
|
|
30
|
+
const [layout, setLayoutState] = useState<CollageLayout>(DEFAULT_COLLAGE_LAYOUT);
|
|
31
|
+
const [images, setImages] = useState<(string | null)[]>(
|
|
32
|
+
new Array(DEFAULT_COLLAGE_LAYOUT.count).fill(null),
|
|
33
|
+
);
|
|
34
|
+
const [spacing, setSpacing] = useState(DEFAULT_COLLAGE_SPACING);
|
|
35
|
+
const [borderRadius, setBorderRadius] = useState(DEFAULT_COLLAGE_BORDER_RADIUS);
|
|
36
|
+
|
|
37
|
+
const setLayout = useCallback((newLayout: CollageLayout) => {
|
|
38
|
+
setLayoutState(newLayout);
|
|
39
|
+
setImages(new Array(newLayout.count).fill(null));
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const setImage = useCallback((index: number, uri: string) => {
|
|
43
|
+
setImages((prev) => {
|
|
44
|
+
const next = [...prev];
|
|
45
|
+
next[index] = uri;
|
|
46
|
+
return next;
|
|
47
|
+
});
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const clearImage = useCallback((index: number) => {
|
|
51
|
+
setImages((prev) => {
|
|
52
|
+
const next = [...prev];
|
|
53
|
+
next[index] = null;
|
|
54
|
+
return next;
|
|
55
|
+
});
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const filledCount = images.filter((img) => img !== null).length;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
layout,
|
|
62
|
+
images,
|
|
63
|
+
spacing,
|
|
64
|
+
borderRadius,
|
|
65
|
+
setLayout,
|
|
66
|
+
setImage,
|
|
67
|
+
clearImage,
|
|
68
|
+
setSpacing,
|
|
69
|
+
setBorderRadius,
|
|
70
|
+
filledCount,
|
|
71
|
+
allLayouts: COLLAGE_LAYOUTS,
|
|
72
|
+
};
|
|
73
|
+
}
|