@umituz/react-native-video-editor 1.1.70 → 1.2.0
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 -6
- package/src/index.ts +0 -4
- package/src/presentation/hooks/useDraggableLayerGestures.ts +9 -17
- package/src/infrastructure/constants/animation-layer.constants.ts +0 -32
- package/src/presentation/components/AnimationEditor.tsx +0 -101
- 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.
|
|
3
|
+
"version": "1.2.0",
|
|
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,15 +43,11 @@
|
|
|
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": {
|
|
50
49
|
"react-native-gesture-handler": {
|
|
51
50
|
"optional": true
|
|
52
|
-
},
|
|
53
|
-
"react-native-reanimated": {
|
|
54
|
-
"optional": true
|
|
55
51
|
}
|
|
56
52
|
},
|
|
57
53
|
"devDependencies": {
|
|
@@ -74,7 +70,6 @@
|
|
|
74
70
|
"react": "19.1.0",
|
|
75
71
|
"react-native": "0.81.5",
|
|
76
72
|
"react-native-gesture-handler": "^2.30.0",
|
|
77
|
-
"react-native-reanimated": "^3.16.0",
|
|
78
73
|
"typescript": "~5.9.2"
|
|
79
74
|
},
|
|
80
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";
|
|
@@ -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
|
-
* AnimationEditor Component
|
|
3
|
-
* Main component for editing animation layers
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React from "react";
|
|
7
|
-
import { View, ScrollView, StyleSheet } from "react-native";
|
|
8
|
-
|
|
9
|
-
import type { Animation } from "../../domain/entities/video-project.types";
|
|
10
|
-
import { useAnimationLayerForm } from "../hooks/useAnimationLayerForm";
|
|
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";
|
|
22
|
-
|
|
23
|
-
interface AnimationEditorProps {
|
|
24
|
-
animation?: Animation;
|
|
25
|
-
onSave: (animation: Animation) => void;
|
|
26
|
-
onRemove?: () => void;
|
|
27
|
-
onCancel: () => void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const AnimationEditor: React.FC<AnimationEditorProps> = ({
|
|
31
|
-
animation,
|
|
32
|
-
onSave,
|
|
33
|
-
onRemove,
|
|
34
|
-
onCancel,
|
|
35
|
-
}) => {
|
|
36
|
-
const {
|
|
37
|
-
formState,
|
|
38
|
-
setAnimationType,
|
|
39
|
-
setDuration,
|
|
40
|
-
setDelay,
|
|
41
|
-
setEasing,
|
|
42
|
-
buildAnimationData,
|
|
43
|
-
} = useAnimationLayerForm(animation);
|
|
44
|
-
|
|
45
|
-
const handleSave = () => {
|
|
46
|
-
onSave(buildAnimationData());
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<View style={styles.container}>
|
|
51
|
-
<ScrollView showsVerticalScrollIndicator={false}>
|
|
52
|
-
<AnimationTypeSelector
|
|
53
|
-
selectedType={formState.animationType}
|
|
54
|
-
onTypeChange={setAnimationType}
|
|
55
|
-
/>
|
|
56
|
-
|
|
57
|
-
{formState.animationType !== "none" && (
|
|
58
|
-
<>
|
|
59
|
-
<ValueSelector
|
|
60
|
-
title="Duration"
|
|
61
|
-
value={formState.duration}
|
|
62
|
-
options={DURATIONS}
|
|
63
|
-
formatValue={(val) => `${val}ms`}
|
|
64
|
-
onValueChange={setDuration}
|
|
65
|
-
/>
|
|
66
|
-
|
|
67
|
-
<ValueSelector
|
|
68
|
-
title="Delay"
|
|
69
|
-
value={formState.delay}
|
|
70
|
-
options={DELAYS}
|
|
71
|
-
formatValue={(val) => `${val}ms`}
|
|
72
|
-
onValueChange={setDelay}
|
|
73
|
-
/>
|
|
74
|
-
|
|
75
|
-
<OptionSelector
|
|
76
|
-
title="Easing"
|
|
77
|
-
options={EASINGS}
|
|
78
|
-
selectedValue={formState.easing}
|
|
79
|
-
onValueChange={(value) => setEasing(value as Easing)}
|
|
80
|
-
/>
|
|
81
|
-
|
|
82
|
-
<AnimationInfoBanner />
|
|
83
|
-
</>
|
|
84
|
-
)}
|
|
85
|
-
</ScrollView>
|
|
86
|
-
|
|
87
|
-
<AnimationEditorActions
|
|
88
|
-
hasAnimation={!!animation}
|
|
89
|
-
onRemove={onRemove}
|
|
90
|
-
onCancel={onCancel}
|
|
91
|
-
onSave={handleSave}
|
|
92
|
-
/>
|
|
93
|
-
</View>
|
|
94
|
-
);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const styles = StyleSheet.create({
|
|
98
|
-
container: {
|
|
99
|
-
paddingVertical: 16,
|
|
100
|
-
},
|
|
101
|
-
});
|
|
@@ -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
|
-
}
|