@umituz/react-native-image 1.3.8 → 1.3.10

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 (23) hide show
  1. package/package.json +8 -2
  2. package/src/index.ts +5 -4
  3. package/src/infrastructure/services/ImageConversionService.ts +7 -32
  4. package/src/infrastructure/services/ImageEditorService.ts +41 -179
  5. package/src/infrastructure/services/{ImageAIEnhancementService.ts → ImageEnhanceService.ts} +42 -55
  6. package/src/infrastructure/services/ImageTemplateService.ts +10 -4
  7. package/src/infrastructure/services/ImageTransformService.ts +43 -93
  8. package/src/infrastructure/utils/FilterProcessor.ts +26 -263
  9. package/src/infrastructure/utils/{AIImageAnalysisUtils.ts → ImageAnalysisUtils.ts} +3 -3
  10. package/src/infrastructure/utils/ImageEditorHistoryUtils.ts +63 -0
  11. package/src/infrastructure/utils/ImageFilterUtils.ts +152 -0
  12. package/src/infrastructure/utils/ImageTransformUtils.ts +25 -0
  13. package/src/infrastructure/utils/LayerManager.ts +0 -81
  14. package/src/presentation/components/editor/FilterPickerSheet.tsx +75 -0
  15. package/src/presentation/components/editor/StickerPickerSheet.tsx +62 -0
  16. package/src/presentation/components/editor/TextEditorSheet.tsx +98 -0
  17. package/src/presentation/components/editor/TextEditorTabs.tsx +111 -0
  18. package/src/presentation/hooks/useImage.ts +5 -8
  19. package/src/presentation/hooks/useImageEnhance.ts +32 -0
  20. package/src/presentation/hooks/useImageTransform.ts +3 -4
  21. package/src/infrastructure/services/ImageAdvancedTransformService.ts +0 -106
  22. package/src/infrastructure/services/ImageSpecializedEnhancementService.ts +0 -57
  23. package/src/presentation/hooks/useImageAIEnhancement.ts +0 -33
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Infrastructure - Filter Utils
3
+ *
4
+ * Low-level filter implementations for raw pixel data
5
+ */
6
+
7
+ export class ImageFilterUtils {
8
+ static applyBrightness(
9
+ data: Uint8ClampedArray,
10
+ brightness: number
11
+ ): Uint8ClampedArray {
12
+ const result = new Uint8ClampedArray(data);
13
+ const adjustment = brightness * 2.55;
14
+
15
+ for (let i = 0; i < result.length; i += 4) {
16
+ result[i] = Math.min(255, Math.max(0, result[i] + adjustment));
17
+ result[i + 1] = Math.min(255, Math.max(0, result[i + 1] + adjustment));
18
+ result[i + 2] = Math.min(255, Math.max(0, result[i + 2] + adjustment));
19
+ }
20
+
21
+ return result;
22
+ }
23
+
24
+ static applyContrast(
25
+ data: Uint8ClampedArray,
26
+ contrast: number
27
+ ): Uint8ClampedArray {
28
+ const result = new Uint8ClampedArray(data);
29
+ const factor = (259 * (contrast * 2.55 + 255)) / (255 * (259 - contrast * 2.55));
30
+
31
+ for (let i = 0; i < result.length; i += 4) {
32
+ result[i] = Math.min(255, Math.max(0, factor * (result[i] - 128) + 128));
33
+ result[i + 1] = Math.min(255, Math.max(0, factor * (result[i + 1] - 128) + 128));
34
+ result[i + 2] = Math.min(255, Math.max(0, factor * (result[i + 2] - 128) + 128));
35
+ }
36
+
37
+ return result;
38
+ }
39
+
40
+ static applySaturation(
41
+ data: Uint8ClampedArray,
42
+ saturation: number
43
+ ): Uint8ClampedArray {
44
+ const result = new Uint8ClampedArray(data);
45
+ const adjustment = 1 + (saturation / 100);
46
+
47
+ for (let i = 0; i < result.length; i += 4) {
48
+ const r = result[i];
49
+ const g = result[i + 1];
50
+ const b = result[i + 2];
51
+
52
+ const gray = 0.299 * r + 0.587 * g + 0.114 * b;
53
+
54
+ result[i] = Math.min(255, Math.max(0, gray + adjustment * (r - gray)));
55
+ result[i + 1] = Math.min(255, Math.max(0, gray + adjustment * (g - gray)));
56
+ result[i + 2] = Math.min(255, Math.max(0, gray + adjustment * (b - gray)));
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ static applyVintage(
63
+ data: Uint8ClampedArray,
64
+ intensity: number,
65
+ warmth: number
66
+ ): Uint8ClampedArray {
67
+ const result = new Uint8ClampedArray(data);
68
+ const factor = intensity / 100;
69
+ const warmFactor = warmth / 100;
70
+
71
+ for (let i = 0; i < result.length; i += 4) {
72
+ let r = result[i];
73
+ let g = result[i + 1];
74
+ let b = result[i + 2];
75
+
76
+ const tr = 0.393 * r + 0.769 * g + 0.189 * b;
77
+ const tg = 0.349 * r + 0.686 * g + 0.168 * b;
78
+ const tb = 0.272 * r + 0.534 * g + 0.131 * b;
79
+
80
+ r = r * (1 - factor) + tr * factor;
81
+ g = g * (1 - factor) + tg * factor;
82
+ b = b * (1 - factor) + tb * factor;
83
+
84
+ if (warmFactor > 0) {
85
+ result[i] = Math.min(255, r + warmFactor * 20);
86
+ result[i + 1] = Math.min(255, g + warmFactor * 10);
87
+ result[i + 2] = Math.min(255, b * (1 - warmFactor * 0.3));
88
+ } else {
89
+ result[i] = r;
90
+ result[i + 1] = g;
91
+ result[i + 2] = Math.min(255, b * (1 - Math.abs(warmFactor) * 0.3));
92
+ }
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ static applyBlur(
99
+ data: Uint8ClampedArray,
100
+ radius: number,
101
+ width: number,
102
+ height: number
103
+ ): Uint8ClampedArray {
104
+ const result = new Uint8ClampedArray(data);
105
+ const size = Math.floor(radius) || 1;
106
+
107
+ for (let y = 0; y < height; y++) {
108
+ for (let x = 0; x < width; x++) {
109
+ let r = 0, g = 0, b = 0, a = 0;
110
+ let count = 0;
111
+
112
+ for (let dy = -size; dy <= size; dy++) {
113
+ for (let dx = -size; dx <= size; dx++) {
114
+ const ny = y + dy;
115
+ const nx = x + dx;
116
+
117
+ if (ny >= 0 && ny < height && nx >= 0 && nx < width) {
118
+ const idx = (ny * width + nx) * 4;
119
+ r += data[idx];
120
+ g += data[idx + 1];
121
+ b += data[idx + 2];
122
+ a += data[idx + 3];
123
+ count++;
124
+ }
125
+ }
126
+ }
127
+
128
+ const idx = (y * width + x) * 4;
129
+ result[idx] = r / count;
130
+ result[idx + 1] = g / count;
131
+ result[idx + 2] = b / count;
132
+ result[idx + 3] = a / count;
133
+ }
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ static applyIntensity(
140
+ originalData: Uint8ClampedArray,
141
+ processedData: Uint8ClampedArray,
142
+ intensity: number
143
+ ): Uint8ClampedArray {
144
+ const result = new Uint8ClampedArray(originalData.length);
145
+
146
+ for (let i = 0; i < originalData.length; i++) {
147
+ result[i] = originalData[i] * (1 - intensity) + processedData[i] * intensity;
148
+ }
149
+
150
+ return result;
151
+ }
152
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Infrastructure - Transform Utils
3
+ *
4
+ * Internal utilities for image transformations
5
+ */
6
+
7
+ import * as ImageManipulator from 'expo-image-manipulator';
8
+ import type { SaveFormat, ImageSaveOptions } from '../../domain/entities/ImageTypes';
9
+ import { IMAGE_CONSTANTS } from '../../domain/entities/ImageConstants';
10
+
11
+ export class ImageTransformUtils {
12
+ static mapFormat(format?: SaveFormat): ImageManipulator.SaveFormat {
13
+ if (format === 'png') return ImageManipulator.SaveFormat.PNG;
14
+ if (format === 'webp') return ImageManipulator.SaveFormat.WEBP;
15
+ return ImageManipulator.SaveFormat.JPEG;
16
+ }
17
+
18
+ static buildSaveOptions(options?: ImageSaveOptions): ImageManipulator.SaveOptions {
19
+ return {
20
+ compress: options?.compress ?? IMAGE_CONSTANTS.defaultQuality,
21
+ format: this.mapFormat(options?.format),
22
+ base64: options?.base64,
23
+ };
24
+ }
25
+ }
@@ -13,44 +13,7 @@ export interface LayerComposition {
13
13
  }
14
14
 
15
15
  export class LayerManager {
16
- static composeLayers(
17
- layers: Array<{
18
- canvas: HTMLCanvasElement | any;
19
- opacity: number;
20
- visible: boolean;
21
- x?: number;
22
- y?: number;
23
- }>,
24
- composition: LayerComposition
25
- ): HTMLCanvasElement | any {
26
- // Create composition canvas
27
- const composedCanvas = document.createElement('canvas') || {} as any;
28
- composedCanvas.width = composition.width;
29
- composedCanvas.height = composition.height;
30
- const ctx = composedCanvas.getContext('2d');
31
-
32
- if (!ctx) return composedCanvas;
33
-
34
- // Clear canvas with background color
35
- ctx.fillStyle = composition.backgroundColor || '#ffffff';
36
- ctx.fillRect(0, 0, composition.width, composition.height);
37
-
38
- // Draw each layer
39
- layers.forEach(layer => {
40
- if (!layer.visible) return;
41
16
 
42
- ctx.save();
43
- ctx.globalAlpha = layer.opacity;
44
- ctx.drawImage(
45
- layer.canvas,
46
- layer.x || 0,
47
- layer.y || 0
48
- );
49
- ctx.restore();
50
- });
51
-
52
- return composedCanvas;
53
- }
54
17
 
55
18
  static mergeLayers(
56
19
  layers: Array<{
@@ -110,49 +73,5 @@ export class LayerManager {
110
73
  return result.map((layer, index) => ({ ...layer, index }));
111
74
  }
112
75
 
113
- static flattenLayers(
114
- layers: Array<{
115
- canvas: HTMLCanvasElement | any;
116
- opacity: number;
117
- visible: boolean;
118
- }>,
119
- composition: LayerComposition
120
- ): HTMLCanvasElement | any {
121
- return LayerManager.composeLayers(layers, composition);
122
- }
123
-
124
- static createLayerCanvas(
125
- width: number,
126
- height: number,
127
- transparent: boolean = true
128
- ): HTMLCanvasElement | any {
129
- const canvas = document.createElement('canvas') || {} as any;
130
- canvas.width = width;
131
- canvas.height = height;
132
-
133
- if (!transparent) {
134
- const ctx = canvas.getContext('2d');
135
- if (ctx) {
136
- ctx.fillStyle = '#ffffff';
137
- ctx.fillRect(0, 0, width, height);
138
- }
139
- }
140
76
 
141
- return canvas;
142
- }
143
-
144
- static clearLayer(
145
- canvas: HTMLCanvasElement | any,
146
- preserveAlpha: boolean = true
147
- ): void {
148
- const ctx = canvas.getContext('2d');
149
- if (!ctx) return;
150
-
151
- if (preserveAlpha) {
152
- ctx.clearRect(0, 0, canvas.width, canvas.height);
153
- } else {
154
- ctx.fillStyle = 'transparent';
155
- ctx.fillRect(0, 0, canvas.width, canvas.height);
156
- }
157
- }
158
77
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Presentation - Filter Picker Sheet
3
+ */
4
+
5
+ import React, { forwardRef } from 'react';
6
+ import { View, TouchableOpacity, ScrollView } from 'react-native';
7
+ import {
8
+ BottomSheetModal,
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ BottomSheetModalRef
13
+ } from '@umituz/react-native-design-system';
14
+ import { FilterProcessor, type FilterPreset } from '../../../infrastructure/utils/FilterProcessor';
15
+
16
+ export interface FilterPickerSheetProps {
17
+ onSelectFilter: (filterId: string) => void;
18
+ onDismiss: () => void;
19
+ activeFilterId?: string;
20
+ snapPoints?: string[];
21
+ title?: string;
22
+ }
23
+
24
+ export const FilterPickerSheet = forwardRef<BottomSheetModalRef, FilterPickerSheetProps>(
25
+ ({ onSelectFilter, onDismiss, activeFilterId, snapPoints = ['50%'], title = 'Filters' }, ref) => {
26
+ const tokens = useAppDesignTokens();
27
+ const presets = FilterProcessor.getPresets();
28
+
29
+ return (
30
+ <BottomSheetModal ref={ref} snapPoints={snapPoints} onDismiss={onDismiss}>
31
+ <View style={{ padding: tokens.spacing.lg, flex: 1 }}>
32
+ <AtomicText style={{ ...tokens.typography.headingSmall, marginBottom: tokens.spacing.md }}>
33
+ {title}
34
+ </AtomicText>
35
+
36
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
37
+ <View style={{ flexDirection: 'row', gap: tokens.spacing.md }}>
38
+ {presets.map((preset: FilterPreset) => (
39
+ <TouchableOpacity
40
+ key={preset.id}
41
+ onPress={() => onSelectFilter(preset.id)}
42
+ style={{
43
+ width: 100,
44
+ height: 120,
45
+ backgroundColor: activeFilterId === preset.id ? tokens.colors.primaryContainer : tokens.colors.surfaceVariant,
46
+ borderRadius: tokens.borderRadius.lg,
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ borderWidth: 2,
50
+ borderColor: activeFilterId === preset.id ? tokens.colors.primary : 'transparent',
51
+ }}
52
+ >
53
+ <AtomicIcon
54
+ name="color-filter"
55
+ size={32}
56
+ color={activeFilterId === preset.id ? 'primary' : 'secondary'}
57
+ />
58
+ <AtomicText style={{
59
+ ...tokens.typography.labelSmall,
60
+ marginTop: tokens.spacing.sm,
61
+ color: activeFilterId === preset.id ? tokens.colors.primary : tokens.colors.textSecondary
62
+ }}>
63
+ {preset.name}
64
+ </AtomicText>
65
+ </TouchableOpacity>
66
+ ))}
67
+ </View>
68
+ </ScrollView>
69
+ </View>
70
+ </BottomSheetModal>
71
+ );
72
+ }
73
+ );
74
+
75
+ FilterPickerSheet.displayName = 'FilterPickerSheet';
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Presentation - Sticker Picker Sheet
3
+ */
4
+
5
+ import React, { forwardRef } from 'react';
6
+ import { View, TouchableOpacity, ScrollView, Image, Dimensions } from 'react-native';
7
+ import {
8
+ BottomSheetModal,
9
+ AtomicText,
10
+ useAppDesignTokens,
11
+ BottomSheetModalRef
12
+ } from '@umituz/react-native-design-system';
13
+
14
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
15
+
16
+ export interface StickerPickerSheetProps {
17
+ stickers: string[];
18
+ onSelectSticker: (uri: string) => void;
19
+ onDismiss: () => void;
20
+ title?: string;
21
+ snapPoints?: string[];
22
+ }
23
+
24
+ export const StickerPickerSheet = forwardRef<BottomSheetModalRef, StickerPickerSheetProps>(
25
+ ({ stickers, onSelectSticker, onDismiss, title = 'Select Sticker', snapPoints = ['60%'] }, ref) => {
26
+ const tokens = useAppDesignTokens();
27
+
28
+ return (
29
+ <BottomSheetModal ref={ref} snapPoints={snapPoints} onDismiss={onDismiss}>
30
+ <View style={{ padding: tokens.spacing.lg, flex: 1 }}>
31
+ <AtomicText style={{ ...tokens.typography.headingSmall, marginBottom: tokens.spacing.md }}>
32
+ {title}
33
+ </AtomicText>
34
+
35
+ <ScrollView showsVerticalScrollIndicator={false}>
36
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: tokens.spacing.md }}>
37
+ {stickers.map((uri, index) => (
38
+ <TouchableOpacity
39
+ key={index}
40
+ onPress={() => onSelectSticker(uri)}
41
+ style={{
42
+ width: (SCREEN_WIDTH - 64) / 3,
43
+ aspectRatio: 1,
44
+ backgroundColor: tokens.colors.surfaceVariant,
45
+ borderRadius: tokens.borderRadius.md,
46
+ padding: tokens.spacing.sm,
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ }}
50
+ >
51
+ <Image source={{ uri }} style={{ width: '100%', height: '100%' }} resizeMode="contain" />
52
+ </TouchableOpacity>
53
+ ))}
54
+ </View>
55
+ </ScrollView>
56
+ </View>
57
+ </BottomSheetModal>
58
+ );
59
+ }
60
+ );
61
+
62
+ StickerPickerSheet.displayName = 'StickerPickerSheet';
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Presentation - Text Editor Sheet
3
+ */
4
+
5
+ import React, { forwardRef, useState } from 'react';
6
+ import { View, TouchableOpacity, ScrollView } from 'react-native';
7
+ import {
8
+ BottomSheetModal,
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ BottomSheetModalRef
13
+ } from '@umituz/react-native-design-system';
14
+ import { TextContentTab, TextStyleTab, TextTransformTab } from './TextEditorTabs';
15
+
16
+ export interface TextEditorSheetProps {
17
+ text: string;
18
+ onTextChange: (text: string) => void;
19
+ color: string;
20
+ onColorChange: (color: string) => void;
21
+ fontSize: number;
22
+ onFontSizeChange: (size: number) => void;
23
+ fontFamily: string;
24
+ onFontFamilyChange: (font: string) => void;
25
+ scale: number;
26
+ onScaleChange: (scale: number) => void;
27
+ rotation: number;
28
+ onRotationChange: (rotation: number) => void;
29
+ opacity: number;
30
+ onOpacityChange: (opacity: number) => void;
31
+ onDelete?: () => void;
32
+ onDismiss: () => void;
33
+ snapPoints?: string[];
34
+ t: (key: string) => string;
35
+ }
36
+
37
+ export const TextEditorSheet = forwardRef<BottomSheetModalRef, TextEditorSheetProps>((props, ref) => {
38
+ const tokens = useAppDesignTokens();
39
+ const [activeTab, setActiveTab] = useState<'content' | 'style' | 'transform'>('content');
40
+ const { onDismiss, snapPoints = ['75%'], t } = props;
41
+
42
+ const tabs = [
43
+ { id: 'content', label: 'Text', icon: 'text' },
44
+ { id: 'style', label: 'Style', icon: 'color-palette' },
45
+ { id: 'transform', label: 'Edit', icon: 'options' },
46
+ ];
47
+
48
+ return (
49
+ <BottomSheetModal ref={ref} snapPoints={snapPoints} onDismiss={onDismiss}>
50
+ <View style={{ padding: tokens.spacing.lg, flex: 1 }}>
51
+ <View style={{ flexDirection: 'row', gap: tokens.spacing.md, marginBottom: tokens.spacing.lg }}>
52
+ {tabs.map(tab => (
53
+ <TouchableOpacity
54
+ key={tab.id}
55
+ onPress={() => setActiveTab(tab.id as any)}
56
+ style={{
57
+ flex: 1, alignItems: 'center', paddingVertical: tokens.spacing.sm,
58
+ borderBottomWidth: 3, borderBottomColor: activeTab === tab.id ? tokens.colors.primary : 'transparent'
59
+ }}
60
+ >
61
+ <AtomicIcon name={tab.icon as any} size={20} color={activeTab === tab.id ? 'primary' : 'secondary'} />
62
+ <AtomicText style={{
63
+ ...tokens.typography.labelSmall,
64
+ color: activeTab === tab.id ? tokens.colors.primary : tokens.colors.textSecondary,
65
+ marginTop: 4
66
+ }}>
67
+ {tab.label}
68
+ </AtomicText>
69
+ </TouchableOpacity>
70
+ ))}
71
+ </View>
72
+
73
+ <ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 60 }}>
74
+ {activeTab === 'content' && <TextContentTab text={props.text} onTextChange={props.onTextChange} t={t} />}
75
+ {activeTab === 'style' && (
76
+ <TextStyleTab
77
+ fontSize={props.fontSize} setFontSize={props.onFontSizeChange}
78
+ color={props.color} setColor={props.onColorChange}
79
+ fontFamily={props.fontFamily} setFontFamily={props.onFontFamilyChange}
80
+ t={t}
81
+ />
82
+ )}
83
+ {activeTab === 'transform' && (
84
+ <TextTransformTab
85
+ scale={props.scale} setScale={props.onScaleChange}
86
+ rotation={props.rotation} setRotation={props.onRotationChange}
87
+ opacity={props.opacity} setOpacity={props.onOpacityChange}
88
+ onDelete={props.onDelete}
89
+ t={t}
90
+ />
91
+ )}
92
+ </ScrollView>
93
+ </View>
94
+ </BottomSheetModal>
95
+ );
96
+ });
97
+
98
+ TextEditorSheet.displayName = 'TextEditorSheet';
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Presentation - Text Editor Tabs
3
+ */
4
+
5
+ import React from 'react';
6
+ import { View, TextInput, ScrollView, TouchableOpacity } from 'react-native';
7
+ import Slider from '@react-native-community/slider';
8
+ import { AtomicText, AtomicIcon, useAppDesignTokens } from '@umituz/react-native-design-system';
9
+
10
+ interface TabProps {
11
+ t: (key: string) => string;
12
+ }
13
+
14
+ export const TextContentTab: React.FC<TabProps & { text: string; onTextChange: (t: string) => void }> = ({ text, onTextChange, t }) => {
15
+ const tokens = useAppDesignTokens();
16
+ return (
17
+ <View style={{ gap: tokens.spacing.lg }}>
18
+ <TextInput
19
+ value={text}
20
+ onChangeText={onTextChange}
21
+ placeholder={t('editor.text_placeholder')}
22
+ style={{
23
+ ...tokens.typography.bodyLarge,
24
+ borderWidth: 1,
25
+ borderColor: tokens.colors.border,
26
+ borderRadius: tokens.borderRadius.md,
27
+ padding: tokens.spacing.md,
28
+ minHeight: 120,
29
+ textAlignVertical: 'top',
30
+ }}
31
+ multiline
32
+ />
33
+ </View>
34
+ );
35
+ };
36
+
37
+ export const TextStyleTab: React.FC<TabProps & {
38
+ fontSize: number; setFontSize: (s: number) => void;
39
+ color: string; setColor: (c: string) => void;
40
+ fontFamily: string; setFontFamily: (f: string) => void;
41
+ }> = ({ fontSize, setFontSize, color, setColor, fontFamily, setFontFamily, t }) => {
42
+ const tokens = useAppDesignTokens();
43
+ const colors = ['#FFFFFF', '#000000', '#FF0000', '#FFFF00', '#0000FF', '#00FF00', '#FF00FF', '#FFA500'];
44
+ const fonts = ['System', 'serif', 'sans-serif', 'monospace'];
45
+
46
+ return (
47
+ <View style={{ gap: tokens.spacing.xl }}>
48
+ <View>
49
+ <AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.sm }}>Font</AtomicText>
50
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: tokens.spacing.sm }}>
51
+ {fonts.map(f => (
52
+ <TouchableOpacity key={f} onPress={() => setFontFamily(f)} style={{
53
+ paddingHorizontal: tokens.spacing.md, paddingVertical: tokens.spacing.xs, borderRadius: tokens.borderRadius.full,
54
+ borderWidth: 1, borderColor: fontFamily === f ? tokens.colors.primary : tokens.colors.border,
55
+ backgroundColor: fontFamily === f ? tokens.colors.primaryContainer : tokens.colors.surface
56
+ }}>
57
+ <AtomicText style={{ fontFamily: f }}>{f}</AtomicText>
58
+ </TouchableOpacity>
59
+ ))}
60
+ </ScrollView>
61
+ </View>
62
+
63
+ <View>
64
+ <AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.sm }}>Color</AtomicText>
65
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: tokens.spacing.sm }}>
66
+ {colors.map(c => (
67
+ <TouchableOpacity key={c} onPress={() => setColor(c)} style={{
68
+ width: 40, height: 40, borderRadius: 20, backgroundColor: c,
69
+ borderWidth: color === c ? 3 : 1, borderColor: tokens.colors.primary
70
+ }} />
71
+ ))}
72
+ </ScrollView>
73
+ </View>
74
+
75
+ <View>
76
+ <AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.xs }}>Size: {fontSize}px</AtomicText>
77
+ <Slider value={fontSize} onValueChange={setFontSize} minimumValue={12} maximumValue={100} step={1} minimumTrackTintColor={tokens.colors.primary} />
78
+ </View>
79
+ </View>
80
+ );
81
+ };
82
+
83
+ export const TextTransformTab: React.FC<TabProps & {
84
+ scale: number; setScale: (s: number) => void;
85
+ rotation: number; setRotation: (r: number) => void;
86
+ opacity: number; setOpacity: (o: number) => void;
87
+ onDelete?: () => void;
88
+ }> = ({ scale, setScale, rotation, setRotation, opacity, setOpacity, onDelete, t }) => {
89
+ const tokens = useAppDesignTokens();
90
+ return (
91
+ <View style={{ gap: tokens.spacing.xl }}>
92
+ <View>
93
+ <AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.xs }}>Scale: {scale.toFixed(2)}x</AtomicText>
94
+ <Slider value={scale} onValueChange={setScale} minimumValue={0.2} maximumValue={3} step={0.1} minimumTrackTintColor={tokens.colors.primary} />
95
+ </View>
96
+ <View>
97
+ <AtomicText style={{ ...tokens.typography.labelMedium, marginBottom: tokens.spacing.xs }}>Rotation: {Math.round(rotation)}°</AtomicText>
98
+ <Slider value={rotation} onValueChange={setRotation} minimumValue={0} maximumValue={360} step={1} minimumTrackTintColor={tokens.colors.primary} />
99
+ </View>
100
+ {onDelete && (
101
+ <TouchableOpacity onPress={onDelete} style={{
102
+ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: tokens.spacing.sm,
103
+ padding: tokens.spacing.md, borderRadius: tokens.borderRadius.md, borderWidth: 1, borderColor: tokens.colors.error
104
+ }}>
105
+ <AtomicIcon name="trash" size={20} color="error" />
106
+ <AtomicText style={{ ...tokens.typography.labelMedium, color: tokens.colors.error }}>Delete Layer</AtomicText>
107
+ </TouchableOpacity>
108
+ )}
109
+ </View>
110
+ );
111
+ };
@@ -7,36 +7,33 @@
7
7
  import { useImageTransform } from './useImageTransform';
