@umituz/react-native-video-editor 1.1.71 → 1.2.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 +1 -3
- package/src/index.ts +0 -4
- package/src/presentation/components/AnimationEditor.tsx +225 -53
- package/src/presentation/components/text-layer/EditorActions.tsx +25 -0
- package/src/presentation/hooks/index.ts +29 -0
- package/src/presentation/hooks/useAnimationForm.tsx +98 -0
- package/src/presentation/hooks/useDraggableLayerGestures.ts +9 -17
- package/src/infrastructure/constants/animation-layer.constants.ts +0 -32
- package/src/presentation/components/animation-layer/AnimationEditorActions.tsx +0 -101
- package/src/presentation/components/animation-layer/AnimationInfoBanner.tsx +0 -40
- package/src/presentation/components/animation-layer/AnimationTypeSelector.tsx +0 -102
- package/src/presentation/hooks/useAnimationLayerForm.ts +0 -72
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.2.1",
|
|
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",
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"react": ">=18.2.0",
|
|
44
44
|
"react-native": ">=0.74.0",
|
|
45
45
|
"react-native-gesture-handler": ">=2.0.0",
|
|
46
|
-
"react-native-reanimated": ">=3.0.0",
|
|
47
46
|
"zustand": ">=4.0.0"
|
|
48
47
|
},
|
|
49
48
|
"peerDependenciesMeta": {
|
|
@@ -71,7 +70,6 @@
|
|
|
71
70
|
"react": "19.1.0",
|
|
72
71
|
"react-native": "0.81.5",
|
|
73
72
|
"react-native-gesture-handler": "^2.30.0",
|
|
74
|
-
"react-native-reanimated": "^3.16.0",
|
|
75
73
|
"typescript": "~5.9.2"
|
|
76
74
|
},
|
|
77
75
|
"publishConfig": {
|
package/src/index.ts
CHANGED
|
@@ -15,13 +15,11 @@ export type {
|
|
|
15
15
|
ImageLayer,
|
|
16
16
|
VideoLayer,
|
|
17
17
|
ShapeLayer,
|
|
18
|
-
Animation,
|
|
19
18
|
Audio,
|
|
20
19
|
ExportSettings,
|
|
21
20
|
AspectRatio,
|
|
22
21
|
LayerType,
|
|
23
22
|
TransitionType,
|
|
24
|
-
AnimationType,
|
|
25
23
|
Position,
|
|
26
24
|
Size,
|
|
27
25
|
Transition,
|
|
@@ -73,7 +71,6 @@ export { SceneActionsMenu } from "./presentation/components/SceneActionsMenu";
|
|
|
73
71
|
export { TextLayerEditor } from "./presentation/components/TextLayerEditor";
|
|
74
72
|
export { AudioEditor } from "./presentation/components/AudioEditor";
|
|
75
73
|
export { ShapeLayerEditor } from "./presentation/components/ShapeLayerEditor";
|
|
76
|
-
export { AnimationEditor } from "./presentation/components/AnimationEditor";
|
|
77
74
|
export { DraggableLayer } from "./presentation/components/DraggableLayer";
|
|
78
75
|
export { ImageLayerEditor } from "./presentation/components/ImageLayerEditor";
|
|
79
76
|
export { ExportDialog } from "./presentation/components/ExportDialog";
|
|
@@ -94,7 +91,6 @@ export { useEditorActions } from "./presentation/hooks/useEditorActions";
|
|
|
94
91
|
export { useTextLayerForm } from "./presentation/hooks/useTextLayerForm";
|
|
95
92
|
export { useImageLayerForm } from "./presentation/hooks/useImageLayerForm";
|
|
96
93
|
export { useShapeLayerForm } from "./presentation/hooks/useShapeLayerForm";
|
|
97
|
-
export { useAnimationLayerForm } from "./presentation/hooks/useAnimationLayerForm";
|
|
98
94
|
export { useAudioLayerForm } from "./presentation/hooks/useAudioLayerForm";
|
|
99
95
|
export { useTextLayerOperations } from "./presentation/hooks/useTextLayerOperations";
|
|
100
96
|
export { useImageLayerOperations } from "./presentation/hooks/useImageLayerOperations";
|
|
@@ -1,38 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AnimationEditor Component
|
|
3
|
-
* Main component for editing
|
|
3
|
+
* Main component for editing layer animations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React from "react";
|
|
6
|
+
import React, { useCallback } from "react";
|
|
7
7
|
import { View, ScrollView, StyleSheet } from "react-native";
|
|
8
|
-
|
|
8
|
+
import { useLocalization } from "@umituz/react-native-settings";
|
|
9
|
+
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
10
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
9
11
|
import type { Animation } from "../../domain/entities/video-project.types";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
DURATIONS,
|
|
13
|
-
DELAYS,
|
|
14
|
-
EASINGS,
|
|
15
|
-
type Easing,
|
|
16
|
-
} from "../../infrastructure/constants/animation-layer.constants";
|
|
17
|
-
import { ValueSelector } from "./shape-layer/ValueSelector";
|
|
18
|
-
import { OptionSelector } from "./text-layer/OptionSelector";
|
|
19
|
-
import { AnimationTypeSelector } from "./animation-layer/AnimationTypeSelector";
|
|
20
|
-
import { AnimationEditorActions } from "./animation-layer/AnimationEditorActions";
|
|
21
|
-
import { AnimationInfoBanner } from "./animation-layer/AnimationInfoBanner";
|
|
12
|
+
import { useAnimationForm } from "../hooks/useAnimationForm";
|
|
13
|
+
import { EditorActions } from "./text-layer/EditorActions";
|
|
22
14
|
|
|
23
15
|
interface AnimationEditorProps {
|
|
24
|
-
animation?: Animation;
|
|
25
|
-
onSave: (animation: Animation) => void;
|
|
26
|
-
onRemove?: () => void;
|
|
27
|
-
onCancel: () => void;
|
|
16
|
+
readonly animation?: Animation;
|
|
17
|
+
readonly onSave: (animation: Animation) => void;
|
|
18
|
+
readonly onRemove?: () => void;
|
|
19
|
+
readonly onCancel: () => void;
|
|
28
20
|
}
|
|
29
21
|
|
|
22
|
+
const ANIMATION_TYPES = [
|
|
23
|
+
{ label: "None", value: "none" as const },
|
|
24
|
+
{ label: "Fade", value: "fade" as const },
|
|
25
|
+
{ label: "Slide", value: "slide" as const },
|
|
26
|
+
{ label: "Bounce", value: "bounce" as const },
|
|
27
|
+
{ label: "Zoom", value: "zoom" as const },
|
|
28
|
+
{ label: "Rotate", value: "rotate" as const },
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
const EASING_OPTIONS = [
|
|
32
|
+
{ label: "Linear", value: "linear" as const },
|
|
33
|
+
{ label: "Ease In", value: "ease-in" as const },
|
|
34
|
+
{ label: "Ease Out", value: "ease-out" as const },
|
|
35
|
+
{ label: "Ease In Out", value: "ease-in-out" as const },
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
30
38
|
export const AnimationEditor: React.FC<AnimationEditorProps> = ({
|
|
31
39
|
animation,
|
|
32
40
|
onSave,
|
|
33
41
|
onRemove,
|
|
34
42
|
onCancel,
|
|
35
43
|
}) => {
|
|
44
|
+
const { t } = useLocalization();
|
|
45
|
+
const tokens = useAppDesignTokens();
|
|
36
46
|
const {
|
|
37
47
|
formState,
|
|
38
48
|
setAnimationType,
|
|
@@ -40,55 +50,183 @@ export const AnimationEditor: React.FC<AnimationEditorProps> = ({
|
|
|
40
50
|
setDelay,
|
|
41
51
|
setEasing,
|
|
42
52
|
buildAnimationData,
|
|
43
|
-
|
|
53
|
+
isValid,
|
|
54
|
+
} = useAnimationForm(animation);
|
|
44
55
|
|
|
45
|
-
const handleSave = () => {
|
|
56
|
+
const handleSave = useCallback(() => {
|
|
57
|
+
if (!isValid) return;
|
|
46
58
|
onSave(buildAnimationData());
|
|
47
|
-
};
|
|
59
|
+
}, [isValid, buildAnimationData, onSave]);
|
|
48
60
|
|
|
49
61
|
return (
|
|
50
62
|
<View style={styles.container}>
|
|
51
63
|
<ScrollView showsVerticalScrollIndicator={false}>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
{/* Animation Type Selector */}
|
|
65
|
+
<View style={styles.section}>
|
|
66
|
+
<AtomicText
|
|
67
|
+
type="labelMedium"
|
|
68
|
+
color="textSecondary"
|
|
69
|
+
style={{ marginBottom: tokens.spacing.sm }}
|
|
70
|
+
>
|
|
71
|
+
{t("editor.properties.animation_type") || "Animation Type"}
|
|
72
|
+
</AtomicText>
|
|
73
|
+
<View style={styles.optionsContainer}>
|
|
74
|
+
{ANIMATION_TYPES.map((option) => (
|
|
75
|
+
<View
|
|
76
|
+
key={option.value}
|
|
77
|
+
style={[
|
|
78
|
+
styles.optionButton,
|
|
79
|
+
formState.type === option.value && {
|
|
80
|
+
backgroundColor: tokens.colors.primary,
|
|
81
|
+
},
|
|
82
|
+
]}
|
|
83
|
+
>
|
|
84
|
+
<AtomicText
|
|
85
|
+
fontWeight="medium"
|
|
86
|
+
color={
|
|
87
|
+
formState.type === option.value
|
|
88
|
+
? "onPrimary"
|
|
89
|
+
: "textPrimary"
|
|
90
|
+
}
|
|
91
|
+
onPress={() => setAnimationType(option.value)}
|
|
92
|
+
>
|
|
93
|
+
{option.label}
|
|
94
|
+
</AtomicText>
|
|
95
|
+
</View>
|
|
96
|
+
))}
|
|
97
|
+
</View>
|
|
98
|
+
</View>
|
|
56
99
|
|
|
57
|
-
{
|
|
100
|
+
{/* Duration Selector */}
|
|
101
|
+
{formState.type !== "none" && (
|
|
58
102
|
<>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
<View style={styles.section}>
|
|
104
|
+
<AtomicText
|
|
105
|
+
type="labelMedium"
|
|
106
|
+
color="textSecondary"
|
|
107
|
+
style={{ marginBottom: tokens.spacing.sm }}
|
|
108
|
+
>
|
|
109
|
+
{t("editor.properties.duration") || "Duration"}
|
|
110
|
+
</AtomicText>
|
|
111
|
+
<View style={styles.sliderContainer}>
|
|
112
|
+
<AtomicText type="bodySmall" color="textSecondary">
|
|
113
|
+
{formState.duration}ms
|
|
114
|
+
</AtomicText>
|
|
115
|
+
</View>
|
|
116
|
+
<View style={styles.valueButtons}>
|
|
117
|
+
{[300, 500, 1000, 1500, 2000].map((value) => (
|
|
118
|
+
<View
|
|
119
|
+
key={value}
|
|
120
|
+
style={[
|
|
121
|
+
styles.valueButton,
|
|
122
|
+
formState.duration === value && {
|
|
123
|
+
backgroundColor: tokens.colors.primary,
|
|
124
|
+
},
|
|
125
|
+
]}
|
|
126
|
+
>
|
|
127
|
+
<AtomicText
|
|
128
|
+
type="labelSmall"
|
|
129
|
+
color={
|
|
130
|
+
formState.duration === value
|
|
131
|
+
? "onPrimary"
|
|
132
|
+
: "textPrimary"
|
|
133
|
+
}
|
|
134
|
+
onPress={() => setDuration(value)}
|
|
135
|
+
>
|
|
136
|
+
{value}ms
|
|
137
|
+
</AtomicText>
|
|
138
|
+
</View>
|
|
139
|
+
))}
|
|
140
|
+
</View>
|
|
141
|
+
</View>
|
|
66
142
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
143
|
+
{/* Delay Selector */}
|
|
144
|
+
<View style={styles.section}>
|
|
145
|
+
<AtomicText
|
|
146
|
+
type="labelMedium"
|
|
147
|
+
color="textSecondary"
|
|
148
|
+
style={{ marginBottom: tokens.spacing.sm }}
|
|
149
|
+
>
|
|
150
|
+
{t("editor.properties.delay") || "Delay"}
|
|
151
|
+
</AtomicText>
|
|
152
|
+
<View style={styles.sliderContainer}>
|
|
153
|
+
<AtomicText type="bodySmall" color="textSecondary">
|
|
154
|
+
{formState.delay || 0}ms
|
|
155
|
+
</AtomicText>
|
|
156
|
+
</View>
|
|
157
|
+
<View style={styles.valueButtons}>
|
|
158
|
+
{[0, 100, 200, 500, 1000].map((value) => (
|
|
159
|
+
<View
|
|
160
|
+
key={value}
|
|
161
|
+
style={[
|
|
162
|
+
styles.valueButton,
|
|
163
|
+
(formState.delay || 0) === value && {
|
|
164
|
+
backgroundColor: tokens.colors.primary,
|
|
165
|
+
},
|
|
166
|
+
]}
|
|
167
|
+
>
|
|
168
|
+
<AtomicText
|
|
169
|
+
type="labelSmall"
|
|
170
|
+
color={
|
|
171
|
+
(formState.delay || 0) === value
|
|
172
|
+
? "onPrimary"
|
|
173
|
+
: "textPrimary"
|
|
174
|
+
}
|
|
175
|
+
onPress={() => setDelay(value)}
|
|
176
|
+
>
|
|
177
|
+
{value}ms
|
|
178
|
+
</AtomicText>
|
|
179
|
+
</View>
|
|
180
|
+
))}
|
|
181
|
+
</View>
|
|
182
|
+
</View>
|
|
74
183
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
184
|
+
{/* Easing Selector */}
|
|
185
|
+
<View style={styles.section}>
|
|
186
|
+
<AtomicText
|
|
187
|
+
type="labelMedium"
|
|
188
|
+
color="textSecondary"
|
|
189
|
+
style={{ marginBottom: tokens.spacing.sm }}
|
|
190
|
+
>
|
|
191
|
+
{t("editor.properties.easing") || "Easing"}
|
|
192
|
+
</AtomicText>
|
|
193
|
+
<View style={styles.optionsContainer}>
|
|
194
|
+
{EASING_OPTIONS.map((option) => (
|
|
195
|
+
<View
|
|
196
|
+
key={option.value}
|
|
197
|
+
style={[
|
|
198
|
+
styles.optionButton,
|
|
199
|
+
formState.easing === option.value && {
|
|
200
|
+
backgroundColor: tokens.colors.primary,
|
|
201
|
+
},
|
|
202
|
+
]}
|
|
203
|
+
>
|
|
204
|
+
<AtomicText
|
|
205
|
+
fontWeight="medium"
|
|
206
|
+
color={
|
|
207
|
+
formState.easing === option.value
|
|
208
|
+
? "onPrimary"
|
|
209
|
+
: "textPrimary"
|
|
210
|
+
}
|
|
211
|
+
onPress={() => setEasing(option.value)}
|
|
212
|
+
>
|
|
213
|
+
{option.label}
|
|
214
|
+
</AtomicText>
|
|
215
|
+
</View>
|
|
216
|
+
))}
|
|
217
|
+
</View>
|
|
218
|
+
</View>
|
|
83
219
|
</>
|
|
84
220
|
)}
|
|
85
221
|
</ScrollView>
|
|
86
222
|
|
|
87
|
-
<
|
|
88
|
-
hasAnimation={!!animation}
|
|
89
|
-
onRemove={onRemove}
|
|
223
|
+
<EditorActions
|
|
90
224
|
onCancel={onCancel}
|
|
91
225
|
onSave={handleSave}
|
|
226
|
+
saveLabel={animation ? "Update Animation" : "Add Animation"}
|
|
227
|
+
isValid={isValid}
|
|
228
|
+
onRemove={onRemove}
|
|
229
|
+
removeLabel="Remove Animation"
|
|
92
230
|
/>
|
|
93
231
|
</View>
|
|
94
232
|
);
|
|
@@ -96,6 +234,40 @@ export const AnimationEditor: React.FC<AnimationEditorProps> = ({
|
|
|
96
234
|
|
|
97
235
|
const styles = StyleSheet.create({
|
|
98
236
|
container: {
|
|
99
|
-
|
|
237
|
+
maxHeight: "80%",
|
|
238
|
+
},
|
|
239
|
+
section: {
|
|
240
|
+
marginBottom: 20,
|
|
241
|
+
},
|
|
242
|
+
optionsContainer: {
|
|
243
|
+
flexDirection: "row",
|
|
244
|
+
flexWrap: "wrap",
|
|
245
|
+
gap: 8,
|
|
246
|
+
},
|
|
247
|
+
optionButton: {
|
|
248
|
+
paddingHorizontal: 12,
|
|
249
|
+
paddingVertical: 8,
|
|
250
|
+
borderRadius: 8,
|
|
251
|
+
borderWidth: 1,
|
|
252
|
+
borderColor: "#ccc",
|
|
253
|
+
minWidth: 80,
|
|
254
|
+
alignItems: "center",
|
|
255
|
+
},
|
|
256
|
+
sliderContainer: {
|
|
257
|
+
marginBottom: 12,
|
|
258
|
+
},
|
|
259
|
+
valueButtons: {
|
|
260
|
+
flexDirection: "row",
|
|
261
|
+
flexWrap: "wrap",
|
|
262
|
+
gap: 8,
|
|
263
|
+
},
|
|
264
|
+
valueButton: {
|
|
265
|
+
paddingHorizontal: 12,
|
|
266
|
+
paddingVertical: 8,
|
|
267
|
+
borderRadius: 8,
|
|
268
|
+
borderWidth: 1,
|
|
269
|
+
borderColor: "#ccc",
|
|
270
|
+
minWidth: 60,
|
|
271
|
+
alignItems: "center",
|
|
100
272
|
},
|
|
101
273
|
});
|
|
@@ -14,6 +14,8 @@ interface EditorActionsProps {
|
|
|
14
14
|
onSave: () => void;
|
|
15
15
|
saveLabel: string;
|
|
16
16
|
isValid: boolean;
|
|
17
|
+
onRemove?: () => void;
|
|
18
|
+
removeLabel?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export const EditorActions: React.FC<EditorActionsProps> = ({
|
|
@@ -21,6 +23,8 @@ export const EditorActions: React.FC<EditorActionsProps> = ({
|
|
|
21
23
|
onSave,
|
|
22
24
|
saveLabel,
|
|
23
25
|
isValid,
|
|
26
|
+
onRemove,
|
|
27
|
+
removeLabel,
|
|
24
28
|
}) => {
|
|
25
29
|
const tokens = useAppDesignTokens();
|
|
26
30
|
const { t } = useLocalization();
|
|
@@ -29,6 +33,24 @@ export const EditorActions: React.FC<EditorActionsProps> = ({
|
|
|
29
33
|
<View
|
|
30
34
|
style={[styles.actions, { borderTopColor: tokens.colors.borderLight }]}
|
|
31
35
|
>
|
|
36
|
+
{onRemove && (
|
|
37
|
+
<TouchableOpacity
|
|
38
|
+
style={[
|
|
39
|
+
styles.actionButton,
|
|
40
|
+
styles.removeButton,
|
|
41
|
+
{ borderColor: tokens.colors.error },
|
|
42
|
+
]}
|
|
43
|
+
onPress={onRemove}
|
|
44
|
+
>
|
|
45
|
+
<AtomicText
|
|
46
|
+
type="bodyMedium"
|
|
47
|
+
style={{ color: tokens.colors.error }}
|
|
48
|
+
>
|
|
49
|
+
{removeLabel || t("common.buttons.remove")}
|
|
50
|
+
</AtomicText>
|
|
51
|
+
</TouchableOpacity>
|
|
52
|
+
)}
|
|
53
|
+
|
|
32
54
|
<TouchableOpacity
|
|
33
55
|
style={[
|
|
34
56
|
styles.actionButton,
|
|
@@ -83,6 +105,9 @@ const styles = StyleSheet.create({
|
|
|
83
105
|
alignItems: "center",
|
|
84
106
|
justifyContent: "center",
|
|
85
107
|
},
|
|
108
|
+
removeButton: {
|
|
109
|
+
borderWidth: 1,
|
|
110
|
+
},
|
|
86
111
|
cancelButton: {
|
|
87
112
|
borderWidth: 1,
|
|
88
113
|
},
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation Hooks
|
|
3
|
+
* All hooks for the video editor presentation layer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { useAnimationForm } from "./useAnimationForm";
|
|
7
|
+
export { useAudioLayerForm } from "./useAudioLayerForm";
|
|
8
|
+
export { useCollageEditor } from "./useCollageEditor";
|
|
9
|
+
export { useDraggableLayerGestures } from "./useDraggableLayerGestures";
|
|
10
|
+
export { useEditorActions } from "./useEditorActions";
|
|
11
|
+
export { useEditorBottomSheet } from "./useEditorBottomSheet";
|
|
12
|
+
export { useEditorHistory } from "./useEditorHistory";
|
|
13
|
+
export { useEditorLayers } from "./useEditorLayers";
|
|
14
|
+
export { useEditorPlayback } from "./useEditorPlayback";
|
|
15
|
+
export { useEditorScenes } from "./useEditorScenes";
|
|
16
|
+
export { useExport } from "./useExport";
|
|
17
|
+
export { useExportActions } from "./useExportActions";
|
|
18
|
+
export { useExportForm } from "./useExportForm";
|
|
19
|
+
export { useImageLayerForm } from "./useImageLayerForm";
|
|
20
|
+
export { useImageLayerOperations } from "./useImageLayerOperations";
|
|
21
|
+
export { useLayerActions } from "./useLayerActions";
|
|
22
|
+
export { useLayerManipulation } from "./useLayerManipulation";
|
|
23
|
+
export { useMenuActions } from "./useMenuActions";
|
|
24
|
+
export { useSceneActions } from "./useSceneActions";
|
|
25
|
+
export { useShapeLayerForm } from "./useShapeLayerForm";
|
|
26
|
+
export { useShapeLayerOperations } from "./useShapeLayerOperations";
|
|
27
|
+
export { useSubtitleEditor } from "./useSubtitleEditor";
|
|
28
|
+
export { useTextLayerForm } from "./useTextLayerForm";
|
|
29
|
+
export { useTextLayerOperations } from "./useTextLayerOperations";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAnimationForm Hook
|
|
3
|
+
* Manages form state for animation editing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useMemo } from "react";
|
|
7
|
+
import type { Animation, AnimationType } from "../../domain/entities/video-project.types";
|
|
8
|
+
|
|
9
|
+
interface AnimationFormState {
|
|
10
|
+
type: AnimationType;
|
|
11
|
+
duration: number;
|
|
12
|
+
delay?: number;
|
|
13
|
+
easing: "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UseAnimationFormParams {
|
|
17
|
+
animation?: Animation;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface UseAnimationFormReturn {
|
|
21
|
+
formState: AnimationFormState;
|
|
22
|
+
setAnimationType: (type: AnimationType) => void;
|
|
23
|
+
setDuration: (duration: number) => void;
|
|
24
|
+
setDelay: (delay: number) => void;
|
|
25
|
+
setEasing: (easing: "linear" | "ease-in" | "ease-out" | "ease-in-out") => void;
|
|
26
|
+
buildAnimationData: () => Animation;
|
|
27
|
+
isValid: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_ANIMATION: AnimationFormState = {
|
|
31
|
+
type: "none",
|
|
32
|
+
duration: 500,
|
|
33
|
+
delay: 0,
|
|
34
|
+
easing: "ease-in-out",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function useAnimationForm({
|
|
38
|
+
animation,
|
|
39
|
+
}: UseAnimationFormParams = {}): UseAnimationFormReturn {
|
|
40
|
+
const formState = useMemo<AnimationFormState>(() => {
|
|
41
|
+
if (!animation) {
|
|
42
|
+
return DEFAULT_ANIMATION;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
type: animation.type,
|
|
47
|
+
duration: animation.duration,
|
|
48
|
+
delay: animation.delay,
|
|
49
|
+
easing: animation.easing || "ease-in-out",
|
|
50
|
+
};
|
|
51
|
+
}, [animation]);
|
|
52
|
+
|
|
53
|
+
const setAnimationType = useCallback((type: AnimationType) => {
|
|
54
|
+
(formState as any).type = type;
|
|
55
|
+
}, [formState]);
|
|
56
|
+
|
|
57
|
+
const setDuration = useCallback((duration: number) => {
|
|
58
|
+
(formState as any).duration = duration;
|
|
59
|
+
}, [formState]);
|
|
60
|
+
|
|
61
|
+
const setDelay = useCallback((delay: number) => {
|
|
62
|
+
(formState as any).delay = delay;
|
|
63
|
+
}, [formState]);
|
|
64
|
+
|
|
65
|
+
const setEasing = useCallback((
|
|
66
|
+
easing: "linear" | "ease-in" | "ease-out" | "ease-in-out"
|
|
67
|
+
) => {
|
|
68
|
+
(formState as any).easing = easing;
|
|
69
|
+
}, [formState]);
|
|
70
|
+
|
|
71
|
+
const buildAnimationData = useCallback((): Animation => {
|
|
72
|
+
const data: Animation = {
|
|
73
|
+
type: formState.type,
|
|
74
|
+
duration: formState.duration,
|
|
75
|
+
easing: formState.easing,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (formState.delay && formState.delay > 0) {
|
|
79
|
+
data.delay = formState.delay;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return data;
|
|
83
|
+
}, [formState]);
|
|
84
|
+
|
|
85
|
+
const isValid = useMemo(() => {
|
|
86
|
+
return formState.type !== "none" && formState.duration > 0;
|
|
87
|
+
}, [formState]);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
formState,
|
|
91
|
+
setAnimationType,
|
|
92
|
+
setDuration,
|
|
93
|
+
setDelay,
|
|
94
|
+
setEasing,
|
|
95
|
+
buildAnimationData,
|
|
96
|
+
isValid,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
import { useState, useRef, useCallback, useEffect } from "react";
|
|
8
8
|
import { Gesture } from "react-native-gesture-handler";
|
|
9
|
-
// @ts-ignore - react-native-reanimated is an optional peer dependency
|
|
10
|
-
import { runOnJS } from "react-native-reanimated";
|
|
11
9
|
|
|
12
10
|
interface UseDraggableLayerGesturesParams {
|
|
13
11
|
initialX: number;
|
|
@@ -77,23 +75,20 @@ export function useDraggableLayerGestures({
|
|
|
77
75
|
const gestureHandler = Gesture.Pan()
|
|
78
76
|
.onStart(() => {
|
|
79
77
|
startRef.current = { ...state, x: state.x, y: state.y };
|
|
80
|
-
|
|
78
|
+
onSelect();
|
|
81
79
|
})
|
|
82
80
|
.onUpdate((event) => {
|
|
83
|
-
'worklet';
|
|
84
81
|
const newX = startRef.current.x + event.translationX;
|
|
85
82
|
const newY = startRef.current.y + event.translationY;
|
|
86
|
-
|
|
87
|
-
runOnJS(setState)({ x: newX, y: newY, width: state.width, height: state.height });
|
|
83
|
+
setState({ x: newX, y: newY, width: state.width, height: state.height });
|
|
88
84
|
})
|
|
89
85
|
.onEnd(() => {
|
|
90
|
-
'worklet';
|
|
91
86
|
const clampedX = clamp(state.x, 0, canvasWidth - state.width);
|
|
92
87
|
const clampedY = clamp(state.y, 0, canvasHeight - state.height);
|
|
93
88
|
const newX = (clampedX / canvasWidth) * 100;
|
|
94
89
|
const newY = (clampedY / canvasHeight) * 100;
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
setState({ x: clampedX, y: clampedY, width: state.width, height: state.height });
|
|
91
|
+
onPositionChange(newX, newY);
|
|
97
92
|
});
|
|
98
93
|
|
|
99
94
|
const createResizeHandler = (
|
|
@@ -102,12 +97,10 @@ export function useDraggableLayerGestures({
|
|
|
102
97
|
) => {
|
|
103
98
|
return Gesture.Pan()
|
|
104
99
|
.onStart(() => {
|
|
105
|
-
'worklet';
|
|
106
100
|
startRef.current = { ...state };
|
|
107
|
-
|
|
101
|
+
onSelect();
|
|
108
102
|
})
|
|
109
103
|
.onUpdate((event) => {
|
|
110
|
-
'worklet';
|
|
111
104
|
const newWidth = Math.max(MIN_SIZE, startRef.current.width + deltaX(event.translationX));
|
|
112
105
|
const newHeight = Math.max(MIN_SIZE, startRef.current.height + deltaY(event.translationY));
|
|
113
106
|
const clampedWidth = Math.min(newWidth, canvasWidth - startRef.current.x);
|
|
@@ -123,10 +116,9 @@ export function useDraggableLayerGestures({
|
|
|
123
116
|
newY = Math.max(0, startRef.current.y + (startRef.current.height - clampedHeight));
|
|
124
117
|
}
|
|
125
118
|
|
|
126
|
-
|
|
119
|
+
setState({ x: newX, y: newY, width: clampedWidth, height: clampedHeight });
|
|
127
120
|
})
|
|
128
121
|
.onEnd(() => {
|
|
129
|
-
'worklet';
|
|
130
122
|
// Clamp position to canvas bounds
|
|
131
123
|
const clampedX = Math.max(0, Math.min(state.x, canvasWidth - state.width));
|
|
132
124
|
const clampedY = Math.max(0, Math.min(state.y, canvasHeight - state.height));
|
|
@@ -136,9 +128,9 @@ export function useDraggableLayerGestures({
|
|
|
136
128
|
const newX = (clampedX / canvasWidth) * 100;
|
|
137
129
|
const newY = (clampedY / canvasHeight) * 100;
|
|
138
130
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
131
|
+
onSizeChange(newWidth, newHeight);
|
|
132
|
+
onPositionChange(newX, newY);
|
|
133
|
+
setState({ x: clampedX, y: clampedY, width: state.width, height: state.height });
|
|
142
134
|
});
|
|
143
135
|
};
|
|
144
136
|
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Animation Layer Constants
|
|
3
|
-
* Centralized constants for animation layer editor
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { AnimationType } from "../../domain/entities/video-project.types";
|
|
7
|
-
|
|
8
|
-
export type Easing = "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
9
|
-
|
|
10
|
-
export const ANIMATION_TYPES: {
|
|
11
|
-
type: AnimationType;
|
|
12
|
-
label: string;
|
|
13
|
-
icon: string;
|
|
14
|
-
}[] = [
|
|
15
|
-
{ type: "none", label: "None", icon: "Ban" },
|
|
16
|
-
{ type: "fade", label: "Fade", icon: "Eye" },
|
|
17
|
-
{ type: "slide", label: "Slide", icon: "MoveRight" },
|
|
18
|
-
{ type: "bounce", label: "Bounce", icon: "ArrowUp" },
|
|
19
|
-
{ type: "zoom", label: "Zoom", icon: "Maximize2" },
|
|
20
|
-
{ type: "rotate", label: "Rotate", icon: "RotateCw" },
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
export const DURATIONS = [300, 500, 800, 1000, 1500, 2000];
|
|
24
|
-
|
|
25
|
-
export const DELAYS = [0, 200, 500, 1000];
|
|
26
|
-
|
|
27
|
-
export const EASINGS: { value: Easing; label: string }[] = [
|
|
28
|
-
{ value: "linear", label: "Linear" },
|
|
29
|
-
{ value: "ease-in", label: "Ease In" },
|
|
30
|
-
{ value: "ease-out", label: "Ease Out" },
|
|
31
|
-
{ value: "ease-in-out", label: "Ease In-Out" },
|
|
32
|
-
];
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AnimationEditorActions Component
|
|
3
|
-
* Action buttons for animation editor
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React from "react";
|
|
7
|
-
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
|
-
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
9
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
|
-
|
|
11
|
-
interface AnimationEditorActionsProps {
|
|
12
|
-
hasAnimation: boolean;
|
|
13
|
-
onRemove?: () => void;
|
|
14
|
-
onCancel: () => void;
|
|
15
|
-
onSave: () => void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const AnimationEditorActions: React.FC<AnimationEditorActionsProps> = ({
|
|
19
|
-
hasAnimation,
|
|
20
|
-
onRemove,
|
|
21
|
-
onCancel,
|
|
22
|
-
onSave,
|
|
23
|
-
}) => {
|
|
24
|
-
const tokens = useAppDesignTokens();
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<View style={styles.actions}>
|
|
28
|
-
{hasAnimation && onRemove && (
|
|
29
|
-
<TouchableOpacity
|
|
30
|
-
style={[
|
|
31
|
-
styles.actionButton,
|
|
32
|
-
styles.removeButton,
|
|
33
|
-
{ borderColor: tokens.colors.error },
|
|
34
|
-
]}
|
|
35
|
-
onPress={onRemove}
|
|
36
|
-
>
|
|
37
|
-
<AtomicIcon name="trash-outline" size="sm" color="error" />
|
|
38
|
-
</TouchableOpacity>
|
|
39
|
-
)}
|
|
40
|
-
|
|
41
|
-
<TouchableOpacity
|
|
42
|
-
style={[
|
|
43
|
-
styles.actionButton,
|
|
44
|
-
styles.cancelButton,
|
|
45
|
-
{ borderColor: tokens.colors.borderLight },
|
|
46
|
-
]}
|
|
47
|
-
onPress={onCancel}
|
|
48
|
-
>
|
|
49
|
-
<AtomicText
|
|
50
|
-
type="bodyMedium"
|
|
51
|
-
style={{ color: tokens.colors.textSecondary }}
|
|
52
|
-
>
|
|
53
|
-
Cancel
|
|
54
|
-
</AtomicText>
|
|
55
|
-
</TouchableOpacity>
|
|
56
|
-
|
|
57
|
-
<TouchableOpacity
|
|
58
|
-
style={[
|
|
59
|
-
styles.actionButton,
|
|
60
|
-
styles.saveButton,
|
|
61
|
-
{ backgroundColor: tokens.colors.primary },
|
|
62
|
-
]}
|
|
63
|
-
onPress={onSave}
|
|
64
|
-
>
|
|
65
|
-
<AtomicIcon name="checkmark-outline" size="sm" color="onSurface" />
|
|
66
|
-
<AtomicText
|
|
67
|
-
type="bodyMedium"
|
|
68
|
-
style={{ color: tokens.colors.onPrimary, fontWeight: "600", marginLeft: 6 }}
|
|
69
|
-
>
|
|
70
|
-
Apply
|
|
71
|
-
</AtomicText>
|
|
72
|
-
</TouchableOpacity>
|
|
73
|
-
</View>
|
|
74
|
-
);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const styles = StyleSheet.create({
|
|
78
|
-
actions: {
|
|
79
|
-
flexDirection: "row",
|
|
80
|
-
gap: 8,
|
|
81
|
-
},
|
|
82
|
-
actionButton: {
|
|
83
|
-
flex: 1,
|
|
84
|
-
flexDirection: "row",
|
|
85
|
-
paddingVertical: 12,
|
|
86
|
-
borderRadius: 12,
|
|
87
|
-
alignItems: "center",
|
|
88
|
-
justifyContent: "center",
|
|
89
|
-
},
|
|
90
|
-
removeButton: {
|
|
91
|
-
flex: 0,
|
|
92
|
-
paddingHorizontal: 16,
|
|
93
|
-
borderWidth: 1,
|
|
94
|
-
},
|
|
95
|
-
cancelButton: {
|
|
96
|
-
borderWidth: 1,
|
|
97
|
-
},
|
|
98
|
-
saveButton: {
|
|
99
|
-
// backgroundColor set dynamically
|
|
100
|
-
},
|
|
101
|
-
});
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AnimationInfoBanner Component
|
|
3
|
-
* Info banner for animation editor
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React from "react";
|
|
7
|
-
import { View, StyleSheet } from "react-native";
|
|
8
|
-
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
9
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
|
-
|
|
11
|
-
export const AnimationInfoBanner: React.FC = () => {
|
|
12
|
-
const tokens = useAppDesignTokens();
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<View
|
|
16
|
-
style={[
|
|
17
|
-
styles.infoBanner,
|
|
18
|
-
{ backgroundColor: tokens.colors.primary + "20" },
|
|
19
|
-
]}
|
|
20
|
-
>
|
|
21
|
-
<AtomicIcon name="information-circle-outline" size="sm" color="primary" />
|
|
22
|
-
<AtomicText
|
|
23
|
-
type="labelSmall"
|
|
24
|
-
style={{ color: tokens.colors.primary, marginLeft: 8, flex: 1 }}
|
|
25
|
-
>
|
|
26
|
-
Animation will play when the layer first appears in the scene
|
|
27
|
-
</AtomicText>
|
|
28
|
-
</View>
|
|
29
|
-
);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const styles = StyleSheet.create({
|
|
33
|
-
infoBanner: {
|
|
34
|
-
flexDirection: "row",
|
|
35
|
-
alignItems: "center",
|
|
36
|
-
padding: 12,
|
|
37
|
-
borderRadius: 8,
|
|
38
|
-
marginBottom: 8,
|
|
39
|
-
},
|
|
40
|
-
});
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AnimationTypeSelector Component
|
|
3
|
-
* Animation type selector for animation layer
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React from "react";
|
|
7
|
-
import { View, ScrollView, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
|
-
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
9
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
|
-
import { ANIMATION_TYPES } from "../../../infrastructure/constants/animation-layer.constants";
|
|
11
|
-
import type { AnimationType } from "../../../domain/entities/video-project.types";
|
|
12
|
-
|
|
13
|
-
interface AnimationTypeSelectorProps {
|
|
14
|
-
selectedType: AnimationType;
|
|
15
|
-
onTypeChange: (type: AnimationType) => void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const AnimationTypeSelector: React.FC<AnimationTypeSelectorProps> = ({
|
|
19
|
-
selectedType,
|
|
20
|
-
onTypeChange,
|
|
21
|
-
}) => {
|
|
22
|
-
const tokens = useAppDesignTokens();
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<View style={styles.section}>
|
|
26
|
-
<AtomicText
|
|
27
|
-
type="bodyMedium"
|
|
28
|
-
style={{
|
|
29
|
-
color: tokens.colors.textPrimary,
|
|
30
|
-
fontWeight: "600",
|
|
31
|
-
marginBottom: 12,
|
|
32
|
-
}}
|
|
33
|
-
>
|
|
34
|
-
Animation Type
|
|
35
|
-
</AtomicText>
|
|
36
|
-
|
|
37
|
-
<ScrollView
|
|
38
|
-
horizontal
|
|
39
|
-
showsHorizontalScrollIndicator={false}
|
|
40
|
-
style={styles.animationTypesScroll}
|
|
41
|
-
>
|
|
42
|
-
{ANIMATION_TYPES.map((anim) => (
|
|
43
|
-
<TouchableOpacity
|
|
44
|
-
key={anim.type}
|
|
45
|
-
style={[
|
|
46
|
-
styles.animationTypeCard,
|
|
47
|
-
{
|
|
48
|
-
backgroundColor:
|
|
49
|
-
selectedType === anim.type
|
|
50
|
-
? tokens.colors.primary
|
|
51
|
-
: tokens.colors.surface,
|
|
52
|
-
borderColor:
|
|
53
|
-
selectedType === anim.type
|
|
54
|
-
? tokens.colors.primary
|
|
55
|
-
: tokens.colors.borderLight,
|
|
56
|
-
},
|
|
57
|
-
]}
|
|
58
|
-
onPress={() => onTypeChange(anim.type)}
|
|
59
|
-
>
|
|
60
|
-
<AtomicIcon
|
|
61
|
-
name={anim.icon as "Ban" | "Eye" | "MoveRight" | "ArrowUp" | "Maximize2" | "RotateCw"}
|
|
62
|
-
size="md"
|
|
63
|
-
color={selectedType === anim.type ? "onSurface" : "primary"}
|
|
64
|
-
/>
|
|
65
|
-
<AtomicText
|
|
66
|
-
type="labelSmall"
|
|
67
|
-
style={{
|
|
68
|
-
color:
|
|
69
|
-
selectedType === anim.type
|
|
70
|
-
? tokens.colors.onPrimary
|
|
71
|
-
: tokens.colors.textPrimary,
|
|
72
|
-
marginTop: 6,
|
|
73
|
-
fontWeight: selectedType === anim.type ? "600" : "400",
|
|
74
|
-
}}
|
|
75
|
-
>
|
|
76
|
-
{anim.label}
|
|
77
|
-
</AtomicText>
|
|
78
|
-
</TouchableOpacity>
|
|
79
|
-
))}
|
|
80
|
-
</ScrollView>
|
|
81
|
-
</View>
|
|
82
|
-
);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const styles = StyleSheet.create({
|
|
86
|
-
section: {
|
|
87
|
-
marginBottom: 24,
|
|
88
|
-
},
|
|
89
|
-
animationTypesScroll: {
|
|
90
|
-
marginHorizontal: -16,
|
|
91
|
-
paddingHorizontal: 16,
|
|
92
|
-
},
|
|
93
|
-
animationTypeCard: {
|
|
94
|
-
width: 90,
|
|
95
|
-
padding: 12,
|
|
96
|
-
borderRadius: 12,
|
|
97
|
-
borderWidth: 2,
|
|
98
|
-
alignItems: "center",
|
|
99
|
-
justifyContent: "center",
|
|
100
|
-
marginRight: 12,
|
|
101
|
-
},
|
|
102
|
-
});
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useAnimationLayerForm Hook
|
|
3
|
-
* Manages form state for animation layer editor
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useState, useCallback } from "react";
|
|
7
|
-
import type { Animation, AnimationType } from "../../domain/entities/video-project.types";
|
|
8
|
-
import type { Easing } from "../../infrastructure/constants/animation-layer.constants";
|
|
9
|
-
|
|
10
|
-
interface AnimationLayerFormState {
|
|
11
|
-
animationType: AnimationType;
|
|
12
|
-
duration: number;
|
|
13
|
-
delay: number;
|
|
14
|
-
easing: Easing;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface UseAnimationLayerFormReturn {
|
|
18
|
-
formState: AnimationLayerFormState;
|
|
19
|
-
setAnimationType: (type: AnimationType) => void;
|
|
20
|
-
setDuration: (duration: number) => void;
|
|
21
|
-
setDelay: (delay: number) => void;
|
|
22
|
-
setEasing: (easing: Easing) => void;
|
|
23
|
-
buildAnimationData: () => Animation;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Hook for managing animation layer form state
|
|
28
|
-
*/
|
|
29
|
-
export function useAnimationLayerForm(
|
|
30
|
-
initialAnimation?: Animation,
|
|
31
|
-
): UseAnimationLayerFormReturn {
|
|
32
|
-
const [formState, setFormState] = useState<AnimationLayerFormState>({
|
|
33
|
-
animationType: initialAnimation?.type || "fade",
|
|
34
|
-
duration: initialAnimation?.duration || 500,
|
|
35
|
-
delay: initialAnimation?.delay || 0,
|
|
36
|
-
easing: initialAnimation?.easing || "ease-in-out",
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const setAnimationType = useCallback((type: AnimationType) => {
|
|
40
|
-
setFormState((prev) => ({ ...prev, animationType: type }));
|
|
41
|
-
}, []);
|
|
42
|
-
|
|
43
|
-
const setDuration = useCallback((duration: number) => {
|
|
44
|
-
setFormState((prev) => ({ ...prev, duration }));
|
|
45
|
-
}, []);
|
|
46
|
-
|
|
47
|
-
const setDelay = useCallback((delay: number) => {
|
|
48
|
-
setFormState((prev) => ({ ...prev, delay }));
|
|
49
|
-
}, []);
|
|
50
|
-
|
|
51
|
-
const setEasing = useCallback((easing: Easing) => {
|
|
52
|
-
setFormState((prev) => ({ ...prev, easing }));
|
|
53
|
-
}, []);
|
|
54
|
-
|
|
55
|
-
const buildAnimationData = useCallback((): Animation => {
|
|
56
|
-
return {
|
|
57
|
-
type: formState.animationType,
|
|
58
|
-
duration: formState.duration,
|
|
59
|
-
delay: formState.delay,
|
|
60
|
-
easing: formState.easing,
|
|
61
|
-
};
|
|
62
|
-
}, [formState]);
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
formState,
|
|
66
|
-
setAnimationType,
|
|
67
|
-
setDuration,
|
|
68
|
-
setDelay,
|
|
69
|
-
setEasing,
|
|
70
|
-
buildAnimationData,
|
|
71
|
-
};
|
|
72
|
-
}
|