@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.
- package/package.json +9 -3
- package/src/index.ts +3 -0
- package/src/infrastructure/services/ImageConversionService.ts +7 -32
- package/src/infrastructure/services/ImageEditorService.ts +41 -179
- package/src/infrastructure/services/ImageTransformService.ts +28 -168
- package/src/infrastructure/utils/FilterProcessor.ts +21 -236
- package/src/infrastructure/utils/ImageEditorHistoryUtils.ts +63 -0
- package/src/infrastructure/utils/ImageFilterUtils.ts +152 -0
- package/src/infrastructure/utils/ImageTransformUtils.ts +25 -0
- package/src/presentation/components/editor/FilterPickerSheet.tsx +75 -0
- package/src/presentation/components/editor/StickerPickerSheet.tsx +62 -0
- package/src/presentation/components/editor/TextEditorSheet.tsx +98 -0
- package/src/presentation/components/editor/TextEditorTabs.tsx +111 -0
- package/src/presentation/hooks/useImage.ts +5 -8
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Infrastructure - Filter Processor
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
102
|
+
processedData = ImageFilterUtils.applyBrightness(processedData, filterState.parameters.brightness) as any;
|
|
166
103
|
break;
|
|
167
104
|
case 'contrast':
|
|
168
|
-
processedData =
|
|
105
|
+
processedData = ImageFilterUtils.applyContrast(processedData, filterState.parameters.contrast) as any;
|
|
169
106
|
break;
|
|
170
107
|
case 'saturation':
|
|
171
|
-
processedData =
|
|
108
|
+
processedData = ImageFilterUtils.applySaturation(processedData, filterState.parameters.saturation) as any;
|
|
172
109
|
break;
|
|
173
110
|
case 'vintage':
|
|
174
|
-
processedData =
|
|
111
|
+
processedData = ImageFilterUtils.applyVintage(processedData, filterState.parameters.intensity, filterState.parameters.warmth) as any;
|
|
175
112
|
break;
|
|
176
113
|
case 'blur':
|
|
177
|
-
processedData =
|
|
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 =
|
|
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
|
|
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';
|