8
8
  import { useImageConversion } from './useImageConversion';
9
9
  import { useImageBatch } from './useImageBatch';
10
- import { useImageAIEnhancement } from './useImageAIEnhancement';
10
+ import { useImageEnhance } from './useImageEnhance';
11
11
  import { useImageMetadata } from './useImageMetadata';
12
12
 
13
13
  export const useImage = () => {
14
14
  const transform = useImageTransform();
15
15
  const conversion = useImageConversion();
16
16
  const batch = useImageBatch();
17
- const aiEnhancement = useImageAIEnhancement();
17
+ const enhance = useImageEnhance();
18
18
  const metadata = useImageMetadata();
19
19
 
20
20
  return {
21
- // Basic operations
22
21
  ...transform,
23
22
  ...conversion,
24
- // Advanced operations
25
23
  ...batch,
26
- ...aiEnhancement,
24
+ ...enhance,
27
25
  ...metadata,
28
- // Combined state
29
26
  isProcessing:
30
27
  transform.isTransforming ||
31
28
  conversion.isConverting ||
32
29
  batch.isBatchProcessing ||
33
- aiEnhancement.isEnhancing ||
30
+ enhance.isEnhancing ||
34
31
  metadata.isExtracting,
35
32
  error:
36
33
  transform.transformError ||
37
34
  conversion.conversionError ||
38
35
  batch.batchError ||
39
- aiEnhancement.enhancementError ||
36
+ enhance.enhancementError ||
40
37
  metadata.metadataError,
41
38
  };
42
39
  };