@umituz/react-native-video-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.
Files changed (97) hide show
  1. package/README.md +92 -0
  2. package/package.json +48 -0
  3. package/src/domain/entities/index.ts +50 -0
  4. package/src/domain/entities/video-project.types.ts +153 -0
  5. package/src/index.ts +100 -0
  6. package/src/infrastructure/constants/animation-layer.constants.ts +32 -0
  7. package/src/infrastructure/constants/audio-layer.constants.ts +14 -0
  8. package/src/infrastructure/constants/export.constants.ts +28 -0
  9. package/src/infrastructure/constants/image-layer.constants.ts +12 -0
  10. package/src/infrastructure/constants/index.ts +11 -0
  11. package/src/infrastructure/constants/shape-layer.constants.ts +29 -0
  12. package/src/infrastructure/constants/text-layer.constants.ts +40 -0
  13. package/src/infrastructure/services/export-orchestrator.service.ts +122 -0
  14. package/src/infrastructure/services/image-layer-operations.service.ts +108 -0
  15. package/src/infrastructure/services/layer-manipulation.service.ts +93 -0
  16. package/src/infrastructure/services/layer-operations/index.ts +9 -0
  17. package/src/infrastructure/services/layer-operations/layer-delete.service.ts +47 -0
  18. package/src/infrastructure/services/layer-operations/layer-duplicate.service.ts +66 -0
  19. package/src/infrastructure/services/layer-operations/layer-order.service.ts +82 -0
  20. package/src/infrastructure/services/layer-operations/layer-transform.service.ts +160 -0
  21. package/src/infrastructure/services/layer-operations.service.ts +198 -0
  22. package/src/infrastructure/services/scene-operations.service.ts +166 -0
  23. package/src/infrastructure/services/shape-layer-operations.service.ts +65 -0
  24. package/src/infrastructure/services/text-layer-operations.service.ts +114 -0
  25. package/src/presentation/components/AnimationEditor.tsx +103 -0
  26. package/src/presentation/components/AudioEditor.tsx +144 -0
  27. package/src/presentation/components/DraggableLayer.tsx +110 -0
  28. package/src/presentation/components/EditorHeader.tsx +107 -0
  29. package/src/presentation/components/EditorPreviewArea.tsx +221 -0
  30. package/src/presentation/components/EditorTimeline.tsx +136 -0
  31. package/src/presentation/components/EditorToolPanel.tsx +180 -0
  32. package/src/presentation/components/ExportDialog.tsx +135 -0
  33. package/src/presentation/components/ImageLayerEditor.tsx +95 -0
  34. package/src/presentation/components/LayerActionsMenu.tsx +197 -0
  35. package/src/presentation/components/SceneActionsMenu.tsx +69 -0
  36. package/src/presentation/components/ShapeLayerEditor.tsx +108 -0
  37. package/src/presentation/components/TextLayerEditor.tsx +104 -0
  38. package/src/presentation/components/animation-layer/AnimationEditorActions.tsx +104 -0
  39. package/src/presentation/components/animation-layer/AnimationInfoBanner.tsx +43 -0
  40. package/src/presentation/components/animation-layer/AnimationTypeSelector.tsx +105 -0
  41. package/src/presentation/components/animation-layer/index.ts +8 -0
  42. package/src/presentation/components/audio-layer/AudioEditorActions.tsx +115 -0
  43. package/src/presentation/components/audio-layer/AudioFileSelector.tsx +126 -0
  44. package/src/presentation/components/audio-layer/FadeEffectsSelector.tsx +151 -0
  45. package/src/presentation/components/audio-layer/InfoBanner.tsx +43 -0
  46. package/src/presentation/components/audio-layer/VolumeSelector.tsx +98 -0
  47. package/src/presentation/components/audio-layer/index.ts +10 -0
  48. package/src/presentation/components/draggable-layer/LayerContent.tsx +106 -0
  49. package/src/presentation/components/draggable-layer/ResizeHandles.tsx +97 -0
  50. package/src/presentation/components/draggable-layer/index.ts +7 -0
  51. package/src/presentation/components/export/ExportActions.tsx +101 -0
  52. package/src/presentation/components/export/ExportInfoBanner.tsx +44 -0
  53. package/src/presentation/components/export/ExportProgress.tsx +114 -0
  54. package/src/presentation/components/export/OptionSelectorRow.tsx +101 -0
  55. package/src/presentation/components/export/ProjectInfoBox.tsx +61 -0
  56. package/src/presentation/components/export/WatermarkToggle.tsx +87 -0
  57. package/src/presentation/components/export/index.ts +11 -0
  58. package/src/presentation/components/image-layer/ImagePreview.tsx +70 -0
  59. package/src/presentation/components/image-layer/ImageSelectionButtons.tsx +82 -0
  60. package/src/presentation/components/image-layer/OpacitySelector.tsx +91 -0
  61. package/src/presentation/components/image-layer/index.ts +8 -0
  62. package/src/presentation/components/index.ts +17 -0
  63. package/src/presentation/components/shape-layer/ColorPickerHorizontal.tsx +92 -0
  64. package/src/presentation/components/shape-layer/ShapePreview.tsx +57 -0
  65. package/src/presentation/components/shape-layer/ShapeTypeSelector.tsx +102 -0
  66. package/src/presentation/components/shape-layer/ValueSelector.tsx +106 -0
  67. package/src/presentation/components/shape-layer/index.ts +9 -0
  68. package/src/presentation/components/text-layer/ColorPicker.tsx +91 -0
  69. package/src/presentation/components/text-layer/EditorActions.tsx +95 -0
  70. package/src/presentation/components/text-layer/FontSizeSelector.tsx +86 -0
  71. package/src/presentation/components/text-layer/OptionSelector.tsx +98 -0
  72. package/src/presentation/components/text-layer/TextAlignSelector.tsx +87 -0
  73. package/src/presentation/components/text-layer/TextInputSection.tsx +70 -0
  74. package/src/presentation/components/text-layer/TextPreview.tsx +71 -0
  75. package/src/presentation/components/text-layer/index.ts +12 -0
  76. package/src/presentation/hooks/useAnimationLayerForm.ts +72 -0
  77. package/src/presentation/hooks/useAudioLayerForm.ts +76 -0
  78. package/src/presentation/hooks/useDraggableLayerGestures.ts +166 -0
  79. package/src/presentation/hooks/useEditorActions.tsx +93 -0
  80. package/src/presentation/hooks/useEditorBottomSheet.ts +43 -0
  81. package/src/presentation/hooks/useEditorHistory.ts +80 -0
  82. package/src/presentation/hooks/useEditorLayers.ts +97 -0
  83. package/src/presentation/hooks/useEditorPlayback.ts +90 -0
  84. package/src/presentation/hooks/useEditorScenes.ts +106 -0
  85. package/src/presentation/hooks/useExport.ts +67 -0
  86. package/src/presentation/hooks/useExportActions.tsx +51 -0
  87. package/src/presentation/hooks/useExportForm.ts +96 -0
  88. package/src/presentation/hooks/useImageLayerForm.ts +57 -0
  89. package/src/presentation/hooks/useImageLayerOperations.ts +71 -0
  90. package/src/presentation/hooks/useLayerActions.tsx +162 -0
  91. package/src/presentation/hooks/useLayerManipulation.ts +178 -0
  92. package/src/presentation/hooks/useMenuActions.tsx +92 -0
  93. package/src/presentation/hooks/useSceneActions.tsx +81 -0
  94. package/src/presentation/hooks/useShapeLayerForm.ts +84 -0
  95. package/src/presentation/hooks/useShapeLayerOperations.ts +52 -0
  96. package/src/presentation/hooks/useTextLayerForm.ts +100 -0
  97. package/src/presentation/hooks/useTextLayerOperations.ts +74 -0
