@umituz/react-native-image 1.1.5 → 1.2.1
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 +25 -10
- package/src/domain/entities/EditorTypes.ts +180 -0
- package/src/domain/entities/ImageFilterTypes.ts +70 -0
- package/src/index.ts +54 -0
- package/src/infrastructure/services/ImageAIEnhancementService.ts +136 -0
- package/src/infrastructure/services/ImageAnnotationService.ts +189 -0
- package/src/infrastructure/services/ImageBatchService.ts +199 -0
- package/src/infrastructure/services/ImageEditorService.ts +274 -0
- package/src/infrastructure/services/ImageFilterService.ts +168 -0
- package/src/infrastructure/services/ImageMetadataService.ts +187 -0
- package/src/infrastructure/services/ImageSpecializedEnhancementService.ts +57 -0
- package/src/infrastructure/utils/AIImageAnalysisUtils.ts +122 -0
- package/src/infrastructure/utils/CanvasRenderingService.ts +134 -0
- package/src/infrastructure/utils/CropTool.ts +260 -0
- package/src/infrastructure/utils/DrawingEngine.ts +210 -0
- package/src/infrastructure/utils/FilterEffects.ts +51 -0
- package/src/infrastructure/utils/FilterProcessor.ts +361 -0
- package/src/infrastructure/utils/ImageQualityPresets.ts +110 -0
- package/src/infrastructure/utils/LayerManager.ts +158 -0
- package/src/infrastructure/utils/ShapeRenderer.ts +168 -0
- package/src/infrastructure/utils/TextEditor.ts +273 -0
- package/src/presentation/components/CropComponent.tsx +183 -0
- package/src/presentation/components/Editor.tsx +261 -0
- package/src/presentation/components/EditorCanvas.tsx +135 -0
- package/src/presentation/components/EditorPanel.tsx +321 -0
- package/src/presentation/components/EditorToolbar.tsx +180 -0
- package/src/presentation/components/FilterSlider.tsx +123 -0
- package/src/presentation/components/GalleryHeader.tsx +87 -25
- package/src/presentation/components/ImageGallery.tsx +97 -48
- package/src/presentation/hooks/useEditorTools.ts +188 -0
- package/src/presentation/hooks/useImage.ts +33 -2
- package/src/presentation/hooks/useImageAIEnhancement.ts +33 -0
- package/src/presentation/hooks/useImageAnnotation.ts +32 -0
- package/src/presentation/hooks/useImageBatch.ts +33 -0
- package/src/presentation/hooks/useImageEditor.ts +165 -38
- package/src/presentation/hooks/useImageFilter.ts +38 -0
- package/src/presentation/hooks/useImageMetadata.ts +28 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure - Filter Processor
|
|
3
|
+
*
|
|
4
|
+
* Real-time filter processing with preview
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface FilterPreset {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
category: 'basic' | 'color' | 'artistic' | 'vintage';
|
|
11
|
+
parameters: FilterParameter[];
|
|
12
|
+
preview?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FilterParameter {
|
|
16
|
+
name: string;
|
|
17
|
+
type: 'slider' | 'color' | 'boolean';
|
|
18
|
+
min?: number;
|
|
19
|
+
max?: number;
|
|
20
|
+
value: number | string | boolean;
|
|
21
|
+
step?: number;
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FilterState {
|
|
26
|
+
id: string;
|
|
27
|
+
intensity: number;
|
|
28
|
+
parameters: Record<string, any>;
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class FilterProcessor {
|
|
33
|
+
private static readonly PRESETS: FilterPreset[] = [
|
|
34
|
+
{
|
|
35
|
+
id: 'brightness',
|
|
36
|
+
name: 'Brightness',
|
|
37
|
+
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
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'contrast',
|
|
51
|
+
name: 'Contrast',
|
|
52
|
+
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
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'saturation',
|
|
66
|
+
name: 'Saturation',
|
|
67
|
+
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
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'vintage',
|
|
81
|
+
name: 'Vintage',
|
|
82
|
+
category: 'vintage',
|
|
83
|
+
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
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'blur',
|
|
104
|
+
name: 'Blur',
|
|
105
|
+
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
|
+
],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
static getPresets(category?: string): FilterPreset[] {
|
|
120
|
+
if (category) {
|
|
121
|
+
return this.PRESETS.filter(preset => preset.category === category);
|
|
122
|
+
}
|
|
123
|
+
return this.PRESETS;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static getPreset(id: string): FilterPreset | undefined {
|
|
127
|
+
return this.PRESETS.find(preset => preset.id === id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static createFilterState(presetId: string): FilterState {
|
|
131
|
+
const preset = this.getPreset(presetId);
|
|
132
|
+
if (!preset) {
|
|
133
|
+
throw new Error(`Filter preset not found: ${presetId}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const parameters: Record<string, any> = {};
|
|
137
|
+
preset.parameters.forEach(param => {
|
|
138
|
+
parameters[param.name] = param.value;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
id: presetId,
|
|
143
|
+
intensity: 100,
|
|
144
|
+
parameters,
|
|
145
|
+
enabled: true,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static applyFilter(
|
|
150
|
+
imageData: ImageData,
|
|
151
|
+
filterState: FilterState
|
|
152
|
+
): ImageData {
|
|
153
|
+
const preset = this.getPreset(filterState.id);
|
|
154
|
+
if (!preset || !filterState.enabled) {
|
|
155
|
+
return imageData;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let processedData = new Uint8ClampedArray(imageData.data);
|
|
159
|
+
|
|
160
|
+
// Apply filter based on preset type
|
|
161
|
+
switch (filterState.id) {
|
|
162
|
+
case 'brightness':
|
|
163
|
+
processedData = this.applyBrightness(processedData, filterState.parameters.brightness) as any;
|
|
164
|
+
break;
|
|
165
|
+
case 'contrast':
|
|
166
|
+
processedData = this.applyContrast(processedData, filterState.parameters.contrast) as any;
|
|
167
|
+
break;
|
|
168
|
+
case 'saturation':
|
|
169
|
+
processedData = this.applySaturation(processedData, filterState.parameters.saturation) as any;
|
|
170
|
+
break;
|
|
171
|
+
case 'vintage':
|
|
172
|
+
processedData = this.applyVintage(processedData, filterState.parameters.intensity, filterState.parameters.warmth) as any;
|
|
173
|
+
break;
|
|
174
|
+
case 'blur':
|
|
175
|
+
processedData = this.applyBlur(processedData, filterState.parameters.radius) as any;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Apply intensity
|
|
180
|
+
if (filterState.intensity < 100) {
|
|
181
|
+
processedData = this.applyIntensity(imageData.data, processedData, filterState.intensity / 100) as any;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
...imageData,
|
|
186
|
+
data: processedData,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private static applyBrightness(
|
|
191
|
+
data: Uint8ClampedArray,
|
|
192
|
+
brightness: number
|
|
193
|
+
): Uint8ClampedArray {
|
|
194
|
+
const result = new Uint8ClampedArray(data);
|
|
195
|
+
const adjustment = brightness * 2.55; // Convert -100 to 100 to -255 to 255
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < result.length; i += 4) {
|
|
198
|
+
result[i] = Math.min(255, Math.max(0, result[i] + adjustment));
|
|
199
|
+
result[i + 1] = Math.min(255, Math.max(0, result[i + 1] + adjustment));
|
|
200
|
+
result[i + 2] = Math.min(255, Math.max(0, result[i + 2] + adjustment));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private static applyContrast(
|
|
207
|
+
data: Uint8ClampedArray,
|
|
208
|
+
contrast: number
|
|
209
|
+
): Uint8ClampedArray {
|
|
210
|
+
const result = new Uint8ClampedArray(data);
|
|
211
|
+
const factor = (259 * (contrast * 255 + 255)) / (255 * (259 - contrast * 255));
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < result.length; i += 4) {
|
|
214
|
+
result[i] = Math.min(255, Math.max(0, factor * (result[i] - 128) + 128));
|
|
215
|
+
result[i + 1] = Math.min(255, Math.max(0, factor * (result[i + 1] - 128) + 128));
|
|
216
|
+
result[i + 2] = Math.min(255, Math.max(0, factor * (result[i + 2] - 128) + 128));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private static applySaturation(
|
|
223
|
+
data: Uint8ClampedArray,
|
|
224
|
+
saturation: number
|
|
225
|
+
): Uint8ClampedArray {
|
|
226
|
+
const result = new Uint8ClampedArray(data);
|
|
227
|
+
const adjustment = 1 + (saturation / 100);
|
|
228
|
+
|
|
229
|
+
for (let i = 0; i < result.length; i += 4) {
|
|
230
|
+
const r = result[i];
|
|
231
|
+
const g = result[i + 1];
|
|
232
|
+
const b = result[i + 2];
|
|
233
|
+
|
|
234
|
+
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
235
|
+
|
|
236
|
+
result[i] = Math.min(255, Math.max(0, gray + adjustment * (r - gray)));
|
|
237
|
+
result[i + 1] = Math.min(255, Math.max(0, gray + adjustment * (g - gray)));
|
|
238
|
+
result[i + 2] = Math.min(255, Math.max(0, gray + adjustment * (b - gray)));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private static applyVintage(
|
|
245
|
+
data: Uint8ClampedArray,
|
|
246
|
+
intensity: number,
|
|
247
|
+
warmth: number
|
|
248
|
+
): Uint8ClampedArray {
|
|
249
|
+
const result = new Uint8ClampedArray(data);
|
|
250
|
+
const factor = intensity / 100;
|
|
251
|
+
const warmFactor = warmth / 100;
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < result.length; i += 4) {
|
|
254
|
+
let r = result[i];
|
|
255
|
+
let g = result[i + 1];
|
|
256
|
+
let b = result[i + 2];
|
|
257
|
+
|
|
258
|
+
// Apply sepia effect
|
|
259
|
+
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
|
|
260
|
+
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
|
|
261
|
+
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
|
|
262
|
+
|
|
263
|
+
// Mix with original based on intensity
|
|
264
|
+
r = r * (1 - factor) + tr * factor;
|
|
265
|
+
g = g * (1 - factor) + tg * factor;
|
|
266
|
+
b = b * (1 - factor) + tb * factor;
|
|
267
|
+
|
|
268
|
+
// Apply warmth
|
|
269
|
+
if (warmFactor > 0) {
|
|
270
|
+
result[i] = Math.min(255, r + warmFactor * 20);
|
|
271
|
+
result[i + 1] = Math.min(255, g + warmFactor * 10);
|
|
272
|
+
result[i + 2] = Math.min(255, b * (1 - warmFactor * 0.3));
|
|
273
|
+
} else {
|
|
274
|
+
result[i] = r;
|
|
275
|
+
result[i + 1] = g;
|
|
276
|
+
result[i + 2] = Math.min(255, b * (1 - Math.abs(warmFactor) * 0.3));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private static applyBlur(
|
|
284
|
+
data: Uint8ClampedArray,
|
|
285
|
+
radius: number
|
|
286
|
+
): Uint8ClampedArray {
|
|
287
|
+
// Simple box blur implementation
|
|
288
|
+
const result = new Uint8ClampedArray(data);
|
|
289
|
+
const width = Math.sqrt(data.length / 4);
|
|
290
|
+
const height = width;
|
|
291
|
+
const size = Math.floor(radius) || 1;
|
|
292
|
+
|
|
293
|
+
for (let y = 0; y < height; y++) {
|
|
294
|
+
for (let x = 0; x < width; x++) {
|
|
295
|
+
let r = 0, g = 0, b = 0, a = 0;
|
|
296
|
+
let count = 0;
|
|
297
|
+
|
|
298
|
+
for (let dy = -size; dy <= size; dy++) {
|
|
299
|
+
for (let dx = -size; dx <= size; dx++) {
|
|
300
|
+
const ny = y + dy;
|
|
301
|
+
const nx = x + dx;
|
|
302
|
+
|
|
303
|
+
if (ny >= 0 && ny < height && nx >= 0 && nx < width) {
|
|
304
|
+
const idx = (ny * width + nx) * 4;
|
|
305
|
+
r += data[idx];
|
|
306
|
+
g += data[idx + 1];
|
|
307
|
+
b += data[idx + 2];
|
|
308
|
+
a += data[idx + 3];
|
|
309
|
+
count++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const idx = (y * width + x) * 4;
|
|
315
|
+
result[idx] = r / count;
|
|
316
|
+
result[idx + 1] = g / count;
|
|
317
|
+
result[idx + 2] = b / count;
|
|
318
|
+
result[idx + 3] = a / count;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private static applyIntensity(
|
|
326
|
+
originalData: Uint8ClampedArray,
|
|
327
|
+
processedData: Uint8ClampedArray,
|
|
328
|
+
intensity: number
|
|
329
|
+
): Uint8ClampedArray {
|
|
330
|
+
const result = new Uint8ClampedArray(originalData.length);
|
|
331
|
+
|
|
332
|
+
for (let i = 0; i < originalData.length; i++) {
|
|
333
|
+
result[i] = originalData[i] * (1 - intensity) + processedData[i] * intensity;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
static createPreview(
|
|
340
|
+
imageData: ImageData,
|
|
341
|
+
filterState: FilterState,
|
|
342
|
+
previewSize: { width: number; height: number }
|
|
343
|
+
): ImageData {
|
|
344
|
+
// Create a smaller preview version
|
|
345
|
+
const previewCanvas = document.createElement('canvas') || {} as any;
|
|
346
|
+
previewCanvas.width = previewSize.width;
|
|
347
|
+
previewCanvas.height = previewSize.height;
|
|
348
|
+
const ctx = previewCanvas.getContext('2d');
|
|
349
|
+
|
|
350
|
+
if (!ctx) return imageData;
|
|
351
|
+
|
|
352
|
+
// Scale down the image for preview
|
|
353
|
+
ctx.drawImage(
|
|
354
|
+
{} as HTMLImageElement, // Would be the actual image
|
|
355
|
+
0, 0, previewSize.width, previewSize.height
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const previewImageData = ctx.getImageData(0, 0, previewSize.width, previewSize.height);
|
|
359
|
+
return this.applyFilter(previewImageData, filterState);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Quality Presets
|
|
3
|
+
*
|
|
4
|
+
* Predefined quality settings for different use cases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SaveFormat } from '../../domain/entities/ImageTypes';
|
|
8
|
+
import { IMAGE_CONSTANTS } from '../../domain/entities/ImageConstants';
|
|
9
|
+
|
|
10
|
+
export interface QualityPreset {
|
|
11
|
+
format: SaveFormat;
|
|
12
|
+
quality: number;
|
|
13
|
+
maxWidth?: number;
|
|
14
|
+
maxHeight?: number;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QualityPresets {
|
|
19
|
+
web: QualityPreset;
|
|
20
|
+
mobile: QualityPreset;
|
|
21
|
+
print: QualityPreset;
|
|
22
|
+
thumbnail: QualityPreset;
|
|
23
|
+
preview: QualityPreset;
|
|
24
|
+
archive: QualityPreset;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const IMAGE_QUALITY_PRESETS: QualityPresets = {
|
|
28
|
+
web: {
|
|
29
|
+
format: 'jpeg',
|
|
30
|
+
quality: 0.8,
|
|
31
|
+
maxWidth: 1920,
|
|
32
|
+
maxHeight: 1080,
|
|
33
|
+
description: 'Optimized for web use with good balance of quality and size',
|
|
34
|
+
},
|
|
35
|
+
mobile: {
|
|
36
|
+
format: 'jpeg',
|
|
37
|
+
quality: 0.7,
|
|
38
|
+
maxWidth: 1080,
|
|
39
|
+
maxHeight: 1920,
|
|
40
|
+
description: 'Optimized for mobile devices with smaller file size',
|
|
41
|
+
},
|
|
42
|
+
print: {
|
|
43
|
+
format: 'png',
|
|
44
|
+
quality: 1.0,
|
|
45
|
+
maxWidth: 3000,
|
|
46
|
+
maxHeight: 3000,
|
|
47
|
+
description: 'High quality for printing with maximum detail',
|
|
48
|
+
},
|
|
49
|
+
thumbnail: {
|
|
50
|
+
format: 'jpeg',
|
|
51
|
+
quality: 0.6,
|
|
52
|
+
maxWidth: IMAGE_CONSTANTS.thumbnailSize,
|
|
53
|
+
maxHeight: IMAGE_CONSTANTS.thumbnailSize,
|
|
54
|
+
description: 'Small thumbnail for preview use',
|
|
55
|
+
},
|
|
56
|
+
preview: {
|
|
57
|
+
format: 'jpeg',
|
|
58
|
+
quality: 0.5,
|
|
59
|
+
maxWidth: 800,
|
|
60
|
+
maxHeight: 600,
|
|
61
|
+
description: 'Quick preview with very small file size',
|
|
62
|
+
},
|
|
63
|
+
archive: {
|
|
64
|
+
format: 'png',
|
|
65
|
+
quality: 0.9,
|
|
66
|
+
description: 'High quality archival storage with lossless compression',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export class ImageQualityPresetService {
|
|
71
|
+
static getPreset(name: keyof QualityPresets): QualityPreset {
|
|
72
|
+
return IMAGE_QUALITY_PRESETS[name];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static getAllPresets(): QualityPresets {
|
|
76
|
+
return IMAGE_QUALITY_PRESETS;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static getCustomPreset(options: {
|
|
80
|
+
format?: SaveFormat;
|
|
81
|
+
quality?: number;
|
|
82
|
+
maxWidth?: number;
|
|
83
|
+
maxHeight?: number;
|
|
84
|
+
}): QualityPreset {
|
|
85
|
+
return {
|
|
86
|
+
format: options.format || 'jpeg',
|
|
87
|
+
quality: options.quality || IMAGE_CONSTANTS.defaultQuality,
|
|
88
|
+
maxWidth: options.maxWidth,
|
|
89
|
+
maxHeight: options.maxHeight,
|
|
90
|
+
description: 'Custom quality preset',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static optimizeForUseCase(
|
|
95
|
+
useCase: 'web' | 'mobile' | 'print' | 'thumbnail' | 'preview' | 'archive',
|
|
96
|
+
customOptions?: Partial<QualityPreset>
|
|
97
|
+
): QualityPreset {
|
|
98
|
+
const preset = IMAGE_QUALITY_PRESETS[useCase];
|
|
99
|
+
|
|
100
|
+
if (!customOptions) {
|
|
101
|
+
return preset;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
...preset,
|
|
106
|
+
...customOptions,
|
|
107
|
+
description: `${preset.description} (modified)`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure - Layer Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages editor layers with composition and rendering
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type LayerOperation = 'add' | 'remove' | 'move' | 'merge' | 'duplicate';
|
|
8
|
+
|
|
9
|
+
export interface LayerComposition {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
backgroundColor?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
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
|
+
|
|
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
|
+
|
|
55
|
+
static mergeLayers(
|
|
56
|
+
layers: Array<{
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
elements: any[];
|
|
60
|
+
}>,
|
|
61
|
+
targetIds: string[]
|
|
62
|
+
): Array<{
|
|
63
|
+
id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
elements: any[];
|
|
66
|
+
}> {
|
|
67
|
+
const targetLayers = layers.filter(layer => targetIds.includes(layer.id));
|
|
68
|
+
const otherLayers = layers.filter(layer => !targetIds.includes(layer.id));
|
|
69
|
+
|
|
70
|
+
if (targetLayers.length === 0) return layers;
|
|
71
|
+
|
|
72
|
+
// Merge elements from target layers
|
|
73
|
+
const mergedElements = targetLayers.flatMap(layer => layer.elements);
|
|
74
|
+
const mergedLayer = {
|
|
75
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
76
|
+
name: targetLayers.map(l => l.name).join(' + '),
|
|
77
|
+
elements: mergedElements,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return [...otherLayers, mergedLayer];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static duplicateLayer(
|
|
84
|
+
layer: {
|
|
85
|
+
id: string;
|
|
86
|
+
name: string;
|
|
87
|
+
elements: any[];
|
|
88
|
+
}
|
|
89
|
+
): {
|
|
90
|
+
id: string;
|
|
91
|
+
name: string;
|
|
92
|
+
elements: any[];
|
|
93
|
+
} {
|
|
94
|
+
return {
|
|
95
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
96
|
+
name: `${layer.name} Copy`,
|
|
97
|
+
elements: [...layer.elements],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static reorderLayers(
|
|
102
|
+
layers: Array<{ id: string; index?: number }>,
|
|
103
|
+
fromIndex: number,
|
|
104
|
+
toIndex: number
|
|
105
|
+
): Array<{ id: string; index?: number }> {
|
|
106
|
+
const result = [...layers];
|
|
107
|
+
const [moved] = result.splice(fromIndex, 1);
|
|
108
|
+
result.splice(toIndex, 0, moved);
|
|
109
|
+
|
|
110
|
+
return result.map((layer, index) => ({ ...layer, index }));
|
|
111
|
+
}
|
|
112
|
+
|
|
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
|
+
|
|
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
|
+
}
|