@umituz/react-native-photo-editor 2.0.23 → 2.0.25

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 (57) hide show
  1. package/package.json +1 -1
  2. package/src/PhotoEditor.tsx +43 -137
  3. package/src/application/hooks/useEditor.ts +4 -6
  4. package/src/application/hooks/useEditorUI.ts +8 -5
  5. package/src/application/stores/EditorStore.ts +17 -6
  6. package/src/domain/entities/Layer.entity.ts +86 -0
  7. package/src/domain/entities/{Layer.ts → Layer.legacy.ts} +3 -3
  8. package/src/domain/entities/StickerLayer.entity.ts +37 -0
  9. package/src/domain/entities/TextLayer.entity.ts +58 -0
  10. package/src/domain/entities/index.ts +9 -0
  11. package/src/domain/services/History.service.ts +69 -0
  12. package/src/domain/services/LayerFactory.service.ts +81 -0
  13. package/src/domain/services/LayerRepository.service.ts +85 -0
  14. package/src/domain/services/LayerService.ts +1 -1
  15. package/src/domain/types.ts +39 -0
  16. package/src/domain/value-objects/FilterSettings.vo.ts +89 -0
  17. package/src/domain/value-objects/LayerDefaults.vo.ts +56 -0
  18. package/src/domain/value-objects/Transform.vo.ts +61 -0
  19. package/src/domain/value-objects/index.ts +13 -0
  20. package/src/index.ts +4 -4
  21. package/src/infrastructure/gesture/createTransformGesture.ts +127 -0
  22. package/src/infrastructure/gesture/useTransformGesture.ts +7 -13
  23. package/src/presentation/components/DraggableLayer.tsx +13 -13
  24. package/src/presentation/components/EditorCanvas.tsx +5 -5
  25. package/src/presentation/components/EditorContent.tsx +72 -0
  26. package/src/presentation/components/EditorHeader.tsx +48 -0
  27. package/src/presentation/components/EditorSheets.tsx +85 -0
  28. package/src/presentation/components/FontControls.tsx +2 -2
  29. package/src/presentation/components/sheets/AdjustmentsSheet.tsx +4 -4
  30. package/src/presentation/components/sheets/FilterSheet.tsx +1 -1
  31. package/src/presentation/components/sheets/LayerManager.tsx +3 -4
  32. package/src/presentation/components/sheets/TextEditorSheet.tsx +1 -1
  33. package/src/types.ts +8 -18
  34. package/src/utils/constants.ts +84 -0
  35. package/src/utils/formatters.ts +29 -0
  36. package/src/utils/helpers.ts +51 -0
  37. package/src/utils/index.ts +9 -0
  38. package/src/utils/validators.ts +38 -0
  39. package/ARCHITECTURE.md +0 -104
  40. package/MIGRATION.md +0 -100
  41. package/src/components/AIMagicSheet.tsx +0 -107
  42. package/src/components/AdjustmentsSheet.tsx +0 -108
  43. package/src/components/ColorPicker.tsx +0 -77
  44. package/src/components/DraggableSticker.tsx +0 -161
  45. package/src/components/DraggableText.tsx +0 -181
  46. package/src/components/EditorCanvas.tsx +0 -106
  47. package/src/components/EditorToolbar.tsx +0 -155
  48. package/src/components/FilterPicker.tsx +0 -73
  49. package/src/components/FontControls.tsx +0 -132
  50. package/src/components/LayerManager.tsx +0 -164
  51. package/src/components/Slider.tsx +0 -112
  52. package/src/components/StickerPicker.tsx +0 -47
  53. package/src/components/TextEditorSheet.tsx +0 -160
  54. package/src/core/HistoryManager.ts +0 -53
  55. package/src/hooks/usePhotoEditor.ts +0 -172
  56. package/src/hooks/usePhotoEditorUI.ts +0 -162
  57. package/src/infrastructure/history/HistoryManager.ts +0 -38
