@umituz/react-native-photo-editor 2.0.22 → 2.0.24

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 (28) hide show
  1. package/package.json +1 -1
  2. package/src/PhotoEditor.tsx +56 -44
  3. package/src/application/hooks/useEditor.ts +67 -0
  4. package/src/application/hooks/useEditorUI.ts +145 -0
  5. package/src/application/stores/EditorStore.ts +137 -0
  6. package/src/constants.ts +5 -52
  7. package/src/domain/entities/Filters.ts +72 -0
  8. package/src/domain/entities/Layer.ts +126 -0
  9. package/src/domain/entities/Transform.ts +55 -0
  10. package/src/domain/services/HistoryService.ts +60 -0
  11. package/src/domain/services/LayerService.ts +105 -0
  12. package/src/index.ts +25 -5
  13. package/src/infrastructure/gesture/types.ts +27 -0
  14. package/src/infrastructure/gesture/useTransformGesture.ts +136 -0
  15. package/src/infrastructure/history/HistoryManager.ts +38 -0
  16. package/src/presentation/components/DraggableLayer.tsx +114 -0
  17. package/src/presentation/components/EditorCanvas.tsx +90 -0
  18. package/src/presentation/components/EditorToolbar.tsx +192 -0
  19. package/src/presentation/components/FontControls.tsx +99 -0
  20. package/src/presentation/components/sheets/AIMagicSheet.tsx +99 -0
  21. package/src/presentation/components/sheets/AdjustmentsSheet.tsx +113 -0
  22. package/src/presentation/components/sheets/FilterSheet.tsx +128 -0
  23. package/src/presentation/components/sheets/LayerManager.tsx +151 -0
  24. package/src/presentation/components/sheets/StickerPicker.tsx +67 -0
  25. package/src/presentation/components/sheets/TextEditorSheet.tsx +159 -0
  26. package/src/presentation/components/ui/ColorPicker.tsx +78 -0
  27. package/src/presentation/components/ui/Slider.tsx +116 -0
  28. package/src/types.ts +13 -58
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Layer Entities
3
+ * Base layer and specialized text/sticker layers
4
+ */
5
+
6
+ import type { Transform } from "./Transform";
7
+
8
+ export type LayerType = "text" | "sticker";
9
+ export type TextAlign = "left" | "center" | "right";
10
+
11
+ export interface BaseLayerData {
12
+ id: string;
13
+ type: LayerType;
14
+ x: number;
15
+ y: number;
16
+ rotation: number;
17
+ scale: number;
18
+ opacity: number;
19
+ zIndex: number;
20
+ }
21
+
22
+ export interface TextLayerData extends BaseLayerData {
23
+ type: "text";
24
+ text: string;
25
+ fontSize: number;
26
+ fontFamily: string;
27
+ color: string;
28
+ backgroundColor: string;
29
+ textAlign: TextAlign;
30
+ isBold?: boolean;
31
+ isItalic?: boolean;
32
+ strokeColor?: string;
33
+ strokeWidth?: number;
34
+ }
35
+
36
+ export interface StickerLayerData extends BaseLayerData {
37
+ type: "sticker";
38
+ uri: string;
39
+ }
40
+
41
+ export type LayerData = TextLayerData | StickerLayerData;
42
+
43
+ export class Layer {
44
+ constructor(private readonly data: LayerData) {}
45
+
46
+ get id(): string { return this.data.id; }
47
+ get type(): LayerType { return this.data.type; }
48
+ get x(): number { return this.data.x; }
49
+ get y(): number { return this.data.y; }
50
+ get rotation(): number { return this.data.rotation; }
51
+ get scale(): number { return this.data.scale; }
52
+ get opacity(): number { return this.data.opacity; }
53
+ get zIndex(): number { return this.data.zIndex; }
54
+
55
+ isText(): this is TextLayer {
56
+ return this.data.type === "text";
57
+ }
58
+
59
+ isSticker(): this is StickerLayer {
60
+ return this.data.type === "sticker";
61
+ }
62
+
63
+ withTransform(transform: Partial<Transform>): Layer {
64
+ const newData = { ...this.data, ...transform };
65
+ return this.recreate(newData);
66
+ }
67
+
68
+ withOpacity(opacity: number): Layer {
69
+ return this.recreate({ ...this.data, opacity });
70
+ }
71
+
72
+ withZIndex(zIndex: number): Layer {
73
+ return this.recreate({ ...this.data, zIndex });
74
+ }
75
+
76
+ protected recreate(data: LayerData): Layer {
77
+ return data.type === "text" ? new TextLayer(data) : new StickerLayer(data);
78
+ }
79
+
80
+ toJSON(): LayerData {
81
+ return { ...this.data };
82
+ }
83
+
84
+ static from(data: LayerData): Layer {
85
+ return data.type === "text" ? new TextLayer(data) : new StickerLayer(data);
86
+ }
87
+ }
88
+
89
+ export class TextLayer extends Layer {
90
+ declare readonly data: TextLayerData;
91
+
92
+ get text(): string { return this.data.text; }
93
+ get fontSize(): number { return this.data.fontSize; }
94
+ get fontFamily(): string { return this.data.fontFamily; }
95
+ get color(): string { return this.data.color; }
96
+ get backgroundColor(): string { return this.data.backgroundColor; }
97
+ get textAlign(): TextAlign { return this.data.textAlign; }
98
+ get isBold(): boolean { return this.data.isBold ?? false; }
99
+ get isItalic(): boolean { return this.data.isItalic ?? false; }
100
+
101
+ withText(text: string): TextLayer {
102
+ return new TextLayer({ ...this.data, text });
103
+ }
104
+
105
+ withStyle(styles: Partial<Omit<TextLayerData, "id" | "type">>): TextLayer {
106
+ return new TextLayer({ ...this.data, ...styles });
107
+ }
108
+ }
109
+
110
+ export class StickerLayer extends Layer {
111
+ declare readonly data: StickerLayerData;
112
+
113
+ get uri(): string { return this.data.uri; }
114
+
115
+ withUri(uri: string): StickerLayer {
116
+ return new StickerLayer({ ...this.data, uri });
117
+ }
118
+ }
119
+
120
+ export function isTextLayer(layer: Layer): layer is TextLayer {
121
+ return layer.isText();
122
+ }
123
+
124
+ export function isStickerLayer(layer: Layer): layer is StickerLayer {
125
+ return layer.isSticker();
126
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Transform Value Object
3
+ * Represents position, scale, and rotation state
4
+ */
5
+
6
+ export interface Transform {
7
+ x: number;
8
+ y: number;
9
+ scale: number;
10
+ rotation: number;
11
+ }
12
+
13
+ export const DEFAULT_TRANSFORM: Transform = {
14
+ x: 50,
15
+ y: 50,
16
+ scale: 1,
17
+ rotation: 0,
18
+ };
19
+
20
+ export class TransformVO {
21
+ constructor(private readonly value: Transform) {}
22
+
23
+ get x(): number { return this.value.x; }
24
+ get y(): number { return this.value.y; }
25
+ get scale(): number { return this.value.scale; }
26
+ get rotation(): number { return this.value.rotation; }
27
+
28
+ withX(x: number): TransformVO {
29
+ return new TransformVO({ ...this.value, x });
30
+ }
31
+
32
+ withY(y: number): TransformVO {
33
+ return new TransformVO({ ...this.value, y });
34
+ }
35
+
36
+ withScale(scale: number): TransformVO {
37
+ return new TransformVO({ ...this.value, scale });
38
+ }
39
+
40
+ withRotation(rotation: number): TransformVO {
41
+ return new TransformVO({ ...this.value, rotation });
42
+ }
43
+
44
+ toJSON(): Transform {
45
+ return { ...this.value };
46
+ }
47
+
48
+ static from(transform: Transform): TransformVO {
49
+ return new TransformVO(transform);
50
+ }
51
+
52
+ static default(): TransformVO {
53
+ return new TransformVO(DEFAULT_TRANSFORM);
54
+ }
55
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * History Service
3
+ * Manages undo/redo state with configurable max history
4
+ */
5
+
6
+ export interface HistoryState<T> {
7
+ past: T[];
8
+ present: T;
9
+ future: T[];
10
+ }
11
+
12
+ export class HistoryService<T> {
13
+ constructor(private readonly maxHistory: number = 20) {}
14
+
15
+ createInitialState(initialValue: T): HistoryState<T> {
16
+ return { past: [], present: initialValue, future: [] };
17
+ }
18
+
19
+ push(history: HistoryState<T>, newValue: T): HistoryState<T> {
20
+ return {
21
+ past: [...history.past.slice(-this.maxHistory + 1), history.present],
22
+ present: newValue,
23
+ future: [],
24
+ };
25
+ }
26
+
27
+ undo(history: HistoryState<T>): HistoryState<T> {
28
+ if (!this.canUndo(history)) return history;
29
+
30
+ const previous = history.past[history.past.length - 1];
31
+ return {
32
+ past: history.past.slice(0, -1),
33
+ present: previous,
34
+ future: [history.present, ...history.future],
35
+ };
36
+ }
37
+
38
+ redo(history: HistoryState<T>): HistoryState<T> {
39
+ if (!this.canRedo(history)) return history;
40
+
41
+ const next = history.future[0];
42
+ return {
43
+ past: [...history.past, history.present],
44
+ present: next,
45
+ future: history.future.slice(1),
46
+ };
47
+ }
48
+
49
+ canUndo(history: HistoryState<T>): boolean {
50
+ return history.past.length > 0;
51
+ }
52
+
53
+ canRedo(history: HistoryState<T>): boolean {
54
+ return history.future.length > 0;
55
+ }
56
+
57
+ clear(history: HistoryState<T>): HistoryState<T> {
58
+ return { past: [], present: history.present, future: [] };
59
+ }
60
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Layer Service
3
+ * Business logic for layer operations
4
+ */
5
+
6
+ import { Layer, TextLayer, StickerLayer, isTextLayer } from "../entities/Layer";
7
+ import type { Transform } from "../entities/Transform";
8
+
9
+ export class LayerService {
10
+ generateId(type: "text" | "sticker"): string {
11
+ return `${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
12
+ }
13
+
14
+ createTextLayer(overrides: Partial<Omit<TextLayerData, "id" | "type">> = {}): TextLayer {
15
+ const id = this.generateId("text");
16
+ const defaults: TextLayerData = {
17
+ id,
18
+ type: "text",
19
+ text: "",
20
+ x: 50,
21
+ y: 50,
22
+ rotation: 0,
23
+ scale: 1,
24
+ opacity: 1,
25
+ zIndex: 0,
26
+ fontSize: 32,
27
+ fontFamily: "System",
28
+ color: "#FFFFFF",
29
+ backgroundColor: "transparent",
30
+ textAlign: "center",
31
+ isBold: false,
32
+ isItalic: false,
33
+ };
34
+
35
+ return new TextLayer({ ...defaults, ...overrides });
36
+ }
37
+
38
+ createStickerLayer(uri: string, overrides: Partial<Omit<StickerLayerData, "id" | "type" | "uri">> = {}): StickerLayer {
39
+ const id = this.generateId("sticker");
40
+ const defaults: StickerLayerData = {
41
+ id,
42
+ type: "sticker",
43
+ uri,
44
+ x: 100,
45
+ y: 100,
46
+ rotation: 0,
47
+ scale: 1,
48
+ opacity: 1,
49
+ zIndex: 0,
50
+ };
51
+
52
+ return new StickerLayer({ ...defaults, ...overrides });
53
+ }
54
+
55
+ updateLayer(layers: Layer[], layerId: string, updates: Partial<Transform>): Layer[] {
56
+ return layers.map(layer =>
57
+ layer.id === layerId ? layer.withTransform(updates) : layer
58
+ );
59
+ }
60
+
61
+ deleteLayer(layers: Layer[], layerId: string): Layer[] {
62
+ return layers.filter(layer => layer.id !== layerId);
63
+ }
64
+
65
+ duplicateLayer(layers: Layer[], layerId: string): Layer[] {
66
+ const layer = layers.find(l => l.id === layerId);
67
+ if (!layer) return layers;
68
+
69
+ const maxZIndex = layers.length > 0 ? Math.max(...layers.map(l => l.zIndex)) : -1;
70
+ const duplicateData = { ...layer.toJSON(), id: this.generateId(layer.type), x: layer.x + 20, y: layer.y + 20, zIndex: maxZIndex + 1 };
71
+ const duplicate = Layer.from(duplicateData);
72
+
73
+ return [...layers, duplicate];
74
+ }
75
+
76
+ moveLayerUp(layers: Layer[], layerId: string): Layer[] {
77
+ const sorted = this.sortByZIndex(layers);
78
+ const idx = sorted.findIndex(l => l.id === layerId);
79
+ if (idx >= sorted.length - 1) return layers;
80
+
81
+ const reordered = [...sorted];
82
+ [reordered[idx], reordered[idx + 1]] = [reordered[idx + 1], reordered[idx]];
83
+
84
+ return this.reassignZIndex(reordered);
85
+ }
86
+
87
+ moveLayerDown(layers: Layer[], layerId: string): Layer[] {
88
+ const sorted = this.sortByZIndex(layers);
89
+ const idx = sorted.findIndex(l => l.id === layerId);
90
+ if (idx <= 0) return layers;
91
+
92
+ const reordered = [...sorted];
93
+ [reordered[idx], reordered[idx - 1]] = [reordered[idx - 1], reordered[idx]];
94
+
95
+ return this.reassignZIndex(reordered);
96
+ }
97
+
98
+ sortByZIndex(layers: Layer[]): Layer[] {
99
+ return [...layers].sort((a, b) => a.zIndex - b.zIndex);
100
+ }
101
+
102
+ private reassignZIndex(layers: Layer[]): Layer[] {
103
+ return layers.map((layer, i) => layer.withZIndex(i));
104
+ }
105
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,29 @@
1
- // Public API exports
1
+ /**
2
+ * @umituz/react-native-photo-editor
3
+ *
4
+ * A powerful, generic photo editor for React Native
5
+ * Built with DDD principles for maintainability
6
+ */
7
+
8
+ // Main component
2
9
  export { PhotoEditor } from "./PhotoEditor";
3
10
  export type { PhotoEditorProps } from "./PhotoEditor";
4
11
 
5
- // Type exports for consumer usage
6
- export * from "./types";
12
+ // Domain entities
13
+ export type { Layer, TextLayer, StickerLayer } from "./domain/entities/Layer";
14
+ export type { Transform } from "./domain/entities/Transform";
15
+ export type { FilterValues, FiltersVO } from "./domain/entities/Filters";
16
+ export { isTextLayer, isStickerLayer } from "./domain/entities/Layer";
17
+
18
+ // Application hooks
19
+ export { useEditor } from "./application/hooks/useEditor";
20
+ export { useEditorUI } from "./application/hooks/useEditorUI";
7
21
 
8
- // Constant exports for consumer customization
9
- export * from "./constants";
22
+ // Types & constants
23
+ export type { LayerTransform } from "./infrastructure/gesture/types";
24
+ export { DEFAULT_IMAGE_FILTERS } from "./domain/entities/Filters";
25
+ export { DEFAULT_FONTS, DEFAULT_TEXT_COLORS, DEFAULT_STICKERS, DEFAULT_AI_STYLES } from "./constants";
26
+ export type { FilterOption } from "./presentation/components/sheets/FilterSheet";
27
+
28
+ // Legacy types (for backward compatibility)
29
+ export * from "./types";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Gesture Types
3
+ * Shared types for transform gestures
4
+ */
5
+
6
+ import type { Transform } from "../../domain/entities/Transform";
7
+
8
+ export interface TransformGestureState {
9
+ position: { x: number; y: number };
10
+ scale: number;
11
+ rotation: number;
12
+ }
13
+
14
+ export interface TransformGestureConfig {
15
+ minScale?: number;
16
+ maxScale?: number;
17
+ onTransformEnd: (transform: Transform) => void;
18
+ onPress?: () => void;
19
+ }
20
+
21
+ export interface TransformGestureHandlers {
22
+ panGesture: ReturnType<typeof import("react-native-gesture-handler").Gesture.Pan>;
23
+ pinchGesture: ReturnType<typeof import("react-native-gesture-handler").Gesture.Pinch>;
24
+ rotationGesture: ReturnType<typeof import("react-native-gesture-handler").Gesture.Rotation>;
25
+ tapGesture: ReturnType<typeof import("react-native-gesture-handler").Gesture.Tap>;
26
+ composed: ReturnType<typeof import("react-native-gesture-handler").Gesture.Exclusive>;
27
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Transform Gesture Hook
3
+ * Reusable gesture logic for draggable components
4
+ * Eliminates ~180 lines of duplicate code
5
+ */
6
+
7
+ import { useState, useRef, useCallback, useEffect } from "react";
8
+ import { Gesture } from "react-native-gesture-handler";
9
+ import type { Transform } from "../../domain/entities/Transform";
10
+ import type { TransformGestureConfig, TransformGestureState } from "./types";
11
+
12
+ const DEFAULT_STATE: TransformGestureState = {
13
+ position: { x: 50, y: 50 },
14
+ scale: 1,
15
+ rotation: 0,
16
+ };
17
+
18
+ export function useTransformGesture(
19
+ initialTransform: Partial<Transform>,
20
+ config: TransformGestureConfig
21
+ ) {
22
+ const {
23
+ minScale = 0.2,
24
+ maxScale = 6,
25
+ onTransformEnd,
26
+ onPress,
27
+ } = config;
28
+
29
+ // State
30
+ const [state, setState] = useState<TransformGestureState>(() => ({
31
+ position: { x: initialTransform.x ?? 50, y: initialTransform.y ?? 50 },
32
+ scale: initialTransform.scale ?? 1,
33
+ rotation: initialTransform.rotation ?? 0,
34
+ }));
35
+
36
+ // Sync state when props change (undo/redo)
37
+ useEffect(() => {
38
+ setState(prev => ({
39
+ ...prev,
40
+ position: { x: initialTransform.x ?? prev.position.x, y: initialTransform.y ?? prev.position.y },
41
+ scale: initialTransform.scale ?? prev.scale,
42
+ rotation: initialTransform.rotation ?? prev.rotation,
43
+ }));
44
+ }, [initialTransform.x, initialTransform.y, initialTransform.scale, initialTransform.rotation]);
45
+
46
+ // Refs for gesture callbacks
47
+ const stateRef = useRef(state);
48
+ stateRef.current = state;
49
+ const onTransformEndRef = useRef(onTransformEnd);
50
+ onTransformEndRef.current = onTransformEnd;
51
+ const onPressRef = useRef(onPress);
52
+ onPressRef.current = onPress;
53
+
54
+ // Start values for gestures
55
+ const offsetRef = useRef(state.position);
56
+ const scaleStartRef = useRef(state.scale);
57
+ const rotationStartRef = useRef(state.rotation);
58
+
59
+ // Emit transform
60
+ const emitTransform = useCallback(() => {
61
+ onTransformEndRef.current({
62
+ x: stateRef.current.position.x,
63
+ y: stateRef.current.position.y,
64
+ scale: stateRef.current.scale,
65
+ rotation: stateRef.current.rotation,
66
+ });
67
+ }, []);
68
+
69
+ // Pan gesture
70
+ const panGesture = Gesture.Pan()
71
+ .runOnJS(true)
72
+ .averageTouches(true)
73
+ .onStart(() => {
74
+ offsetRef.current = stateRef.current.position;
75
+ })
76
+ .onUpdate((e) => {
77
+ setState(prev => ({
78
+ ...prev,
79
+ position: {
80
+ x: offsetRef.current.x + e.translationX,
81
+ y: offsetRef.current.y + e.translationY,
82
+ },
83
+ }));
84
+ })
85
+ .onEnd(emitTransform);
86
+
87
+ // Pinch gesture
88
+ const pinchGesture = Gesture.Pinch()
89
+ .runOnJS(true)
90
+ .onStart(() => {
91
+ scaleStartRef.current = stateRef.current.scale;
92
+ })
93
+ .onUpdate((e) => {
94
+ setState(prev => ({
95
+ ...prev,
96
+ scale: Math.max(minScale, Math.min(maxScale, scaleStartRef.current * e.scale)),
97
+ }));
98
+ })
99
+ .onEnd(emitTransform);
100
+
101
+ // Rotation gesture
102
+ const rotationGesture = Gesture.Rotation()
103
+ .runOnJS(true)
104
+ .onStart(() => {
105
+ rotationStartRef.current = stateRef.current.rotation;
106
+ })
107
+ .onUpdate((e) => {
108
+ setState(prev => ({
109
+ ...prev,
110
+ rotation: rotationStartRef.current + (e.rotation * 180) / Math.PI,
111
+ }));
112
+ })
113
+ .onEnd(emitTransform);
114
+
115
+ // Tap gesture
116
+ const tapGesture = Gesture.Tap()
117
+ .runOnJS(true)
118
+ .onEnd(() => onPressRef.current?.());
119
+
120
+ // Composed gesture
121
+ const composed = Gesture.Exclusive(
122
+ Gesture.Simultaneous(panGesture, pinchGesture, rotationGesture),
123
+ tapGesture,
124
+ );
125
+
126
+ return {
127
+ state,
128
+ gestures: {
129
+ pan: panGesture,
130
+ pinch: pinchGesture,
131
+ rotation: rotationGesture,
132
+ tap: tapGesture,
133
+ composed,
134
+ },
135
+ };
136
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * History Manager Implementation
3
+ * Legacy compatibility wrapper for HistoryService
4
+ */
5
+
6
+ import { HistoryService } from "../../domain/services/HistoryService";
7
+
8
+ const historyService = new HistoryService(20);
9
+
10
+ export type { HistoryState } from "../../domain/services/HistoryService";
11
+
12
+ export class HistoryManager<T> {
13
+ private readonly maxHistory = 20;
14
+
15
+ createInitialState(initialValue: T) {
16
+ return historyService.createInitialState(initialValue);
17
+ }
18
+
19
+ push(history: any, newValue: T) {
20
+ return historyService.push(history, newValue);
21
+ }
22
+
23
+ undo(history: any) {
24
+ return historyService.undo(history);
25
+ }
26
+
27
+ redo(history: any) {
28
+ return historyService.redo(history);
29
+ }
30
+
31
+ canUndo(history: any) {
32
+ return historyService.canUndo(history);
33
+ }
34
+
35
+ canRedo(history: any) {
36
+ return historyService.canRedo(history);
37
+ }
38
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Draggable Layer Component
3
+ * Unified draggable component for both text and stickers
4
+ * Replaces DraggableText + DraggableSticker (~180 lines of duplicate code)
5
+ */
6
+
7
+ import React, { memo } from "react";
8
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
9
+ import { GestureDetector } from "react-native-gesture-handler";
10
+ import { Image } from "expo-image";
11
+ import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
12
+ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
13
+ import { useTransformGesture } from "../../infrastructure/gesture/useTransformGesture";
14
+ import { Layer, isTextLayer, isStickerLayer } from "../../domain/entities/Layer";
15
+
16
+ interface DraggableLayerProps {
17
+ layer: Layer;
18
+ isSelected: boolean;
19
+ onPress: () => void;
20
+ onTransformEnd: (transform: { x: number; y: number; scale: number; rotation: number }) => void;
21
+ }
22
+
23
+ const isEmojiString = (str: string) =>
24
+ str.length <= 4 && !/^https?:\/\//i.test(str) && !str.startsWith("/");
25
+
26
+ export const DraggableLayer = memo<DraggableLayerProps>(({
27
+ layer,
28
+ isSelected,
29
+ onPress,
30
+ onTransformEnd,
31
+ }) => {
32
+ const tokens = useAppDesignTokens();
33
+ const { state, gestures } = useTransformGesture(layer, {
34
+ onTransformEnd,
35
+ onPress,
36
+ });
37
+
38
+ return (
39
+ <GestureDetector gesture={gestures.composed}>
40
+ <View
41
+ accessibilityLabel={isTextLayer(layer) ? layer.text || "Text layer" : "Sticker layer"}
42
+ accessibilityRole="button"
43
+ style={[
44
+ styles.container,
45
+ {
46
+ transform: [
47
+ { translateX: state.position.x },
48
+ { translateY: state.position.y },
49
+ { rotate: `${state.rotation}deg` },
50
+ { scale: state.scale },
51
+ ],
52
+ opacity: layer.opacity,
53
+ zIndex: isSelected ? 100 : layer.zIndex,
54
+ },
55
+ ]}
56
+ >
57
+ <View
58
+ style={{
59
+ padding: tokens.spacing.xs,
60
+ borderRadius: tokens.borders.radius.sm,
61
+ borderWidth: isSelected ? 2 : 0,
62
+ borderColor: tokens.colors.primary,
63
+ borderStyle: "dashed",
64
+ backgroundColor: isSelected
65
+ ? tokens.colors.primary + "10"
66
+ : isTextLayer(layer)
67
+ ? layer.backgroundColor
68
+ : "transparent",
69
+ }}
70
+ >
71
+ {isTextLayer(layer) ? (
72
+ <AtomicText
73
+ style={{
74
+ fontSize: layer.fontSize,
75
+ fontFamily: layer.fontFamily === "System" ? undefined : layer.fontFamily,
76
+ color: layer.color,
77
+ textAlign: layer.textAlign,
78
+ fontWeight: layer.isBold ? "900" : "normal",
79
+ fontStyle: layer.isItalic ? "italic" : "normal",
80
+ }}
81
+ >
82
+ {layer.text || "TAP TO EDIT"}
83
+ </AtomicText>
84
+ ) : isStickerLayer(layer) ? (
85
+ renderStickerContent(layer.uri, tokens)
86
+ ) : null}
87
+ </View>
88
+ </View>
89
+ </GestureDetector>
90
+ );
91
+ });
92
+
93
+ DraggableLayer.displayName = "DraggableLayer";
94
+
95
+ function renderStickerContent(uri: string, tokens: any) {
96
+ const isEmoji = isEmojiString(uri);
97
+
98
+ if (isEmoji) {
99
+ return <AtomicText style={{ fontSize: 48 }}>{uri}</AtomicText>;
100
+ }
101
+
102
+ return (
103
+ <Image
104
+ source={{ uri }}
105
+ style={{ width: 80, height: 80 }}
106
+ contentFit="contain"
107
+ accessibilityIgnoresInvertColors
108
+ />
109
+ );
110
+ }
111
+
112
+ const styles = StyleSheet.create({
113
+ container: { position: "absolute" },
114
+ });