@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,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure - Shape Renderer
|
|
3
|
+
*
|
|
4
|
+
* Advanced shape drawing with different styles
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ShapeStyle {
|
|
8
|
+
fill?: string;
|
|
9
|
+
stroke?: string;
|
|
10
|
+
strokeWidth?: number;
|
|
11
|
+
dash?: number[];
|
|
12
|
+
opacity?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ShapeRenderer {
|
|
16
|
+
static drawRoundedRect(
|
|
17
|
+
ctx: CanvasRenderingContext2D,
|
|
18
|
+
x: number,
|
|
19
|
+
y: number,
|
|
20
|
+
width: number,
|
|
21
|
+
height: number,
|
|
22
|
+
radius: number,
|
|
23
|
+
style: ShapeStyle
|
|
24
|
+
): void {
|
|
25
|
+
ctx.save();
|
|
26
|
+
|
|
27
|
+
ctx.globalAlpha = style.opacity || 1;
|
|
28
|
+
ctx.strokeStyle = style.stroke || '#000000';
|
|
29
|
+
ctx.fillStyle = style.fill || 'transparent';
|
|
30
|
+
ctx.lineWidth = style.strokeWidth || 2;
|
|
31
|
+
|
|
32
|
+
if (style.dash) {
|
|
33
|
+
ctx.setLineDash(style.dash);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ctx.beginPath();
|
|
37
|
+
ctx.moveTo(x + radius, y);
|
|
38
|
+
ctx.lineTo(x + width - radius, y);
|
|
39
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
40
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
41
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
42
|
+
ctx.lineTo(x + radius, y + height);
|
|
43
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
44
|
+
ctx.lineTo(x, y + radius);
|
|
45
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
46
|
+
ctx.closePath();
|
|
47
|
+
|
|
48
|
+
if (style.fill) ctx.fill();
|
|
49
|
+
if (style.stroke) ctx.stroke();
|
|
50
|
+
|
|
51
|
+
ctx.restore();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static drawStar(
|
|
55
|
+
ctx: CanvasRenderingContext2D,
|
|
56
|
+
cx: number,
|
|
57
|
+
cy: number,
|
|
58
|
+
outerRadius: number,
|
|
59
|
+
innerRadius: number,
|
|
60
|
+
points: number,
|
|
61
|
+
style: ShapeStyle
|
|
62
|
+
): void {
|
|
63
|
+
ctx.save();
|
|
64
|
+
|
|
65
|
+
ctx.globalAlpha = style.opacity || 1;
|
|
66
|
+
ctx.strokeStyle = style.stroke || '#000000';
|
|
67
|
+
ctx.fillStyle = style.fill || 'transparent';
|
|
68
|
+
ctx.lineWidth = style.strokeWidth || 2;
|
|
69
|
+
|
|
70
|
+
ctx.beginPath();
|
|
71
|
+
for (let i = 0; i < points * 2; i++) {
|
|
72
|
+
const angle = (Math.PI * i) / points - Math.PI / 2;
|
|
73
|
+
const radius = i % 2 === 0 ? outerRadius : innerRadius;
|
|
74
|
+
const x = cx + Math.cos(angle) * radius;
|
|
75
|
+
const y = cy + Math.sin(angle) * radius;
|
|
76
|
+
|
|
77
|
+
if (i === 0) {
|
|
78
|
+
ctx.moveTo(x, y);
|
|
79
|
+
} else {
|
|
80
|
+
ctx.lineTo(x, y);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
ctx.closePath();
|
|
84
|
+
|
|
85
|
+
if (style.fill) ctx.fill();
|
|
86
|
+
if (style.stroke) ctx.stroke();
|
|
87
|
+
|
|
88
|
+
ctx.restore();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static drawHeart(
|
|
92
|
+
ctx: CanvasRenderingContext2D,
|
|
93
|
+
x: number,
|
|
94
|
+
y: number,
|
|
95
|
+
size: number,
|
|
96
|
+
style: ShapeStyle
|
|
97
|
+
): void {
|
|
98
|
+
ctx.save();
|
|
99
|
+
|
|
100
|
+
ctx.globalAlpha = style.opacity || 1;
|
|
101
|
+
ctx.strokeStyle = style.stroke || '#000000';
|
|
102
|
+
ctx.fillStyle = style.fill || 'transparent';
|
|
103
|
+
ctx.lineWidth = style.strokeWidth || 2;
|
|
104
|
+
|
|
105
|
+
ctx.beginPath();
|
|
106
|
+
const topCurveHeight = size * 0.3;
|
|
107
|
+
ctx.moveTo(x, y + topCurveHeight);
|
|
108
|
+
|
|
109
|
+
// Top left curve
|
|
110
|
+
ctx.bezierCurveTo(
|
|
111
|
+
x, y,
|
|
112
|
+
x - size / 2, y,
|
|
113
|
+
x - size / 2, y + topCurveHeight
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Bottom left curve
|
|
117
|
+
ctx.bezierCurveTo(
|
|
118
|
+
x - size / 2, y + (size + topCurveHeight) / 2,
|
|
119
|
+
x, y + (size + topCurveHeight) / 1.5,
|
|
120
|
+
x, y + size
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Bottom right curve
|
|
124
|
+
ctx.bezierCurveTo(
|
|
125
|
+
x, y + (size + topCurveHeight) / 1.5,
|
|
126
|
+
x + size / 2, y + (size + topCurveHeight) / 2,
|
|
127
|
+
x + size / 2, y + topCurveHeight
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Top right curve
|
|
131
|
+
ctx.bezierCurveTo(
|
|
132
|
+
x + size / 2, y,
|
|
133
|
+
x, y,
|
|
134
|
+
x, y + topCurveHeight
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
ctx.closePath();
|
|
138
|
+
|
|
139
|
+
if (style.fill) ctx.fill();
|
|
140
|
+
if (style.stroke) ctx.stroke();
|
|
141
|
+
|
|
142
|
+
ctx.restore();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static drawTriangle(
|
|
146
|
+
ctx: CanvasRenderingContext2D,
|
|
147
|
+
points: Array<{ x: number; y: number }>,
|
|
148
|
+
style: ShapeStyle
|
|
149
|
+
): void {
|
|
150
|
+
ctx.save();
|
|
151
|
+
|
|
152
|
+
ctx.globalAlpha = style.opacity || 1;
|
|
153
|
+
ctx.strokeStyle = style.stroke || '#000000';
|
|
154
|
+
ctx.fillStyle = style.fill || 'transparent';
|
|
155
|
+
ctx.lineWidth = style.strokeWidth || 2;
|
|
156
|
+
|
|
157
|
+
ctx.beginPath();
|
|
158
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
159
|
+
ctx.lineTo(points[1].x, points[1].y);
|
|
160
|
+
ctx.lineTo(points[2].x, points[2].y);
|
|
161
|
+
ctx.closePath();
|
|
162
|
+
|
|
163
|
+
if (style.fill) ctx.fill();
|
|
164
|
+
if (style.stroke) ctx.stroke();
|
|
165
|
+
|
|
166
|
+
ctx.restore();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure - Text Editor
|
|
3
|
+
*
|
|
4
|
+
* Advanced text editing with rich formatting
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface TextFormatting {
|
|
8
|
+
fontSize: number;
|
|
9
|
+
fontFamily: string;
|
|
10
|
+
fontWeight: 'normal' | 'bold';
|
|
11
|
+
fontStyle: 'normal' | 'italic';
|
|
12
|
+
textAlign: 'left' | 'center' | 'right';
|
|
13
|
+
color: string;
|
|
14
|
+
backgroundColor?: string;
|
|
15
|
+
opacity: number;
|
|
16
|
+
lineHeight?: number;
|
|
17
|
+
letterSpacing?: number;
|
|
18
|
+
textShadow?: {
|
|
19
|
+
color: string;
|
|
20
|
+
blur: number;
|
|
21
|
+
offsetX: number;
|
|
22
|
+
offsetY: number;
|
|
23
|
+
};
|
|
24
|
+
textStroke?: {
|
|
25
|
+
color: string;
|
|
26
|
+
width: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TextMeasurement {
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
baseline: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class TextEditor {
|
|
37
|
+
private static systemFonts = [
|
|
38
|
+
'Arial',
|
|
39
|
+
'Helvetica',
|
|
40
|
+
'Times New Roman',
|
|
41
|
+
'Georgia',
|
|
42
|
+
'Courier New',
|
|
43
|
+
'Verdana',
|
|
44
|
+
'Comic Sans MS',
|
|
45
|
+
'Impact',
|
|
46
|
+
'Palatino',
|
|
47
|
+
'Garamond',
|
|
48
|
+
'Bookman',
|
|
49
|
+
'Trebuchet MS',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
private static getFontFamily(fontFamily: string): string {
|
|
53
|
+
// Check if font is available, fallback to system font
|
|
54
|
+
return this.systemFonts.includes(fontFamily) ? fontFamily : 'Arial';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static measureText(
|
|
58
|
+
text: string,
|
|
59
|
+
formatting: TextFormatting,
|
|
60
|
+
ctx: CanvasRenderingContext2D
|
|
61
|
+
): TextMeasurement {
|
|
62
|
+
const {
|
|
63
|
+
fontSize,
|
|
64
|
+
fontFamily,
|
|
65
|
+
fontWeight,
|
|
66
|
+
fontStyle,
|
|
67
|
+
lineHeight = 1.2,
|
|
68
|
+
} = formatting;
|
|
69
|
+
|
|
70
|
+
// Build font string
|
|
71
|
+
const fontString = `${fontStyle} ${fontWeight} ${fontSize}px ${this.getFontFamily(fontFamily)}`;
|
|
72
|
+
ctx.font = fontString;
|
|
73
|
+
|
|
74
|
+
// Measure text
|
|
75
|
+
const metrics = ctx.measureText(text);
|
|
76
|
+
const width = metrics.width;
|
|
77
|
+
const height = fontSize * lineHeight;
|
|
78
|
+
const baseline = fontSize * 0.8;
|
|
79
|
+
|
|
80
|
+
return { width, height, baseline };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static renderText(
|
|
84
|
+
ctx: CanvasRenderingContext2D,
|
|
85
|
+
text: string,
|
|
86
|
+
position: { x: number; y: number },
|
|
87
|
+
formatting: TextFormatting,
|
|
88
|
+
maxWidth?: number
|
|
89
|
+
): TextMeasurement {
|
|
90
|
+
const measurement = this.measureText(text, formatting, ctx);
|
|
91
|
+
|
|
92
|
+
ctx.save();
|
|
93
|
+
|
|
94
|
+
// Set text properties
|
|
95
|
+
const fontString = `${formatting.fontStyle} ${formatting.fontWeight} ${formatting.fontSize}px ${this.getFontFamily(formatting.fontFamily)}`;
|
|
96
|
+
ctx.font = fontString;
|
|
97
|
+
ctx.fillStyle = formatting.color;
|
|
98
|
+
ctx.globalAlpha = formatting.opacity;
|
|
99
|
+
ctx.textAlign = formatting.textAlign;
|
|
100
|
+
ctx.textBaseline = 'alphabetic';
|
|
101
|
+
|
|
102
|
+
// Apply text shadow
|
|
103
|
+
if (formatting.textShadow) {
|
|
104
|
+
ctx.shadowColor = formatting.textShadow.color;
|
|
105
|
+
ctx.shadowBlur = formatting.textShadow.blur;
|
|
106
|
+
ctx.shadowOffsetX = formatting.textShadow.offsetX;
|
|
107
|
+
ctx.shadowOffsetY = formatting.textShadow.offsetY;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Draw background if specified
|
|
111
|
+
if (formatting.backgroundColor) {
|
|
112
|
+
const padding = 4;
|
|
113
|
+
let x = position.x;
|
|
114
|
+
let y = position.y - measurement.baseline - padding;
|
|
115
|
+
|
|
116
|
+
if (formatting.textAlign === 'center') {
|
|
117
|
+
x -= measurement.width / 2;
|
|
118
|
+
} else if (formatting.textAlign === 'right') {
|
|
119
|
+
x -= measurement.width;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ctx.fillStyle = formatting.backgroundColor;
|
|
123
|
+
ctx.fillRect(
|
|
124
|
+
x - padding,
|
|
125
|
+
y - padding,
|
|
126
|
+
measurement.width + padding * 2,
|
|
127
|
+
measurement.height + padding * 2
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Reset fill style for text
|
|
131
|
+
ctx.fillStyle = formatting.color;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Draw text
|
|
135
|
+
let drawX = position.x;
|
|
136
|
+
if (formatting.textAlign === 'center') {
|
|
137
|
+
drawX = position.x;
|
|
138
|
+
} else if (formatting.textAlign === 'right') {
|
|
139
|
+
drawX = position.x;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (maxWidth && measurement.width > maxWidth) {
|
|
143
|
+
// Text wrapping
|
|
144
|
+
const words = text.split(' ');
|
|
145
|
+
let currentLine = '';
|
|
146
|
+
let currentY = position.y;
|
|
147
|
+
|
|
148
|
+
for (const word of words) {
|
|
149
|
+
const testLine = currentLine + (currentLine ? ' ' : '') + word;
|
|
150
|
+
const testMetrics = this.measureText(testLine, formatting, ctx);
|
|
151
|
+
|
|
152
|
+
if (testMetrics.width > maxWidth && currentLine) {
|
|
153
|
+
ctx.fillText(currentLine, drawX, currentY);
|
|
154
|
+
currentLine = word;
|
|
155
|
+
currentY += measurement.height;
|
|
156
|
+
} else {
|
|
157
|
+
currentLine = testLine;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
ctx.fillText(currentLine, drawX, currentY);
|
|
161
|
+
} else {
|
|
162
|
+
ctx.fillText(text, drawX, position.y);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Apply text stroke if specified
|
|
166
|
+
if (formatting.textStroke) {
|
|
167
|
+
ctx.strokeStyle = formatting.textStroke.color;
|
|
168
|
+
ctx.lineWidth = formatting.textStroke.width;
|
|
169
|
+
ctx.strokeText(text, drawX, position.y);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
ctx.restore();
|
|
173
|
+
|
|
174
|
+
return measurement;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static wrapText(
|
|
178
|
+
text: string,
|
|
179
|
+
maxWidth: number,
|
|
180
|
+
formatting: TextFormatting,
|
|
181
|
+
ctx: CanvasRenderingContext2D
|
|
182
|
+
): string[] {
|
|
183
|
+
const words = text.split(' ');
|
|
184
|
+
const lines: string[] = [];
|
|
185
|
+
let currentLine = '';
|
|
186
|
+
|
|
187
|
+
for (const word of words) {
|
|
188
|
+
const testLine = currentLine + (currentLine ? ' ' : '') + word;
|
|
189
|
+
const testMetrics = this.measureText(testLine, formatting, ctx);
|
|
190
|
+
|
|
191
|
+
if (testMetrics.width > maxWidth && currentLine) {
|
|
192
|
+
lines.push(currentLine);
|
|
193
|
+
currentLine = word;
|
|
194
|
+
} else {
|
|
195
|
+
currentLine = testLine;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (currentLine) {
|
|
200
|
+
lines.push(currentLine);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return lines;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
static getAvailableFonts(): string[] {
|
|
207
|
+
return [...this.systemFonts];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static calculateTextBounds(
|
|
211
|
+
text: string,
|
|
212
|
+
formatting: TextFormatting,
|
|
213
|
+
ctx: CanvasRenderingContext2D,
|
|
214
|
+
maxWidth?: number
|
|
215
|
+
): {
|
|
216
|
+
x: number;
|
|
217
|
+
y: number;
|
|
218
|
+
width: number;
|
|
219
|
+
height: number;
|
|
220
|
+
} {
|
|
221
|
+
const measurement = this.measureText(text, formatting, ctx);
|
|
222
|
+
|
|
223
|
+
if (maxWidth && measurement.width > maxWidth) {
|
|
224
|
+
const lines = this.wrapText(text, maxWidth, formatting, ctx);
|
|
225
|
+
const lineHeight = measurement.height;
|
|
226
|
+
const totalHeight = lines.length * lineHeight;
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
x: 0,
|
|
230
|
+
y: 0,
|
|
231
|
+
width: maxWidth,
|
|
232
|
+
height: totalHeight,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
x: 0,
|
|
238
|
+
y: 0,
|
|
239
|
+
width: measurement.width,
|
|
240
|
+
height: measurement.height,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
static createTextPath(
|
|
245
|
+
text: string,
|
|
246
|
+
position: { x: number; y: number },
|
|
247
|
+
formatting: TextFormatting,
|
|
248
|
+
ctx: CanvasRenderingContext2D
|
|
249
|
+
): Path2D | any {
|
|
250
|
+
const measurement = this.measureText(text, formatting, ctx);
|
|
251
|
+
|
|
252
|
+
ctx.save();
|
|
253
|
+
const fontString = `${formatting.fontStyle} ${formatting.fontWeight} ${formatting.fontSize}px ${this.getFontFamily(formatting.fontFamily)}`;
|
|
254
|
+
ctx.font = fontString;
|
|
255
|
+
|
|
256
|
+
let x = position.x;
|
|
257
|
+
if (formatting.textAlign === 'center') {
|
|
258
|
+
x = position.x;
|
|
259
|
+
} else if (formatting.textAlign === 'right') {
|
|
260
|
+
x = position.x;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const path = new Path2D();
|
|
264
|
+
path.moveTo(x, position.y);
|
|
265
|
+
|
|
266
|
+
// This is a simplified version - in a real implementation,
|
|
267
|
+
// you would use more sophisticated text-to-path conversion
|
|
268
|
+
path.rect(x - measurement.width/2, position.y - measurement.baseline, measurement.width, measurement.height);
|
|
269
|
+
|
|
270
|
+
ctx.restore();
|
|
271
|
+
return path;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation - Crop Component
|
|
3
|
+
*
|
|
4
|
+
* Advanced cropping interface with aspect ratios and grid
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useCallback } from 'react';
|
|
8
|
+
import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
|
|
9
|
+
import { CropTool, type CropConfig, type CropArea } from '../../infrastructure/utils/CropTool';
|
|
10
|
+
|
|
11
|
+
interface CropComponentProps {
|
|
12
|
+
imageWidth: number;
|
|
13
|
+
imageHeight: number;
|
|
14
|
+
onCropChange?: (area: CropArea) => void;
|
|
15
|
+
onCropComplete?: (area: CropArea) => void;
|
|
16
|
+
initialArea?: CropArea;
|
|
17
|
+
config?: CropConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function CropComponent({
|
|
21
|
+
imageWidth,
|
|
22
|
+
imageHeight,
|
|
23
|
+
onCropChange,
|
|
24
|
+
onCropComplete,
|
|
25
|
+
initialArea,
|
|
26
|
+
config = {},
|
|
27
|
+
}: CropComponentProps) {
|
|
28
|
+
const [cropArea, setCropArea] = useState<CropArea>(
|
|
29
|
+
initialArea || {
|
|
30
|
+
x: 0,
|
|
31
|
+
y: 0,
|
|
32
|
+
width: imageWidth,
|
|
33
|
+
height: imageHeight,
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
37
|
+
const [dragHandle, setDragHandle] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const presets = CropTool.getPresets();
|
|
40
|
+
|
|
41
|
+
const handleCropChange = useCallback((newArea: CropArea) => {
|
|
42
|
+
setCropArea(newArea);
|
|
43
|
+
onCropChange?.(newArea);
|
|
44
|
+
}, [onCropChange]);
|
|
45
|
+
|
|
46
|
+
const handlePresetSelect = useCallback((preset: any) => {
|
|
47
|
+
const constrainedArea = CropTool.constrainToAspectRatio(
|
|
48
|
+
cropArea.width,
|
|
49
|
+
cropArea.height,
|
|
50
|
+
preset.aspectRatio
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const centeredArea = {
|
|
54
|
+
...constrainedArea,
|
|
55
|
+
x: (imageWidth - constrainedArea.width) / 2,
|
|
56
|
+
y: (imageHeight - constrainedArea.height) / 2,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
handleCropChange(centeredArea);
|
|
60
|
+
}, [cropArea, imageWidth, imageHeight, handleCropChange]);
|
|
61
|
+
|
|
62
|
+
const handleCenterCrop = useCallback(() => {
|
|
63
|
+
const centerCrop = CropTool.centerCrop(imageWidth, imageHeight);
|
|
64
|
+
handleCropChange(centerCrop);
|
|
65
|
+
}, [imageWidth, imageHeight, handleCropChange]);
|
|
66
|
+
|
|
67
|
+
const renderPresets = () => (
|
|
68
|
+
<View style={styles.presetsContainer}>
|
|
69
|
+
<Text style={styles.presetsLabel}>Aspect Ratio</Text>
|
|
70
|
+
<View style={styles.presetButtons}>
|
|
71
|
+
{presets.map((preset) => (
|
|
72
|
+
<TouchableOpacity
|
|
73
|
+
key={preset.name}
|
|
74
|
+
style={[
|
|
75
|
+
styles.presetButton,
|
|
76
|
+
config.aspectRatio === preset.aspectRatio && styles.selectedPreset,
|
|
77
|
+
]}
|
|
78
|
+
onPress={() => handlePresetSelect(preset)}
|
|
79
|
+
>
|
|
80
|
+
<Text style={[
|
|
81
|
+
styles.presetText,
|
|
82
|
+
config.aspectRatio === preset.aspectRatio && styles.presetTextSelected
|
|
83
|
+
]}>{preset.name}</Text>
|
|
84
|
+
</TouchableOpacity>
|
|
85
|
+
))}
|
|
86
|
+
</View>
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const renderActions = () => (
|
|
91
|
+
<View style={styles.actionsContainer}>
|
|
92
|
+
<TouchableOpacity
|
|
93
|
+
style={styles.actionButton}
|
|
94
|
+
onPress={handleCenterCrop}
|
|
95
|
+
>
|
|
96
|
+
<Text style={styles.actionButtonText}>Center Crop</Text>
|
|
97
|
+
</TouchableOpacity>
|
|
98
|
+
|
|
99
|
+
<TouchableOpacity
|
|
100
|
+
style={[styles.actionButton, styles.primaryButton]}
|
|
101
|
+
onPress={() => onCropComplete?.(cropArea)}
|
|
102
|
+
>
|
|
103
|
+
<Text style={styles.primaryButtonText}>Apply Crop</Text>
|
|
104
|
+
</TouchableOpacity>
|
|
105
|
+
</View>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<View style={styles.container}>
|
|
110
|
+
{renderPresets()}
|
|
111
|
+
{renderActions()}
|
|
112
|
+
</View>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const styles = StyleSheet.create({
|
|
117
|
+
container: {
|
|
118
|
+
padding: 16,
|
|
119
|
+
gap: 16,
|
|
120
|
+
},
|
|
121
|
+
presetsContainer: {
|
|
122
|
+
gap: 8,
|
|
123
|
+
},
|
|
124
|
+
presetsLabel: {
|
|
125
|
+
fontSize: 16,
|
|
126
|
+
fontWeight: '600',
|
|
127
|
+
color: '#333333',
|
|
128
|
+
marginBottom: 8,
|
|
129
|
+
},
|
|
130
|
+
presetButtons: {
|
|
131
|
+
flexDirection: 'row',
|
|
132
|
+
flexWrap: 'wrap',
|
|
133
|
+
gap: 8,
|
|
134
|
+
},
|
|
135
|
+
presetButton: {
|
|
136
|
+
paddingHorizontal: 12,
|
|
137
|
+
paddingVertical: 8,
|
|
138
|
+
borderRadius: 6,
|
|
139
|
+
borderWidth: 1,
|
|
140
|
+
borderColor: '#d0d0d0',
|
|
141
|
+
backgroundColor: '#ffffff',
|
|
142
|
+
},
|
|
143
|
+
selectedPreset: {
|
|
144
|
+
backgroundColor: '#007bff',
|
|
145
|
+
borderColor: '#007bff',
|
|
146
|
+
},
|
|
147
|
+
presetText: {
|
|
148
|
+
fontSize: 14,
|
|
149
|
+
fontWeight: '500',
|
|
150
|
+
color: '#333333',
|
|
151
|
+
},
|
|
152
|
+
presetTextSelected: {
|
|
153
|
+
color: '#ffffff',
|
|
154
|
+
},
|
|
155
|
+
actionsContainer: {
|
|
156
|
+
flexDirection: 'row',
|
|
157
|
+
justifyContent: 'space-between',
|
|
158
|
+
gap: 12,
|
|
159
|
+
},
|
|
160
|
+
actionButton: {
|
|
161
|
+
flex: 1,
|
|
162
|
+
paddingVertical: 12,
|
|
163
|
+
borderRadius: 8,
|
|
164
|
+
borderWidth: 1,
|
|
165
|
+
borderColor: '#d0d0d0',
|
|
166
|
+
backgroundColor: '#ffffff',
|
|
167
|
+
alignItems: 'center',
|
|
168
|
+
},
|
|
169
|
+
primaryButton: {
|
|
170
|
+
backgroundColor: '#007bff',
|
|
171
|
+
borderColor: '#007bff',
|
|
172
|
+
},
|
|
173
|
+
actionButtonText: {
|
|
174
|
+
fontSize: 16,
|
|
175
|
+
fontWeight: '600',
|
|
176
|
+
color: '#333333',
|
|
177
|
+
},
|
|
178
|
+
primaryButtonText: {
|
|
179
|
+
fontSize: 16,
|
|
180
|
+
fontWeight: '600',
|
|
181
|
+
color: '#ffffff',
|
|
182
|
+
},
|
|
183
|
+
});
|