@umituz/react-native-image 1.3.9 → 1.3.11

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.
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Infrastructure - Filter Processor
3
3
  *
4
- * Real-time filter processing with preview
4
+ * Filter processing with preset management
5
5
  */
6
6
 
7
+ import { ImageFilterUtils } from './ImageFilterUtils';
8
+
7
9
  export interface FilterPreset {
8
10
  id: string;
9
11
  name: string;
@@ -35,92 +37,39 @@ export class FilterProcessor {
35
37
  id: 'brightness',
36
38
  name: 'Brightness',
37
39
  category: 'basic',
38
- parameters: [
39
- {
40
- name: 'brightness',
41
- type: 'slider',
42
- min: -100,
43
- max: 100,
44
- value: 0,
45
- label: 'Brightness',
46
- },
47
- ],
40
+ parameters: [{ name: 'brightness', type: 'slider', min: -100, max: 100, value: 0, label: 'Brightness' }],
48
41
  },
49
42
  {
50
43
  id: 'contrast',
51
44
  name: 'Contrast',
52
45
  category: 'basic',
53
- parameters: [
54
- {
55
- name: 'contrast',
56
- type: 'slider',
57
- min: -100,
58
- max: 100,
59
- value: 0,
60
- label: 'Contrast',
61
- },
62
- ],
46
+ parameters: [{ name: 'contrast', type: 'slider', min: -100, max: 100, value: 0, label: 'Contrast' }],
63
47
  },
64
48
  {
65
49
  id: 'saturation',
66
50
  name: 'Saturation',
67
51
  category: 'color',
68
- parameters: [
69
- {
70
- name: 'saturation',
71
- type: 'slider',
72
- min: -100,
73
- max: 100,
74
- value: 0,
75
- label: 'Saturation',
76
- },
77
- ],
52
+ parameters: [{ name: 'saturation', type: 'slider', min: -100, max: 100, value: 0, label: 'Saturation' }],
78
53
  },
79
54
  {
80
55
  id: 'vintage',
81
56
  name: 'Vintage',
82
57
  category: 'vintage',
83
58
  parameters: [
84
- {
85
- name: 'intensity',
86
- type: 'slider',
87
- min: 0,
88
- max: 100,
89
- value: 50,
90
- label: 'Intensity',
91
- },
92
- {
93
- name: 'warmth',
94
- type: 'slider',
95
- min: 0,
96
- max: 100,
97
- value: 30,
98
- label: 'Warmth',
99
- },
59
+ { name: 'intensity', type: 'slider', min: 0, max: 100, value: 50, label: 'Intensity' },
60
+ { name: 'warmth', type: 'slider', min: 0, max: 100, value: 30, label: 'Warmth' },
100
61
  ],
101
62
  },
102
63
  {
103
64
  id: 'blur',
104
65
  name: 'Blur',
105
66
  category: 'artistic',
106
- parameters: [
107
- {
108
- name: 'radius',
109
- type: 'slider',
110
- min: 0,
111
- max: 20,
112
- value: 0,
113
- label: 'Blur Radius',
114
- },
115
- ],
67
+ parameters: [{ name: 'radius', type: 'slider', min: 0, max: 20, value: 0, label: 'Blur Radius' }],
116
68
  },
117
69
  ];
118
70
 
119
71
  static getPresets(category?: string): FilterPreset[] {
120
- if (category) {
121
- return this.PRESETS.filter(preset => preset.category === category);
122
- }
123
- return this.PRESETS;
72
+ return category ? this.PRESETS.filter(preset => preset.category === category) : this.PRESETS;
124
73
  }
125
74
 