@@ -0,0 +1,98 @@
1
+ /**
2
+ * OptionSelector Component
3
+ * Reusable selector for font family, font weight, etc.
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ useAppDesignTokens,
11
+ } from "@umituz/react-native-design-system";
12
+
13
+ interface Option {
14
+ label: string;
15
+ value: string;
16
+ }
17
+
18
+ interface OptionSelectorProps {
19
+ title: string;
20
+ options: Option[];
21
+ selectedValue: string;
22
+ onValueChange: (value: string) => void;
23
+ }
24
+
25
+ export const OptionSelector: React.FC<OptionSelectorProps> = ({
26
+ title,
27
+ options,
28
+ selectedValue,
29
+ onValueChange,
30
+ }) => {
31
+ const tokens = useAppDesignTokens();
32
+
33
+ return (
34
+ <View style={styles.section}>
35
+ <AtomicText
36
+ type="bodyMedium"
37
+ style={{
38
+ color: tokens.colors.textPrimary,
39
+ fontWeight: "600",
40
+ marginBottom: 8,
41
+ }}
42
+ >
43
+ {title}
44
+ </AtomicText>
45
+ <View style={styles.optionsGrid}>
46
+ {options.map((option) => (
47
+ <TouchableOpacity
48
+ key={option.value}
49
+ style={[
50
+ styles.optionButton,
51
+ {
52
+ backgroundColor:
53
+ selectedValue === option.value
54
+ ? tokens.colors.primary + "20"
55
+ : tokens.colors.surface,
56
+ borderColor:
57
+ selectedValue === option.value
58
+ ? tokens.colors.primary
59
+ : tokens.colors.borderLight,
60
+ },
61
+ ]}
62
+ onPress={() => onValueChange(option.value)}
63
+ >
64
+ <AtomicText
65
+ type="labelSmall"
66
+ style={{
67
+ color:
68
+ selectedValue === option.value
69
+ ? tokens.colors.primary
70
+ : tokens.colors.textPrimary,
71
+ fontWeight: selectedValue === option.value ? "600" : "400",
72
+ }}
73
+ >
74
+ {option.label}
75
+ </AtomicText>
76
+ </TouchableOpacity>
77
+ ))}
78
+ </View>
79
+ </View>
80
+ );
81
+ };
82
+
83
+ const styles = StyleSheet.create({
84
+ section: {
85
+ marginBottom: 24,
86
+ },
87
+ optionsGrid: {
88
+ flexDirection: "row",
89
+ flexWrap: "wrap",
90
+ gap: 8,
91
+ },
92
+ optionButton: {
93
+ paddingHorizontal: 16,
94
+ paddingVertical: 10,
95
+ borderRadius: 8,
96
+ borderWidth: 1,
97
+ },
98
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * TextAlignSelector Component
3
+ * Text alignment selector for text layer
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
8
+ import { useLocalization } from "@umituz/react-native-localization";
9
+ import {
10
+ AtomicText,
11
+ AtomicIcon,
12
+ useAppDesignTokens,
13
+ } from "@umituz/react-native-design-system";
14
+ import { TEXT_ALIGNS } from "../../../constants/text-layer.constants";
15
+
16
+ interface TextAlignSelectorProps {
17
+ textAlign: "left" | "center" | "right";
18
+ onTextAlignChange: (align: "left" | "center" | "right") => void;
19
+ }
20
+
21
+ export const TextAlignSelector: React.FC<TextAlignSelectorProps> = ({
22
+ textAlign,
23
+ onTextAlignChange,
24
+ }) => {
25
+ const { t } = useLocalization();
26
+ const tokens = useAppDesignTokens();
27
+
28
+ return (
29
+ <View style={styles.section}>
30
+ <AtomicText
31
+ type="bodyMedium"
32
+ style={{
33
+ color: tokens.colors.textPrimary,
34
+ fontWeight: "600",
35
+ marginBottom: 8,
36
+ }}
37
+ >
38
+ {t("editor.properties.text_align")}
39
+ </AtomicText>
40
+ <View style={styles.alignButtons}>
41
+ {TEXT_ALIGNS.map((align) => (
42
+ <TouchableOpacity
43
+ key={align.value}
44
+ style={[
45
+ styles.alignButton,
46
+ {
47
+ backgroundColor:
48
+ textAlign === align.value
49
+ ? tokens.colors.primary
50
+ : tokens.colors.surface,
51
+ borderColor:
52
+ textAlign === align.value
53
+ ? tokens.colors.primary
54
+ : tokens.colors.borderLight,
55
+ },
56
+ ]}
57
+ onPress={() => onTextAlignChange(align.value)}
58
+ >
59
+ <AtomicIcon
60
+ name={align.icon}
61
+ size="md"
62
+ color={textAlign === align.value ? "onSurface" : "secondary"}
63
+ />
64
+ </TouchableOpacity>
65
+ ))}
66
+ </View>
67
+ </View>
68
+ );
69
+ };
70
+
71
+ const styles = StyleSheet.create({
72
+ section: {
73
+ marginBottom: 24,
74
+ },
75
+ alignButtons: {
76
+ flexDirection: "row",
77
+ gap: 8,
78
+ },
79
+ alignButton: {
80
+ flex: 1,
81
+ paddingVertical: 12,
82
+ borderRadius: 8,
83
+ borderWidth: 2,
84
+ alignItems: "center",
85
+ justifyContent: "center",
86
+ },
87
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * TextInputSection Component
3
+ * Text input for text layer editor
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TextInput, StyleSheet } from "react-native";
8
+ import { useLocalization } from "@umituz/react-native-localization";
9
+ import {
10
+ AtomicText,
11
+ useAppDesignTokens,
12
+ } from "@umituz/react-native-design-system";
13
+
14
+ interface TextInputSectionProps {
15
+ text: string;
16
+ onChangeText: (text: string) => void;
17
+ }
18
+
19
+ export const TextInputSection: React.FC<TextInputSectionProps> = ({
20
+ text,
21
+ onChangeText,
22
+ }) => {
23
+ const { t } = useLocalization();
24
+ const tokens = useAppDesignTokens();
25
+
26
+ return (
27
+ <View style={styles.section}>
28
+ <AtomicText
29
+ type="bodyMedium"
30
+ style={{
31
+ color: tokens.colors.textPrimary,
32
+ fontWeight: "600",
33
+ marginBottom: 8,
34
+ }}
35
+ >
36
+ {t("editor.properties.text")}
37
+ </AtomicText>
38
+ <TextInput
39
+ style={[
40
+ styles.textInput,
41
+ {
42
+ backgroundColor: tokens.colors.surface,
43
+ color: tokens.colors.textPrimary,
44
+ borderColor: tokens.colors.borderLight,
45
+ },
46
+ ]}
47
+ placeholder="Enter your text..."
48
+ placeholderTextColor={tokens.colors.textSecondary}
49
+ value={text}
50
+ onChangeText={onChangeText}
51
+ multiline
52
+ autoFocus
53
+ />
54
+ </View>
55
+ );
56
+ };
57
+
58
+ const styles = StyleSheet.create({
59
+ section: {
60
+ marginBottom: 24,
61
+ },
62
+ textInput: {
63
+ borderWidth: 1,
64
+ borderRadius: 12,
65
+ padding: 16,
66
+ fontSize: 16,
67
+ minHeight: 100,
68
+ textAlignVertical: "top",
69
+ },
70
+ });
@@ -0,0 +1,71 @@
1
+ /**
2
+ * TextPreview Component
3
+ * Preview section for text layer
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import {
9
+ AtomicText,
10
+ useAppDesignTokens,
11
+ } from "@umituz/react-native-design-system";
12
+ import type { TextLayerFormState } from "../../../hooks/useTextLayerForm";
13
+
14
+ interface TextPreviewProps {
15
+ formState: TextLayerFormState;
16
+ }
17
+
18
+ export const TextPreview: React.FC<TextPreviewProps> = ({ formState }) => {
19
+ const tokens = useAppDesignTokens();
20
+
21
+ return (
22
+ <View style={styles.section}>
23
+ <AtomicText
24
+ type="bodyMedium"
25
+ style={{
26
+ color: tokens.colors.textPrimary,
27
+ fontWeight: "600",
28
+ marginBottom: 8,
29
+ }}
30
+ >
31
+ Preview
32
+ </AtomicText>
33
+ <View
34
+ style={[
35
+ styles.preview,
36
+ {
37
+ backgroundColor: tokens.colors.surfaceSecondary,
38
+ borderColor: tokens.colors.borderLight,
39
+ },
40
+ ]}
41
+ >
42
+ <AtomicText
43
+ type="bodyMedium"
44
+ style={{
45
+ fontSize: formState.fontSize,
46
+ fontFamily: formState.fontFamily,
47
+ fontWeight: formState.fontWeight,
48
+ color: formState.color,
49
+ textAlign: formState.textAlign,
50
+ }}
51
+ >
52
+ {formState.text || "Enter text to preview..."}
53
+ </AtomicText>
54
+ </View>
55
+ </View>
56
+ );
57
+ };
58
+
59
+ const styles = StyleSheet.create({
60
+ section: {
61
+ marginBottom: 24,
62
+ },
63
+ preview: {
64
+ padding: 24,
65
+ borderRadius: 12,
66
+ borderWidth: 1,
67
+ minHeight: 120,
68
+ alignItems: "center",
69
+ justifyContent: "center",
70
+ },
71
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Text Layer Editor Components
3
+ * Barrel file for text layer editor components
4
+ */
5
+
6
+ export { TextInputSection } from "./TextInputSection";
7
+ export { FontSizeSelector } from "./FontSizeSelector";
8
+ export { OptionSelector } from "./OptionSelector";
9
+ export { TextAlignSelector } from "./TextAlignSelector";
10
+ export { ColorPicker } from "./ColorPicker";
11
+ export { TextPreview } from "./TextPreview";
12
+ export { EditorActions } from "./EditorActions";
@@ -0,0 +1,72 @@
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 "@domains/video";
8
+ import type { Easing } from "../constants/animation-layer.constants";
9
+
10
+ export interface AnimationLayerFormState {
11
+ animationType: AnimationType;
12
+ duration: number;
13
+ delay: number;
14
+ easing: Easing;
15
+ }
16
+
17
+ export 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
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * useAudioLayerForm Hook
3
+ * Manages form state for audio layer editor
4
+ */
5
+
6
+ import { useState, useCallback } from "react";
7
+ import type { Audio } from "@domains/video";
8
+
9
+ export interface AudioLayerFormState {
10
+ audioUri: string;
11
+ volume: number;
12
+ fadeIn: number;
13
+ fadeOut: number;
14
+ }
15
+
16
+ export interface UseAudioLayerFormReturn {
17
+ formState: AudioLayerFormState;
18
+ setAudioUri: (uri: string) => void;
19
+ setVolume: (volume: number) => void;
20
+ setFadeIn: (fadeIn: number) => void;
21
+ setFadeOut: (fadeOut: number) => void;
22
+ buildAudioData: () => Audio;
23
+ isValid: boolean;
24
+ }
25
+
26
+ /**
27
+ * Hook for managing audio layer form state
28
+ */
29
+ export function useAudioLayerForm(
30
+ initialAudio?: Audio,
31
+ ): UseAudioLayerFormReturn {
32
+ const [formState, setFormState] = useState<AudioLayerFormState>({
33
+ audioUri: initialAudio?.uri || "",
34
+ volume: initialAudio?.volume ?? 0.7,
35
+ fadeIn: initialAudio?.fadeIn ?? 1000,
36
+ fadeOut: initialAudio?.fadeOut ?? 1000,
37
+ });
38
+
39
+ const setAudioUri = useCallback((uri: string) => {
40
+ setFormState((prev) => ({ ...prev, audioUri: uri }));
41
+ }, []);
42
+
43
+ const setVolume = useCallback((volume: number) => {
44
+ setFormState((prev) => ({ ...prev, volume }));
45
+ }, []);
46
+
47
+ const setFadeIn = useCallback((fadeIn: number) => {
48
+ setFormState((prev) => ({ ...prev, fadeIn }));
49
+ }, []);
50
+
51
+ const setFadeOut = useCallback((fadeOut: number) => {
52
+ setFormState((prev) => ({ ...prev, fadeOut }));
53
+ }, []);
54
+
55
+ const buildAudioData = useCallback((): Audio => {
56
+ return {
57
+ uri: formState.audioUri,
58
+ volume: formState.volume,
59
+ startTime: 0,
60
+ fadeIn: formState.fadeIn,
61
+ fadeOut: formState.fadeOut,
62
+ };
63
+ }, [formState]);
64
+
65
+ const isValid = formState.audioUri.length > 0;
66
+
67
+ return {
68
+ formState,
69
+ setAudioUri,
70
+ setVolume,
71
+ setFadeIn,
72
+ setFadeOut,
73
+ buildAudioData,
74
+ isValid,
75
+ };
76
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * useDraggableLayerGestures Hook
3
+ * Manages gesture handling for draggable layers
4
+ */
5
+
6
+ import { useSharedValue } from "react-native-reanimated";
7
+ import { Gesture } from "react-native-gesture-handler";
8
+ import { withSpring, runOnJS } from "react-native-reanimated";
9
+
10
+ interface UseDraggableLayerGesturesParams {
11
+ initialX: number;
12
+ initialY: number;
13
+ initialWidth: number;
14
+ initialHeight: number;
15
+ canvasWidth: number;
16
+ canvasHeight: number;
17
+ onSelect: () => void;
18
+ onPositionChange: (x: number, y: number) => void;
19
+ onSizeChange: (width: number, height: number) => void;
20
+ }
21
+
22
+ interface UseDraggableLayerGesturesReturn {
23
+ translateX: ReturnType<typeof useSharedValue<number>>;
24
+ translateY: ReturnType<typeof useSharedValue<number>>;
25
+ width: ReturnType<typeof useSharedValue<number>>;
26
+ height: ReturnType<typeof useSharedValue<number>>;
27
+ composedGesture: ReturnType<typeof Gesture.Race>;
28
+ topLeftResizeHandler: ReturnType<typeof Gesture.Pan>;
29
+ topRightResizeHandler: ReturnType<typeof Gesture.Pan>;
30
+ bottomLeftResizeHandler: ReturnType<typeof Gesture.Pan>;
31
+ bottomRightResizeHandler: ReturnType<typeof Gesture.Pan>;
32
+ }
33
+
34
+ const MIN_SIZE = 50;
35
+
36
+ /**
37
+ * Hook for managing draggable layer gestures
38
+ */
39
+ export function useDraggableLayerGestures({
40
+ initialX,
41
+ initialY,
42
+ initialWidth,
43
+ initialHeight,
44
+ canvasWidth,
45
+ canvasHeight,
46
+ onSelect,
47
+ onPositionChange,
48
+ onSizeChange,
49
+ }: UseDraggableLayerGesturesParams): UseDraggableLayerGesturesReturn {
50
+ const translateX = useSharedValue(initialX);
51
+ const translateY = useSharedValue(initialY);
52
+ const width = useSharedValue(initialWidth);
53
+ const height = useSharedValue(initialHeight);
54
+
55
+ const startX = useSharedValue(initialX);
56
+ const startY = useSharedValue(initialY);
57
+ const startWidth = useSharedValue(initialWidth);
58
+ const startHeight = useSharedValue(initialHeight);
59
+
60
+ const gestureHandler = Gesture.Pan()
61
+ .onStart(() => {
62
+ startX.value = translateX.value;
63
+ startY.value = translateY.value;
64
+ runOnJS(onSelect)();
65
+ })
66
+ .onUpdate((event) => {
67
+ translateX.value = startX.value + event.translationX;
68
+ translateY.value = startY.value + event.translationY;
69
+ })
70
+ .onEnd(() => {
71
+ translateX.value = withSpring(
72
+ Math.max(0, Math.min(canvasWidth - width.value, translateX.value)),
73
+ );
74
+ translateY.value = withSpring(
75
+ Math.max(0, Math.min(canvasHeight - height.value, translateY.value)),
76
+ );
77
+
78
+ const newX = (translateX.value / canvasWidth) * 100;
79
+ const newY = (translateY.value / canvasHeight) * 100;
80
+ runOnJS(onPositionChange)(newX, newY);
81
+ });
82
+
83
+ const createResizeHandler = (
84
+ deltaX: (translationX: number) => number,
85
+ deltaY: (translationY: number) => number,
86
+ ) => {
87
+ return Gesture.Pan()
88
+ .onStart(() => {
89
+ startWidth.value = width.value;
90
+ startHeight.value = height.value;
91
+ startX.value = translateX.value;
92
+ startY.value = translateY.value;
93
+ runOnJS(onSelect)();
94
+ })
95
+ .onUpdate((event) => {
96
+ const newWidth = Math.max(
97
+ MIN_SIZE,
98
+ startWidth.value + deltaX(event.translationX),
99
+ );
100
+ const newHeight = Math.max(
101
+ MIN_SIZE,
102
+ startHeight.value + deltaY(event.translationY),
103
+ );
104
+ width.value = Math.min(newWidth, canvasWidth - startX.value);
105
+ height.value = Math.min(newHeight, canvasHeight - startY.value);
106
+
107
+ if (deltaX(event.translationX) < 0) {
108
+ translateX.value = Math.max(
109
+ 0,
110
+ startX.value + (startWidth.value - width.value),
111
+ );
112
+ }
113
+ if (deltaY(event.translationY) < 0) {
114
+ translateY.value = Math.max(
115
+ 0,
116
+ startY.value + (startHeight.value - height.value),
117
+ );
118
+ }
119
+ })
120
+ .onEnd(() => {
121
+ const newWidth = (width.value / canvasWidth) * 100;
122
+ const newHeight = (height.value / canvasHeight) * 100;
123
+ const newX = (translateX.value / canvasWidth) * 100;
124
+ const newY = (translateY.value / canvasHeight) * 100;
125
+ runOnJS(onSizeChange)(newWidth, newHeight);
126
+ runOnJS(onPositionChange)(newX, newY);
127
+ });
128
+ };
129
+
130
+ const topLeftResizeHandler = createResizeHandler(
131
+ (tx) => -tx,
132
+ (ty) => -ty,
133
+ );
134
+ const topRightResizeHandler = createResizeHandler(
135
+ (tx) => tx,
136
+ (ty) => -ty,
137
+ );
138
+ const bottomLeftResizeHandler = createResizeHandler(
139
+ (tx) => -tx,
140
+ (ty) => ty,
141
+ );
142
+ const bottomRightResizeHandler = createResizeHandler(
143
+ (tx) => tx,
144
+ (ty) => ty,
145
+ );
146
+
147
+ const composedGesture = Gesture.Race(
148
+ gestureHandler,
149
+ topLeftResizeHandler,
150
+ topRightResizeHandler,
151
+ bottomLeftResizeHandler,
152
+ bottomRightResizeHandler,
153
+ );
154
+
155
+ return {
156
+ translateX,
157
+ translateY,
158
+ width,
159
+ height,
160
+ composedGesture,
161
+ topLeftResizeHandler,
162
+ topRightResizeHandler,
163
+ bottomLeftResizeHandler,
164
+ bottomRightResizeHandler,
165
+ };
166
+ }