package/ARCHITECTURE.md DELETED
@@ -1,104 +0,0 @@
1
- # Architecture - Photo Editor DDD Design
2
-
3
- ## Overview
4
-
5
- This photo editor is built using **Domain-Driven Design (DDD)** principles, ensuring maintainability, testability, and scalability. Every file is kept under 150 lines for clarity.
6
-
7
- ## Layer Structure
8
-
9
- ```
10
- src/
11
- ├── domain/ # Business logic (pure TypeScript)
12
- │ ├── entities/
13
- │ │ ├── Layer.ts # Layer entities (TextLayer, StickerLayer)
14
- │ │ ├── Transform.ts # Transform value object
15
- │ │ └── Filters.ts # Filters value object
16
- │ └── services/
17
- │ ├── LayerService.ts # Layer business logic
18
- │ └── HistoryService.ts # Undo/redo service
19
-
20
- ├── infrastructure/ # External concerns
21
- │ ├── gesture/
22
- │ │ ├── useTransformGesture.ts # Reusable gesture hook
23
- │ │ └── types.ts # Gesture types
24
- │ └── history/
25
- │ └── HistoryManager.ts # Legacy wrapper
26
-
27
- ├── application/ # Application logic
28
- │ ├── stores/
29
- │ │ └── EditorStore.ts # Zustand state store
30
- │ └── hooks/
31
- │ ├── useEditor.ts # Main editor hook
32
- │ └── useEditorUI.ts # UI-specific hook
33
-
34
- └── presentation/ # UI components
35
- └── components/
36
- ├── DraggableLayer.tsx # Unified draggable (replaces Text + Sticker)
37
- ├── EditorCanvas.tsx
38
- ├── EditorToolbar.tsx
39
- ├── FontControls.tsx
40
- ├── ui/
41
- │ ├── ColorPicker.tsx
42
- │ └── Slider.tsx
43
- └── sheets/
44
- ├── TextEditorSheet.tsx
45
- ├── FilterSheet.tsx
46
- ├── AdjustmentsSheet.tsx
47
- ├── LayerManager.tsx
48
- ├── StickerPicker.tsx
49
- └── AIMagicSheet.tsx
50
- ```
51
-
52
- ## Key Improvements
53
-
54
- ### 1. Eliminated Code Duplication (~180 lines)
55
- - **Before**: `DraggableText.tsx` (182 lines) and `DraggableSticker.tsx` (162 lines) - duplicate gesture logic
56
- - **After**: `DraggableLayer.tsx` (110 lines) + `useTransformGesture.ts` (130 lines) - reusable gesture hook
57
-
58
- ### 2. Domain Entities
59
- - Rich domain models with business logic
60
- - Type-safe layer operations
61
- - Value objects for Transform and Filters
62
-
63
- ### 3. Separated Concerns
64
- - **Domain**: Pure business logic, no framework dependencies
65
- - **Infrastructure**: React Native, gesture handlers
66
- - **Application**: State management, orchestration
67
- - **Presentation**: UI components only
68
-
69
- ### 4. State Management
70
- - Zustand store for global editor state
71
- - History service for undo/redo
72
- - Clean separation between domain and UI state
73
-
74
- ## Usage
75
-
76
- ```tsx
77
- import { PhotoEditor } from "@umituz/react-native-photo-editor";
78
-
79
- <PhotoEditor
80
- imageUri={imageUri}
81
- onSave={(uri, layers, filters) => console.log({ uri, layers, filters })}
82
- onClose={() => navigation.goBack()}
83
- t={(key) => i18n.t(key)}
84
- />
85
- ```
86
-
87
- ## Testing
88
-
89
- Each layer can be tested independently:
90
- - Domain: Pure functions, easy to unit test
91
- - Infrastructure: Gesture hooks with test utils
92
- - Application: Store with test environment
93
- - Presentation: React component testing
94
-
95
- ## Migration from Old Architecture
96
-
97
- The old files have been preserved for backward compatibility:
98
- - Old hooks: `src/hooks/`
99
- - Old components: `src/components/`
100
-
101
- New code should use:
102
- - `useEditor()` instead of `usePhotoEditor()`
103
- - `useEditorUI()` instead of `usePhotoEditorUI()`
104
- - Components from `src/presentation/components/`
package/MIGRATION.md DELETED
@@ -1,100 +0,0 @@
1
- # Migration Guide - Old to New Architecture
2
-
3
- ## What Changed?
4
-
5
- ### Before
6
- ```
7
- src/
8
- ├── hooks/
9
- │ ├── usePhotoEditor.ts # 173 lines - mixed concerns
10
- │ └── usePhotoEditorUI.ts # 163 lines - UI + business logic
11
- ├── components/
12
- │ ├── DraggableText.tsx # 182 lines - gesture + UI
13
- │ ├── DraggableSticker.tsx # 162 lines - DUPLICATE of above
14
- │ └── ...
15
- ├── core/
16
- │ └── HistoryManager.ts # Generic, reusable
17
- └── types.ts # Simple type definitions
18
- ```
19
-
20
- ### After
21
- ```
22
- src/
23
- ├── domain/ # Pure business logic
24
- │ ├── entities/ # Rich domain models
25
- │ └── services/ # Business operations
26
- ├── infrastructure/ # External systems
27
- │ ├── gesture/ # Reusable gestures
28
- │ └── history/
29
- ├── application/ # Orchestration
30
- │ ├── stores/ # State management
31
- │ └── hooks/ # Clean hooks
32
- └── presentation/ # UI only
33
- └── components/
34
- ```
35
-
36
- ## API Changes
37
-
38
- ### Hooks
39
-
40
- **Old:**
41
- ```tsx
42
- import { usePhotoEditor } from "./hooks/usePhotoEditor";
43
- import { usePhotoEditorUI } from "./hooks/usePhotoEditorUI";
44
-
45
- const editor = usePhotoEditor([]);
46
- const ui = usePhotoEditorUI();
47
- ```
48
-
49
- **New:**
50
- ```tsx
51
- import { useEditor } from "./application/hooks/useEditor";
52
- import { useEditorUI } from "./application/hooks/useEditorUI";
53
-
54
- const editor = useEditor();
55
- const ui = useEditorUI();
56
- ```
57
-
58
- ### Components
59
-
60
- **Old:**
61
- ```tsx
62
- import DraggableText from "./components/DraggableText";
63
- import DraggableSticker from "./components/DraggableSticker";
64
- ```
65
-
66
- **New:**
67
- ```tsx
68
- import { DraggableLayer } from "./presentation/components/DraggableLayer";
69
- ```
70
-
71
- ### Types
72
-
73
- **Old:**
74
- ```tsx
75
- import type { Layer, TextLayer, ImageFilters } from "./types";
76
- ```
77
-
78
- **New:**
79
- ```tsx
80
- import type { Layer, TextLayer, FilterValues } from "./domain/entities/Layer";
81
- import { FiltersVO } from "./domain/entities/Filters";
82
- ```
83
-
84
- ## Benefits
85
-
86
- 1. **180 lines less code** - Removed duplication between DraggableText and DraggableSticker
87
- 2. **Clear separation** - Business logic separate from UI
88
- 3. **Testable** - Each layer can be tested independently
89
- 4. **Maintainable** - Files under 150 lines each
90
- 5. **Scalable** - Easy to add new features
91
-
92
- ## Backward Compatibility
93
-
94
- The old API is still exported. Your existing code will continue to work.
95
-
96
- To migrate gradually:
97
- 1. Start using `useEditor()` in new features
98
- 2. Replace `DraggableText` + `DraggableSticker` with `DraggableLayer`
99
- 3. Update imports to new paths
100
- 4. Remove old imports when ready
@@ -1,107 +0,0 @@
1
- import React, { useState, useMemo } from "react";
2
- import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
3
- import { AtomicText, AtomicIcon, AtomicButton } from "@umituz/react-native-design-system/atoms";
4
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
- import { DEFAULT_AI_STYLES } from "../constants";
6
-
7
- interface AIMagicSheetProps {
8
- /**
9
- * Called with the selected style ID. Should return a generated caption string.
10
- * If undefined, the AI button is disabled.
11
- */
12
- onGenerateCaption?: (style: string) => Promise<string> | void;
13
- isLoading?: boolean;
14
- }
15
-
16
- export const AIMagicSheet: React.FC<AIMagicSheetProps> = ({
17
- onGenerateCaption,
18
- isLoading = false,
19
- }) => {
20
- const tokens = useAppDesignTokens();
21
- const [selected, setSelected] = useState<string | null>(null);
22
- const [loading, setLoading] = useState(false);
23
-
24
- const styles = useMemo(() => StyleSheet.create({
25
- container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
26
- header: { flexDirection: "row", alignItems: "center", gap: tokens.spacing.sm },
27
- grid: { gap: tokens.spacing.sm },
28
- card: {
29
- flexDirection: "row",
30
- alignItems: "center",
31
- padding: tokens.spacing.md,
32
- backgroundColor: tokens.colors.surfaceVariant,
33
- borderRadius: tokens.borders.radius.md,
34
- borderWidth: 2,
35
- borderColor: "transparent",
36
- },
37
- cardActive: {
38
- borderColor: tokens.colors.primary,
39
- backgroundColor: tokens.colors.primary + "10",
40
- },
41
- info: { flex: 1, marginLeft: tokens.spacing.sm },
42
- }), [tokens]);
43
-
44
- const handleGenerate = async () => {
45
- if (!selected || !onGenerateCaption) return;
46
- setLoading(true);
47
- try {
48
- await onGenerateCaption(selected);
49
- } finally {
50
- setLoading(false);
51
- }
52
- };
53
-
54
- const isGenerating = isLoading || loading;
55
-
56
- return (
57
- <View style={styles.container}>
58
- <View style={styles.header}>
59
- <AtomicIcon name="sparkles" size="md" color="primary" />
60
- <AtomicText type="headlineSmall">AI Caption Magic</AtomicText>
61
- </View>
62
- <ScrollView showsVerticalScrollIndicator={false}>
63
- <View style={styles.grid}>
64
- {DEFAULT_AI_STYLES.map((style) => {
65
- const isActive = selected === style.id;
66
- const [emoji, ...words] = style.label.split(" ");
67
- return (
68
- <TouchableOpacity
69
- key={style.id}
70
- style={[styles.card, isActive && styles.cardActive]}
71
- onPress={() => setSelected(style.id)}
72
- accessibilityLabel={style.label}
73
- accessibilityRole="button"
74
- accessibilityState={{ selected: isActive }}
75
- >
76
- <AtomicText style={{ fontSize: 24 }}>{emoji}</AtomicText>
77
- <View style={styles.info}>
78
- <AtomicText
79
- fontWeight="bold"
80
- color={isActive ? "primary" : "textPrimary"}
81
- >
82
- {words.join(" ")}
83
- </AtomicText>
84
- <AtomicText type="labelSmall" color="textSecondary">
85
- {style.desc}
86
- </AtomicText>
87
- </View>
88
- {isActive && (
89
- <AtomicIcon name="checkmark-circle" size="md" color="primary" />
90
- )}
91
- </TouchableOpacity>
92
- );
93
- })}
94
- </View>
95
- </ScrollView>
96
- <AtomicButton
97
- variant="primary"
98
- disabled={!selected || !onGenerateCaption || isGenerating}
99
- onPress={handleGenerate}
100
- loading={isGenerating}
101
- icon="sparkles"
102
- >
103
- Generate Caption
104
- </AtomicButton>
105
- </View>
106
- );
107
- };
@@ -1,108 +0,0 @@
1
- import React from "react";
2
- import { View, TouchableOpacity } from "react-native";
3
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
4
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
- import { Slider } from "./Slider";
6
- import { ImageFilters, DEFAULT_IMAGE_FILTERS } from "../types";
7
-
8
- interface AdjustmentsSheetProps {
9
- filters: ImageFilters;
10
- onFiltersChange: (filters: ImageFilters) => void;
11
- }
12
-
13
- export const AdjustmentsSheet: React.FC<AdjustmentsSheetProps> = ({
14
- filters,
15
- onFiltersChange,
16
- }) => {
17
- const tokens = useAppDesignTokens();
18
-
19
- const update = (key: keyof ImageFilters, val: number) => {
20
- onFiltersChange({ ...filters, [key]: val });
21
- };
22
-
23
- const handleReset = () => onFiltersChange(DEFAULT_IMAGE_FILTERS);
24
-
25
- return (
26
- <View style={{ padding: tokens.spacing.md, gap: tokens.spacing.lg }}>
27
- <View
28
- style={{
29
- flexDirection: "row",
30
- alignItems: "center",
31
- justifyContent: "space-between",
32
- }}
33
- >
34
- <View style={{ flexDirection: "row", alignItems: "center", gap: tokens.spacing.sm }}>
35
- <AtomicIcon name="brush" size="md" color="primary" />
36
- <AtomicText type="headlineSmall">Adjustments</AtomicText>
37
- </View>
38
- <TouchableOpacity
39
- onPress={handleReset}
40
- accessibilityLabel="Reset adjustments"
41
- accessibilityRole="button"
42
- style={{
43
- paddingHorizontal: tokens.spacing.md,
44
- paddingVertical: tokens.spacing.xs,
45
- backgroundColor: tokens.colors.surfaceVariant,
46
- borderRadius: tokens.borders.radius.sm,
47
- }}
48
- >
49
- <AtomicText type="labelSmall" color="textSecondary">
50
- Reset
51
- </AtomicText>
52
- </TouchableOpacity>
53
- </View>
54
-
55
- <Slider
56
- label="Brightness"
57
- value={filters.brightness}
58
- min={0.5}
59
- max={2}
60
- step={0.05}
61
- onValueChange={(v) => update("brightness", v)}
62
- formatValue={(v) => `${Math.round((v - 1) * 100) >= 0 ? "+" : ""}${Math.round((v - 1) * 100)}%`}
63
- />
64
-
65
- <Slider
66
- label="Contrast"
67
- value={filters.contrast}
68
- min={0.5}
69
- max={2}
70
- step={0.05}
71
- onValueChange={(v) => update("contrast", v)}
72
- formatValue={(v) => `${Math.round((v - 1) * 100) >= 0 ? "+" : ""}${Math.round((v - 1) * 100)}%`}
73
- />
74
-
75
- <Slider
76
- label="Saturation"
77
- value={filters.saturation}
78
- min={0}
79
- max={2}
80
- step={0.05}
81
- onValueChange={(v) => update("saturation", v)}
82
- formatValue={(v) => `${Math.round((v - 1) * 100) >= 0 ? "+" : ""}${Math.round((v - 1) * 100)}%`}
83
- />
84
-
85
- <Slider
86
- label="Hue Rotate"
87
- value={filters.hueRotate ?? 0}
88
- min={0}
89
- max={360}
90
- step={1}
91
- onValueChange={(v) => update("hueRotate", v)}
92
- formatValue={(v) => `${Math.round(v)}°`}
93
- />
94
-
95
- <Slider
96
- label="Sepia"
97
- value={filters.sepia}
98
- min={0}
99
- max={1}
100
- step={0.05}
101
- onValueChange={(v) => update("sepia", v)}
102
- formatValue={(v) => `${Math.round(v * 100)}%`}
103
- />
104
- </View>
105
- );
106
- };
107
-
108
- export default React.memo(AdjustmentsSheet);
@@ -1,77 +0,0 @@
1
- import React from "react";
2
- import { View, TouchableOpacity } from "react-native";
3
- import { AtomicText } from "@umituz/react-native-design-system/atoms";
4
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
- import { DEFAULT_TEXT_COLORS } from "../constants";
6
-
7
- interface ColorPickerProps {
8
- selectedColor: string;
9
- onSelectColor: (color: string) => void;
10
- label?: string;
11
- colors?: readonly string[];
12
- }
13
-
14
- export const ColorPicker: React.FC<ColorPickerProps> = ({
15
- selectedColor,
16
- onSelectColor,
17
- label,
18
- colors = DEFAULT_TEXT_COLORS,
19
- }) => {
20
- const tokens = useAppDesignTokens();
21
-
22
- return (
23
- <View style={{ gap: tokens.spacing.xs }}>
24
- {label && (
25
- <AtomicText type="labelMedium" color="textSecondary">
26
- {label}
27
- </AtomicText>
28
- )}
29
- <View
30
- style={{
31
- flexDirection: "row",
32
- flexWrap: "wrap",
33
- gap: tokens.spacing.xs,
34
- }}
35
- >
36
- {colors.map((color) => {
37
- const isSelected = selectedColor === color;
38
- return (
39
- <TouchableOpacity
40
- key={color}
41
- onPress={() => onSelectColor(color)}
42
- accessibilityLabel={`Color ${color}`}
43
- accessibilityRole="button"
44
- accessibilityState={{ selected: isSelected }}
45
- style={{
46
- width: 34,
47
- height: 34,
48
- borderRadius: 17,
49
- backgroundColor: color,
50
- borderWidth: isSelected ? 3 : 1.5,
51
- borderColor: isSelected
52
- ? tokens.colors.primary
53
- : tokens.colors.border,
54
- alignItems: "center",
55
- justifyContent: "center",
56
- }}
57
- >
58
- {isSelected && (
59
- <View
60
- style={{
61
- width: 10,
62
- height: 10,
63
- borderRadius: 5,
64
- backgroundColor:
65
- color === "#FFFFFF" ? "#000000" : "#FFFFFF",
66
- }}
67
- />
68
- )}
69
- </TouchableOpacity>
70
- );
71
- })}
72
- </View>
73
- </View>
74
- );
75
- };
76
-
77
- export default React.memo(ColorPicker);
@@ -1,161 +0,0 @@
1
- import React, { useState, useRef, useCallback, useEffect } from "react";
2
- import { View, StyleSheet } from "react-native";
3
- import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
- import { Image } from "expo-image";
5
- import { AtomicText } from "@umituz/react-native-design-system/atoms";
6
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
7
- import type { LayerTransform } from "./DraggableText";
8
-
9
- interface DraggableStickerProps {
10
- uri: string;
11
- initialX: number;
12
- initialY: number;
13
- rotation?: number;
14
- scale?: number;
15
- opacity?: number;
16
- onTransformEnd: (transform: LayerTransform) => void;
17
- onPress: () => void;
18
- isSelected?: boolean;
19
- }
20
-
21
- const isEmojiString = (str: string) =>
22
- str.length <= 4 && !/^https?:\/\//i.test(str) && !str.startsWith("/");
23
-
24
- export const DraggableSticker: React.FC<DraggableStickerProps> = ({
25
- uri,
26
- initialX,
27
- initialY,
28
- rotation: rotationProp = 0,
29
- scale: scaleProp = 1,
30
- opacity = 1,
31
- onTransformEnd,
32
- onPress,
33
- isSelected,
34
- }) => {
35
- const tokens = useAppDesignTokens();
36
- const [position, setPosition] = useState({ x: initialX, y: initialY });
37
- const [scale, setScale] = useState(scaleProp);
38
- const [rotation, setRotation] = useState(rotationProp);
39
-
40
- // Sync when props change (e.g., undo/redo)
41
- useEffect(() => { setPosition({ x: initialX, y: initialY }); }, [initialX, initialY]);
42
- useEffect(() => { setScale(scaleProp); }, [scaleProp]);
43
- useEffect(() => { setRotation(rotationProp); }, [rotationProp]);
44
-
45
- const positionRef = useRef(position);
46
- positionRef.current = position;
47
- const scaleRef = useRef(scale);
48
- scaleRef.current = scale;
49
- const rotationRef = useRef(rotation);
50
- rotationRef.current = rotation;
51
- const onTransformEndRef = useRef(onTransformEnd);
52
- onTransformEndRef.current = onTransformEnd;
53
- const onPressRef = useRef(onPress);
54
- onPressRef.current = onPress;
55
-
56
- const offsetRef = useRef({ x: initialX, y: initialY });
57
- const scaleStartRef = useRef(scaleProp);
58
- const rotationStartRef = useRef(rotationProp);
59
-
60
- const emitTransform = useCallback(() => {
61
- onTransformEndRef.current({
62
- x: positionRef.current.x,
63
- y: positionRef.current.y,
64
- scale: scaleRef.current,
65
- rotation: rotationRef.current,
66
- });
67
- }, []);
68
-
69
- const panGesture = Gesture.Pan()
70
- .runOnJS(true)
71
- .averageTouches(true)
72
- .onStart(() => {
73
- offsetRef.current = { x: positionRef.current.x, y: positionRef.current.y };
74
- })
75
- .onUpdate((e) => {
76
- setPosition({
77
- x: offsetRef.current.x + e.translationX,
78
- y: offsetRef.current.y + e.translationY,
79
- });
80
- })
81
- .onEnd(emitTransform);
82
-
83
- const pinchGesture = Gesture.Pinch()
84
- .runOnJS(true)
85
- .onStart(() => {
86
- scaleStartRef.current = scaleRef.current;
87
- })
88
- .onUpdate((e) => {
89
- setScale(Math.max(0.2, Math.min(6, scaleStartRef.current * e.scale)));
90
- })
91
- .onEnd(emitTransform);
92
-
93
- const rotationGesture = Gesture.Rotation()
94
- .runOnJS(true)
95
- .onStart(() => {
96
- rotationStartRef.current = rotationRef.current;
97
- })
98
- .onUpdate((e) => {
99
- setRotation(rotationStartRef.current + (e.rotation * 180) / Math.PI);
100
- })
101
- .onEnd(emitTransform);
102
-
103
- const tapGesture = Gesture.Tap()
104
- .runOnJS(true)
105
- .onEnd(() => onPressRef.current());
106
-
107
- const composed = Gesture.Exclusive(
108
- Gesture.Simultaneous(panGesture, pinchGesture, rotationGesture),
109
- tapGesture,
110
- );
111
-
112
- const isEmoji = isEmojiString(uri);
113
-
114
- return (
115
- <GestureDetector gesture={composed}>
116
- <View
117
- accessibilityLabel={isEmoji ? `Sticker ${uri}` : "Image sticker"}
118
- accessibilityRole="button"
119
- style={[
120
- styles.container,
121
- {
122
- transform: [
123
- { translateX: position.x },
124
- { translateY: position.y },
125
- { rotate: `${rotation}deg` },
126
- { scale },
127
- ],
128
- opacity,
129
- zIndex: isSelected ? 100 : 50,
130
- },
131
- ]}
132
- >
133
- <View
134
- style={{
135
- padding: tokens.spacing.xs,
136
- borderRadius: tokens.borders.radius.sm,
137
- borderWidth: isSelected ? 2 : 0,
138
- borderColor: tokens.colors.primary,
139
- borderStyle: "dashed",
140
- backgroundColor: isSelected ? tokens.colors.primary + "10" : "transparent",
141
- }}
142
- >
143
- {isEmoji ? (
144
- <AtomicText style={{ fontSize: 48 }}>{uri}</AtomicText>
145
- ) : (
146
- <Image
147
- source={{ uri }}
148
- style={{ width: 80, height: 80 }}
149
- contentFit="contain"
150
- accessibilityIgnoresInvertColors
151
- />
152
- )}
153
- </View>
154
- </View>
155
- </GestureDetector>
156
- );
157
- };
158
-
159
- const styles = StyleSheet.create({
160
- container: { position: "absolute" },
161
- });