126
75
  static getPreset(id: string): FilterPreset | undefined {
@@ -129,21 +78,12 @@ export class FilterProcessor {
129
78
 
130
79
  static createFilterState(presetId: string): FilterState {
131
80
  const preset = this.getPreset(presetId);
132
- if (!preset) {
133
- throw new Error(`Filter preset not found: ${presetId}`);
134
- }
81
+ if (!preset) throw new Error(`Filter preset not found: ${presetId}`);
135
82
 
136
83
  const parameters: Record<string, any> = {};
137
- preset.parameters.forEach(param => {
138
- parameters[param.name] = param.value;
139
- });
84
+ preset.parameters.forEach(param => { parameters[param.name] = param.value; });
140
85
 
141
- return {
142
- id: presetId,
143
- intensity: 100,
144
- parameters,
145
- enabled: true,
146
- };
86
+ return { id: presetId, intensity: 100, parameters, enabled: true };
147
87
  }
148
88
 
149
89
  static applyFilter(
@@ -153,187 +93,32 @@ export class FilterProcessor {
153
93
  filterState: FilterState
154
94
  ): Uint8ClampedArray {
155
95
  const preset = this.getPreset(filterState.id);
156
- if (!preset || !filterState.enabled) {
157
- return imageData;
158
- }
96
+ if (!preset || !filterState.enabled) return imageData;
159
97
 
160
98
  let processedData = new Uint8ClampedArray(imageData);
161
99
 
162
- // Apply filter based on preset type
163
100
  switch (filterState.id) {
164
101
  case 'brightness':
165
- processedData = this.applyBrightness(processedData, filterState.parameters.brightness);
102
+ processedData = ImageFilterUtils.applyBrightness(processedData, filterState.parameters.brightness) as any;
166
103
  break;
167
104
  case 'contrast':
168
- processedData = this.applyContrast(processedData, filterState.parameters.contrast);
105
+ processedData = ImageFilterUtils.applyContrast(processedData, filterState.parameters.contrast) as any;
169
106
  break;
170
107
  case 'saturation':
171
- processedData = this.applySaturation(processedData, filterState.parameters.saturation);
108
+ processedData = ImageFilterUtils.applySaturation(processedData, filterState.parameters.saturation) as any;
172
109
  break;
173
110
  case 'vintage':
174
- processedData = this.applyVintage(processedData, filterState.parameters.intensity, filterState.parameters.warmth);
111
+ processedData = ImageFilterUtils.applyVintage(processedData, filterState.parameters.intensity, filterState.parameters.warmth) as any;
175
112
  break;
176
113
  case 'blur':
177
- processedData = this.applyBlur(processedData, filterState.parameters.radius, width, height);
114
+ processedData = ImageFilterUtils.applyBlur(processedData, filterState.parameters.radius, width, height) as any;
178
115
  break;
179
116
  }
180
117
 
181
- // Apply intensity
182
118
  if (filterState.intensity < 100) {
183
- processedData = this.applyIntensity(imageData, processedData, filterState.intensity / 100);
184
- }
185
-
186
- return processedData;
187
- }
188
-
189
- private static applyBrightness(
190
- data: Uint8ClampedArray,
191
- brightness: number
192
- ): Uint8ClampedArray {
193
- const result = new Uint8ClampedArray(data);
194
- const adjustment = brightness * 2.55; // Convert -100 to 100 to -255 to 255
195
-
196
- for (let i = 0; i < result.length; i += 4) {
197
- result[i] = Math.min(255, Math.max(0, result[i] + adjustment));
198
- result[i + 1] = Math.min(255, Math.max(0, result[i + 1] + adjustment));
199
- result[i + 2] = Math.min(255, Math.max(0, result[i + 2] + adjustment));
119
+ processedData = ImageFilterUtils.applyIntensity(imageData, processedData, filterState.intensity / 100) as any;
200
120
  }
201
121
 
202
- return result;
122
+ return processedData as any;
203
123
  }
204
-
205
- private static applyContrast(
206
- data: Uint8ClampedArray,
207
- contrast: number
208
- ): Uint8ClampedArray {
209
- const result = new Uint8ClampedArray(data);
210
- const factor = (259 * (contrast * 255 + 255)) / (255 * (259 - contrast * 255));
211
-
212
- for (let i = 0; i < result.length; i += 4) {
213
- result[i] = Math.min(255, Math.max(0, factor * (result[i] - 128) + 128));
214
- result[i + 1] = Math.min(255, Math.max(0, factor * (result[i + 1] - 128) + 128));
215
- result[i + 2] = Math.min(255, Math.max(0, factor * (result[i + 2] - 128) + 128));
216
- }
217
-
218
- return result;
219
- }
220
-
221
- private static applySaturation(
222
- data: Uint8ClampedArray,
223
- saturation: number
224
- ): Uint8ClampedArray {
225
- const result = new Uint8ClampedArray(data);
226
- const adjustment = 1 + (saturation / 100);
227
-
228
- for (let i = 0; i < result.length; i += 4) {
229
- const r = result[i];
230
- const g = result[i + 1];
231
- const b = result[i + 2];
232
-
233
- const gray = 0.299 * r + 0.587 * g + 0.114 * b;
234
-
235
- result[i] = Math.min(255, Math.max(0, gray + adjustment * (r - gray)));
236
- result[i + 1] = Math.min(255, Math.max(0, gray + adjustment * (g - gray)));
237
- result[i + 2] = Math.min(255, Math.max(0, gray + adjustment * (b - gray)));
238
- }
239
-
240
- return result;
241
- }
242
-
243
- private static applyVintage(
244
- data: Uint8ClampedArray,
245
- intensity: number,
246
- warmth: number
247
- ): Uint8ClampedArray {
248
- const result = new Uint8ClampedArray(data);
249
- const factor = intensity / 100;
250
- const warmFactor = warmth / 100;
251
-
252
- for (let i = 0; i < result.length; i += 4) {
253
- let r = result[i];
254
- let g = result[i + 1];
255
- let b = result[i + 2];
256
-
257
- // Apply sepia effect
258
- const tr = 0.393 * r + 0.769 * g + 0.189 * b;
259
- const tg = 0.349 * r + 0.686 * g + 0.168 * b;
260
- const tb = 0.272 * r + 0.534 * g + 0.131 * b;
261
-
262
- // Mix with original based on intensity
263
- r = r * (1 - factor) + tr * factor;
264
- g = g * (1 - factor) + tg * factor;
265
- b = b * (1 - factor) + tb * factor;
266
-
267
- // Apply warmth
268
- if (warmFactor > 0) {
269
- result[i] = Math.min(255, r + warmFactor * 20);
270
- result[i + 1] = Math.min(255, g + warmFactor * 10);
271
- result[i + 2] = Math.min(255, b * (1 - warmFactor * 0.3));
272
- } else {
273
- result[i] = r;
274
- result[i + 1] = g;
275
- result[i + 2] = Math.min(255, b * (1 - Math.abs(warmFactor) * 0.3));
276
- }
277
- }
278
-
279
- return result;
280
- }
281
-
282
- private static applyBlur(
283
- data: Uint8ClampedArray,
284
- radius: number,
285
- width: number,
286
- height: number
287
- ): Uint8ClampedArray {
288
- // Simple box blur implementation
289
- const result = new Uint8ClampedArray(data);
290
- const size = Math.floor(radius) || 1;
291
-
292
- for (let y = 0; y < height; y++) {
293
- for (let x = 0; x < width; x++) {
294
- let r = 0, g = 0, b = 0, a = 0;
295
- let count = 0;
296
-
297
- for (let dy = -size; dy <= size; dy++) {
298
- for (let dx = -size; dx <= size; dx++) {
299
- const ny = y + dy;
300
- const nx = x + dx;
301
-
302
- if (ny >= 0 && ny < height && nx >= 0 && nx < width) {
303
- const idx = (ny * width + nx) * 4;
304
- r += data[idx];
305
- g += data[idx + 1];
306
- b += data[idx + 2];
307
- a += data[idx + 3];
308
- count++;
309
- }
310
- }
311
- }
312
-
313
- const idx = (y * width + x) * 4;
314
- result[idx] = r / count;
315
- result[idx + 1] = g / count;
316
- result[idx + 2] = b / count;
317
- result[idx + 3] = a / count;
318
- }
319
- }
320
-
321
- return result;
322
- }
323
-
324
- private static applyIntensity(
325
- originalData: Uint8ClampedArray,
326
- processedData: Uint8ClampedArray,
327
- intensity: number
328
- ): Uint8ClampedArray {
329
- const result = new Uint8ClampedArray(originalData.length);
330
-
331
- for (let i = 0; i < originalData.length; i++) {
332
- result[i] = originalData[i] * (1 - intensity) + processedData[i] * intensity;
333
- }
334
-
335
- return result;
336
- }
337
-
338
-
339
124
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Infrastructure - Editor History Utils
3
+ *
4
+ * Logic for managing editor state history and undo/redo
5
+ */
6
+
7
+ import { type EditorState, type EditorHistory } from '../../domain/entities/EditorTypes';
8
+
9
+ export class ImageEditorHistoryUtils {
10
+ static addToHistory(
11
+ state: EditorState,
12
+ newHistory: EditorHistory,
13
+ maxHistory: number = 50
14
+ ): EditorState {
15
+ const newHistoryArray = [...state.history.slice(0, state.historyIndex + 1), newHistory];
16
+
17
+ if (newHistoryArray.length > maxHistory) {
18
+ newHistoryArray.shift();
19
+ }
20
+
21
+ return {
22
+ ...state,
23
+ history: newHistoryArray,
24
+ historyIndex: newHistoryArray.length - 1,
25
+ };
26
+ }
27
+
28
+ static undo(state: EditorState): EditorState {
29
+ if (state.historyIndex <= 0) return state;
30
+
31
+ const newIndex = state.historyIndex - 1;
32
+ const historyState = state.history[newIndex];
33
+
34
+ return {
35
+ ...state,
36
+ layers: historyState.layers,
37
+ historyIndex: newIndex,
38
+ isDirty: true,
39
+ };
40
+ }
41
+
42
+ static redo(state: EditorState): EditorState {
43
+ if (state.historyIndex >= state.history.length - 1) return state;
44
+
45
+ const newIndex = state.historyIndex + 1;
46
+ const historyState = state.history[newIndex];
47
+
48
+ return {
49
+ ...state,
50
+ layers: historyState.layers,
51
+ historyIndex: newIndex,
52
+ isDirty: true,
53
+ };
54
+ }
55
+
56
+ static canUndo(state: EditorState): boolean {
57
+ return state.historyIndex > 0;
58
+ }
59
+
60
+ static canRedo(state: EditorState): boolean {
61
+ return state.historyIndex < state.history.length - 1;
62
+ }
63
+ }
@@ -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
+ }
@@ -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';