tokimeki-image-editor 0.1.1 → 0.1.3
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/dist/components/AdjustTool.svelte +317 -0
- package/dist/components/AdjustTool.svelte.d.ts +9 -0
- package/dist/components/BlurTool.svelte +613 -0
- package/dist/components/BlurTool.svelte.d.ts +15 -0
- package/dist/components/Canvas.svelte +214 -0
- package/dist/components/Canvas.svelte.d.ts +17 -0
- package/dist/components/CropTool.svelte +942 -0
- package/dist/components/CropTool.svelte.d.ts +14 -0
- package/dist/components/ExportTool.svelte +191 -0
- package/dist/components/ExportTool.svelte.d.ts +10 -0
- package/dist/components/FilterTool.svelte +492 -0
- package/dist/components/FilterTool.svelte.d.ts +12 -0
- package/dist/components/ImageEditor.svelte +735 -0
- package/dist/components/ImageEditor.svelte.d.ts +12 -0
- package/dist/components/RotateTool.svelte +157 -0
- package/dist/components/RotateTool.svelte.d.ts +9 -0
- package/dist/components/StampTool.svelte +678 -0
- package/dist/components/StampTool.svelte.d.ts +15 -0
- package/dist/components/Toolbar.svelte +136 -0
- package/dist/components/Toolbar.svelte.d.ts +10 -0
- package/dist/config/stamps.d.ts +2 -0
- package/dist/config/stamps.js +22 -0
- package/dist/i18n/index.d.ts +1 -0
- package/dist/i18n/index.js +9 -0
- package/dist/i18n/locales/en.json +68 -0
- package/dist/i18n/locales/ja.json +68 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +1 -0
- package/dist/utils/adjustments.d.ts +26 -0
- package/dist/utils/adjustments.js +525 -0
- package/dist/utils/canvas.d.ts +30 -0
- package/dist/utils/canvas.js +293 -0
- package/dist/utils/filters.d.ts +18 -0
- package/dist/utils/filters.js +114 -0
- package/dist/utils/history.d.ts +15 -0
- package/dist/utils/history.js +67 -0
- package/package.json +1 -1
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
<script lang="ts">import { onMount } from 'svelte';
|
|
2
|
+
import { _ } from 'svelte-i18n';
|
|
3
|
+
import { screenToImageCoords, imageToCanvasCoords } from '../utils/canvas';
|
|
4
|
+
import { X } from 'lucide-svelte';
|
|
5
|
+
let { canvas, image, viewport, transform, blurAreas, cropArea, onUpdate, onClose, onViewportChange } = $props();
|
|
6
|
+
let containerElement = $state(null);
|
|
7
|
+
// Helper to get coordinates from mouse or touch event
|
|
8
|
+
function getEventCoords(event) {
|
|
9
|
+
if ('touches' in event && event.touches.length > 0) {
|
|
10
|
+
return { clientX: event.touches[0].clientX, clientY: event.touches[0].clientY };
|
|
11
|
+
}
|
|
12
|
+
else if ('clientX' in event) {
|
|
13
|
+
return { clientX: event.clientX, clientY: event.clientY };
|
|
14
|
+
}
|
|
15
|
+
return { clientX: 0, clientY: 0 };
|
|
16
|
+
}
|
|
17
|
+
onMount(() => {
|
|
18
|
+
if (containerElement) {
|
|
19
|
+
// Add touch event listeners with passive: false to allow preventDefault
|
|
20
|
+
containerElement.addEventListener('touchstart', handleContainerTouchStart, { passive: false });
|
|
21
|
+
containerElement.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
22
|
+
containerElement.addEventListener('touchend', handleTouchEnd, { passive: false });
|
|
23
|
+
}
|
|
24
|
+
return () => {
|
|
25
|
+
if (containerElement) {
|
|
26
|
+
containerElement.removeEventListener('touchstart', handleContainerTouchStart);
|
|
27
|
+
containerElement.removeEventListener('touchmove', handleTouchMove);
|
|
28
|
+
containerElement.removeEventListener('touchend', handleTouchEnd);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
// States for creating new blur area
|
|
33
|
+
let isCreating = $state(false);
|
|
34
|
+
let createStart = $state(null);
|
|
35
|
+
let createEnd = $state(null);
|
|
36
|
+
// States for editing existing blur area
|
|
37
|
+
let selectedAreaId = $state(null);
|
|
38
|
+
let isDragging = $state(false);
|
|
39
|
+
let isResizing = $state(false);
|
|
40
|
+
let dragStart = $state({ x: 0, y: 0 });
|
|
41
|
+
let resizeHandle = $state(null);
|
|
42
|
+
let initialArea = $state(null);
|
|
43
|
+
// Viewport panning
|
|
44
|
+
let isPanning = $state(false);
|
|
45
|
+
let lastPanPosition = $state({ x: 0, y: 0 });
|
|
46
|
+
// Convert blur areas to canvas coordinates for rendering
|
|
47
|
+
let canvasBlurAreas = $derived.by(() => {
|
|
48
|
+
if (!canvas || !image)
|
|
49
|
+
return [];
|
|
50
|
+
return blurAreas.map(area => {
|
|
51
|
+
// Determine source dimensions and offset based on crop area
|
|
52
|
+
const sourceWidth = cropArea ? cropArea.width : image.width;
|
|
53
|
+
const sourceHeight = cropArea ? cropArea.height : image.height;
|
|
54
|
+
const offsetX = cropArea ? cropArea.x : 0;
|
|
55
|
+
const offsetY = cropArea ? cropArea.y : 0;
|
|
56
|
+
// Convert to crop-relative coordinates (or image-relative if no crop)
|
|
57
|
+
const adjustedX = area.x - offsetX;
|
|
58
|
+
const adjustedY = area.y - offsetY;
|
|
59
|
+
// Calculate canvas coordinates
|
|
60
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
61
|
+
const centerX = canvas.width / 2;
|
|
62
|
+
const centerY = canvas.height / 2;
|
|
63
|
+
const canvasX = (adjustedX - sourceWidth / 2) * totalScale + centerX + viewport.offsetX;
|
|
64
|
+
const canvasY = (adjustedY - sourceHeight / 2) * totalScale + centerY + viewport.offsetY;
|
|
65
|
+
const canvasWidth = area.width * totalScale;
|
|
66
|
+
const canvasHeight = area.height * totalScale;
|
|
67
|
+
return {
|
|
68
|
+
...area,
|
|
69
|
+
canvasX,
|
|
70
|
+
canvasY,
|
|
71
|
+
canvasWidth,
|
|
72
|
+
canvasHeight
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
// Creating rect in canvas coordinates
|
|
77
|
+
let creatingRect = $derived.by(() => {
|
|
78
|
+
if (!isCreating || !createStart || !createEnd)
|
|
79
|
+
return null;
|
|
80
|
+
const x = Math.min(createStart.x, createEnd.x);
|
|
81
|
+
const y = Math.min(createStart.y, createEnd.y);
|
|
82
|
+
const width = Math.abs(createEnd.x - createStart.x);
|
|
83
|
+
const height = Math.abs(createEnd.y - createStart.y);
|
|
84
|
+
return { x, y, width, height };
|
|
85
|
+
});
|
|
86
|
+
function handleContainerMouseDown(event) {
|
|
87
|
+
if (!canvas || !image)
|
|
88
|
+
return;
|
|
89
|
+
// Check if it's a mouse event with non-left button
|
|
90
|
+
if ('button' in event && event.button !== 0)
|
|
91
|
+
return;
|
|
92
|
+
const coords = getEventCoords(event);
|
|
93
|
+
const rect = canvas.getBoundingClientRect();
|
|
94
|
+
const mouseX = coords.clientX - rect.left;
|
|
95
|
+
const mouseY = coords.clientY - rect.top;
|
|
96
|
+
// Check if clicking on an existing blur area
|
|
97
|
+
for (let i = canvasBlurAreas.length - 1; i >= 0; i--) {
|
|
98
|
+
const area = canvasBlurAreas[i];
|
|
99
|
+
if (mouseX >= area.canvasX &&
|
|
100
|
+
mouseX <= area.canvasX + area.canvasWidth &&
|
|
101
|
+
mouseY >= area.canvasY &&
|
|
102
|
+
mouseY <= area.canvasY + area.canvasHeight) {
|
|
103
|
+
selectedAreaId = area.id;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Not clicking on any existing area - start creating new area
|
|
108
|
+
selectedAreaId = null;
|
|
109
|
+
isCreating = true;
|
|
110
|
+
createStart = { x: mouseX, y: mouseY };
|
|
111
|
+
createEnd = { x: mouseX, y: mouseY };
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
}
|
|
114
|
+
function handleMouseMove(event) {
|
|
115
|
+
if (!canvas || !image)
|
|
116
|
+
return;
|
|
117
|
+
const coords = getEventCoords(event);
|
|
118
|
+
const rect = canvas.getBoundingClientRect();
|
|
119
|
+
const mouseX = coords.clientX - rect.left;
|
|
120
|
+
const mouseY = coords.clientY - rect.top;
|
|
121
|
+
// Handle viewport panning
|
|
122
|
+
if (isPanning && onViewportChange) {
|
|
123
|
+
const deltaX = coords.clientX - lastPanPosition.x;
|
|
124
|
+
const deltaY = coords.clientY - lastPanPosition.y;
|
|
125
|
+
const imgWidth = image.width;
|
|
126
|
+
const imgHeight = image.height;
|
|
127
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
128
|
+
const scaledWidth = imgWidth * totalScale;
|
|
129
|
+
const scaledHeight = imgHeight * totalScale;
|
|
130
|
+
const overflowMargin = 0.2;
|
|
131
|
+
const maxOffsetX = (scaledWidth / 2) - (canvas.width / 2) + (canvas.width * overflowMargin);
|
|
132
|
+
const maxOffsetY = (scaledHeight / 2) - (canvas.height / 2) + (canvas.height * overflowMargin);
|
|
133
|
+
const newOffsetX = viewport.offsetX + deltaX;
|
|
134
|
+
const newOffsetY = viewport.offsetY + deltaY;
|
|
135
|
+
const clampedOffsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, newOffsetX));
|
|
136
|
+
const clampedOffsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, newOffsetY));
|
|
137
|
+
onViewportChange({
|
|
138
|
+
offsetX: clampedOffsetX,
|
|
139
|
+
offsetY: clampedOffsetY
|
|
140
|
+
});
|
|
141
|
+
lastPanPosition = { x: coords.clientX, y: coords.clientY };
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Handle creating new blur area
|
|
146
|
+
if (isCreating && createStart) {
|
|
147
|
+
createEnd = { x: mouseX, y: mouseY };
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Handle dragging selected area
|
|
152
|
+
if (isDragging && initialArea && selectedAreaId) {
|
|
153
|
+
const deltaX = coords.clientX - dragStart.x;
|
|
154
|
+
const deltaY = coords.clientY - dragStart.y;
|
|
155
|
+
// Convert delta to image coordinates
|
|
156
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
157
|
+
const imgDeltaX = deltaX / totalScale;
|
|
158
|
+
const imgDeltaY = deltaY / totalScale;
|
|
159
|
+
// Allow blur areas to extend outside image bounds
|
|
160
|
+
const newX = initialArea.x + imgDeltaX;
|
|
161
|
+
const newY = initialArea.y + imgDeltaY;
|
|
162
|
+
const updatedAreas = blurAreas.map(area => area.id === selectedAreaId
|
|
163
|
+
? { ...area, x: newX, y: newY }
|
|
164
|
+
: area);
|
|
165
|
+
onUpdate(updatedAreas);
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Handle resizing selected area
|
|
170
|
+
if (isResizing && initialArea && resizeHandle && selectedAreaId) {
|
|
171
|
+
const deltaX = coords.clientX - dragStart.x;
|
|
172
|
+
const deltaY = coords.clientY - dragStart.y;
|
|
173
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
174
|
+
const imgDeltaX = deltaX / totalScale;
|
|
175
|
+
const imgDeltaY = deltaY / totalScale;
|
|
176
|
+
let newArea = { ...initialArea };
|
|
177
|
+
switch (resizeHandle) {
|
|
178
|
+
case 'nw':
|
|
179
|
+
newArea.x = initialArea.x + imgDeltaX;
|
|
180
|
+
newArea.y = initialArea.y + imgDeltaY;
|
|
181
|
+
newArea.width = initialArea.width - imgDeltaX;
|
|
182
|
+
newArea.height = initialArea.height - imgDeltaY;
|
|
183
|
+
break;
|
|
184
|
+
case 'n':
|
|
185
|
+
newArea.y = initialArea.y + imgDeltaY;
|
|
186
|
+
newArea.height = initialArea.height - imgDeltaY;
|
|
187
|
+
break;
|
|
188
|
+
case 'ne':
|
|
189
|
+
newArea.y = initialArea.y + imgDeltaY;
|
|
190
|
+
newArea.width = initialArea.width + imgDeltaX;
|
|
191
|
+
newArea.height = initialArea.height - imgDeltaY;
|
|
192
|
+
break;
|
|
193
|
+
case 'w':
|
|
194
|
+
newArea.x = initialArea.x + imgDeltaX;
|
|
195
|
+
newArea.width = initialArea.width - imgDeltaX;
|
|
196
|
+
break;
|
|
197
|
+
case 'e':
|
|
198
|
+
newArea.width = initialArea.width + imgDeltaX;
|
|
199
|
+
break;
|
|
200
|
+
case 'sw':
|
|
201
|
+
newArea.x = initialArea.x + imgDeltaX;
|
|
202
|
+
newArea.width = initialArea.width - imgDeltaX;
|
|
203
|
+
newArea.height = initialArea.height + imgDeltaY;
|
|
204
|
+
break;
|
|
205
|
+
case 's':
|
|
206
|
+
newArea.height = initialArea.height + imgDeltaY;
|
|
207
|
+
break;
|
|
208
|
+
case 'se':
|
|
209
|
+
newArea.width = initialArea.width + imgDeltaX;
|
|
210
|
+
newArea.height = initialArea.height + imgDeltaY;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
// Enforce minimum size (allow areas to extend outside image bounds)
|
|
214
|
+
if (newArea.width >= 20 && newArea.height >= 20) {
|
|
215
|
+
const updatedAreas = blurAreas.map(area => area.id === selectedAreaId ? newArea : area);
|
|
216
|
+
onUpdate(updatedAreas);
|
|
217
|
+
}
|
|
218
|
+
event.preventDefault();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function handleMouseUp(event) {
|
|
222
|
+
if (!canvas || !image)
|
|
223
|
+
return;
|
|
224
|
+
// Finish creating new blur area
|
|
225
|
+
if (isCreating && createStart && createEnd && creatingRect) {
|
|
226
|
+
// Only create if area is large enough
|
|
227
|
+
if (creatingRect.width > 10 && creatingRect.height > 10) {
|
|
228
|
+
// Convert canvas coordinates to image coordinates
|
|
229
|
+
const centerX = canvas.width / 2;
|
|
230
|
+
const centerY = canvas.height / 2;
|
|
231
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
232
|
+
// Determine source dimensions (crop-aware)
|
|
233
|
+
const sourceWidth = cropArea ? cropArea.width : image.width;
|
|
234
|
+
const sourceHeight = cropArea ? cropArea.height : image.height;
|
|
235
|
+
// Top-left corner (crop-relative coordinates)
|
|
236
|
+
const topLeftX = (creatingRect.x - centerX - viewport.offsetX) / totalScale + sourceWidth / 2;
|
|
237
|
+
const topLeftY = (creatingRect.y - centerY - viewport.offsetY) / totalScale + sourceHeight / 2;
|
|
238
|
+
// Bottom-right corner (crop-relative coordinates)
|
|
239
|
+
const bottomRightX = (creatingRect.x + creatingRect.width - centerX - viewport.offsetX) / totalScale + sourceWidth / 2;
|
|
240
|
+
const bottomRightY = (creatingRect.y + creatingRect.height - centerY - viewport.offsetY) / totalScale + sourceHeight / 2;
|
|
241
|
+
// Convert to absolute image coordinates
|
|
242
|
+
const absoluteX = cropArea ? topLeftX + cropArea.x : topLeftX;
|
|
243
|
+
const absoluteY = cropArea ? topLeftY + cropArea.y : topLeftY;
|
|
244
|
+
const newArea = {
|
|
245
|
+
id: `blur-${Date.now()}`,
|
|
246
|
+
x: absoluteX,
|
|
247
|
+
y: absoluteY,
|
|
248
|
+
width: bottomRightX - topLeftX,
|
|
249
|
+
height: bottomRightY - topLeftY,
|
|
250
|
+
blurStrength: 20 // Default blur strength
|
|
251
|
+
};
|
|
252
|
+
onUpdate([...blurAreas, newArea]);
|
|
253
|
+
selectedAreaId = newArea.id;
|
|
254
|
+
}
|
|
255
|
+
isCreating = false;
|
|
256
|
+
createStart = null;
|
|
257
|
+
createEnd = null;
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
isDragging = false;
|
|
262
|
+
isResizing = false;
|
|
263
|
+
isPanning = false;
|
|
264
|
+
resizeHandle = null;
|
|
265
|
+
initialArea = null;
|
|
266
|
+
}
|
|
267
|
+
function handleAreaMouseDown(event, areaId) {
|
|
268
|
+
if (!canvas || !image)
|
|
269
|
+
return;
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
event.stopPropagation();
|
|
272
|
+
const coords = getEventCoords(event);
|
|
273
|
+
selectedAreaId = areaId;
|
|
274
|
+
isDragging = true;
|
|
275
|
+
dragStart = { x: coords.clientX, y: coords.clientY };
|
|
276
|
+
initialArea = blurAreas.find(a => a.id === areaId) || null;
|
|
277
|
+
}
|
|
278
|
+
function handleHandleMouseDown(event, areaId, handle) {
|
|
279
|
+
if (!canvas || !image)
|
|
280
|
+
return;
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
event.stopPropagation();
|
|
283
|
+
const coords = getEventCoords(event);
|
|
284
|
+
selectedAreaId = areaId;
|
|
285
|
+
isResizing = true;
|
|
286
|
+
resizeHandle = handle;
|
|
287
|
+
dragStart = { x: coords.clientX, y: coords.clientY };
|
|
288
|
+
initialArea = blurAreas.find(a => a.id === areaId) || null;
|
|
289
|
+
}
|
|
290
|
+
function handleDeleteArea() {
|
|
291
|
+
if (!selectedAreaId)
|
|
292
|
+
return;
|
|
293
|
+
const updatedAreas = blurAreas.filter(area => area.id !== selectedAreaId);
|
|
294
|
+
onUpdate(updatedAreas);
|
|
295
|
+
selectedAreaId = null;
|
|
296
|
+
}
|
|
297
|
+
function handleBlurStrengthChange(value) {
|
|
298
|
+
if (!selectedAreaId)
|
|
299
|
+
return;
|
|
300
|
+
const updatedAreas = blurAreas.map(area => area.id === selectedAreaId
|
|
301
|
+
? { ...area, blurStrength: value }
|
|
302
|
+
: area);
|
|
303
|
+
onUpdate(updatedAreas);
|
|
304
|
+
}
|
|
305
|
+
const selectedArea = $derived(blurAreas.find(area => area.id === selectedAreaId));
|
|
306
|
+
// Unified touch handlers
|
|
307
|
+
const handleContainerTouchStart = handleContainerMouseDown;
|
|
308
|
+
const handleTouchMove = handleMouseMove;
|
|
309
|
+
function handleTouchEnd(event) {
|
|
310
|
+
if (event.touches.length === 0) {
|
|
311
|
+
handleMouseUp();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
</script>
|
|
315
|
+
|
|
316
|
+
<svelte:window
|
|
317
|
+
onmousemove={handleMouseMove}
|
|
318
|
+
onmouseup={handleMouseUp}
|
|
319
|
+
/>
|
|
320
|
+
|
|
321
|
+
<!-- Overlay -->
|
|
322
|
+
<div
|
|
323
|
+
bind:this={containerElement}
|
|
324
|
+
class="blur-tool-overlay"
|
|
325
|
+
onmousedown={handleContainerMouseDown}
|
|
326
|
+
role="button"
|
|
327
|
+
tabindex="-1"
|
|
328
|
+
>
|
|
329
|
+
<svg class="blur-tool-svg">
|
|
330
|
+
<defs>
|
|
331
|
+
<pattern id="blur-grid" width="10" height="10" patternUnits="userSpaceOnUse">
|
|
332
|
+
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255, 255, 255, 0.2)" stroke-width="1"/>
|
|
333
|
+
</pattern>
|
|
334
|
+
</defs>
|
|
335
|
+
|
|
336
|
+
<!-- Render creating rectangle -->
|
|
337
|
+
{#if creatingRect}
|
|
338
|
+
<rect
|
|
339
|
+
x={creatingRect.x}
|
|
340
|
+
y={creatingRect.y}
|
|
341
|
+
width={creatingRect.width}
|
|
342
|
+
height={creatingRect.height}
|
|
343
|
+
fill="rgba(100, 150, 255, 0.2)"
|
|
344
|
+
stroke="rgba(100, 150, 255, 0.8)"
|
|
345
|
+
stroke-width="2"
|
|
346
|
+
stroke-dasharray="5,5"
|
|
347
|
+
/>
|
|
348
|
+
{/if}
|
|
349
|
+
|
|
350
|
+
<!-- Render existing blur areas -->
|
|
351
|
+
{#each canvasBlurAreas as area (area.id)}
|
|
352
|
+
{@const isSelected = area.id === selectedAreaId}
|
|
353
|
+
|
|
354
|
+
<g>
|
|
355
|
+
<!-- Rectangle -->
|
|
356
|
+
<rect
|
|
357
|
+
x={area.canvasX}
|
|
358
|
+
y={area.canvasY}
|
|
359
|
+
width={area.canvasWidth}
|
|
360
|
+
height={area.canvasHeight}
|
|
361
|
+
fill={isSelected ? "rgba(100, 150, 255, 0.15)" : "rgba(255, 255, 255, 0.1)"}
|
|
362
|
+
stroke={isSelected ? "rgba(100, 150, 255, 0.9)" : "rgba(255, 255, 255, 0.5)"}
|
|
363
|
+
stroke-width={isSelected ? "3" : "2"}
|
|
364
|
+
stroke-dasharray="5,5"
|
|
365
|
+
onmousedown={(e) => handleAreaMouseDown(e, area.id)}
|
|
366
|
+
ontouchstart={(e) => handleAreaMouseDown(e, area.id)}
|
|
367
|
+
style="cursor: move;"
|
|
368
|
+
/>
|
|
369
|
+
|
|
370
|
+
<!-- Resize handles (only for selected area) -->
|
|
371
|
+
{#if isSelected}
|
|
372
|
+
{#each ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'] as handle}
|
|
373
|
+
{@const handleSize = 10}
|
|
374
|
+
{@const handleX = handle.includes('w') ? area.canvasX - handleSize/2
|
|
375
|
+
: handle.includes('e') ? area.canvasX + area.canvasWidth - handleSize/2
|
|
376
|
+
: area.canvasX + area.canvasWidth/2 - handleSize/2}
|
|
377
|
+
{@const handleY = handle.includes('n') ? area.canvasY - handleSize/2
|
|
378
|
+
: handle.includes('s') ? area.canvasY + area.canvasHeight - handleSize/2
|
|
379
|
+
: area.canvasY + area.canvasHeight/2 - handleSize/2}
|
|
380
|
+
{@const cursor = handle === 'n' || handle === 's' ? 'ns-resize'
|
|
381
|
+
: handle === 'w' || handle === 'e' ? 'ew-resize'
|
|
382
|
+
: handle === 'nw' || handle === 'se' ? 'nwse-resize'
|
|
383
|
+
: 'nesw-resize'}
|
|
384
|
+
|
|
385
|
+
<rect
|
|
386
|
+
x={handleX}
|
|
387
|
+
y={handleY}
|
|
388
|
+
width={handleSize}
|
|
389
|
+
height={handleSize}
|
|
390
|
+
fill="rgba(100, 150, 255, 0.9)"
|
|
391
|
+
stroke="#fff"
|
|
392
|
+
stroke-width="2"
|
|
393
|
+
onmousedown={(e) => handleHandleMouseDown(e, area.id, handle)}
|
|
394
|
+
ontouchstart={(e) => handleHandleMouseDown(e, area.id, handle)}
|
|
395
|
+
style="cursor: {cursor};"
|
|
396
|
+
/>
|
|
397
|
+
{/each}
|
|
398
|
+
{/if}
|
|
399
|
+
</g>
|
|
400
|
+
{/each}
|
|
401
|
+
</svg>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<!-- Control panel -->
|
|
405
|
+
<div class="blur-tool-panel">
|
|
406
|
+
<div class="panel-header">
|
|
407
|
+
<h3>{$_('editor.blur')}</h3>
|
|
408
|
+
<button class="close-btn" onclick={onClose} title={$_('editor.close')}>
|
|
409
|
+
<X size={20} />
|
|
410
|
+
</button>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
{#if selectedArea}
|
|
414
|
+
<div class="panel-content">
|
|
415
|
+
<div class="control-group">
|
|
416
|
+
<label for="blur-strength">
|
|
417
|
+
<span>{$_('blur.strength')}</span>
|
|
418
|
+
<span class="value">{selectedArea.blurStrength}</span>
|
|
419
|
+
</label>
|
|
420
|
+
<input
|
|
421
|
+
id="blur-strength"
|
|
422
|
+
type="range"
|
|
423
|
+
min="0"
|
|
424
|
+
max="100"
|
|
425
|
+
value={selectedArea.blurStrength}
|
|
426
|
+
oninput={(e) => handleBlurStrengthChange(Number(e.currentTarget.value))}
|
|
427
|
+
/>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<div class="panel-actions">
|
|
431
|
+
<button class="btn btn-danger" onclick={handleDeleteArea}>
|
|
432
|
+
{$_('editor.delete')}
|
|
433
|
+
</button>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
{:else}
|
|
437
|
+
<div class="panel-hint">
|
|
438
|
+
<p>{$_('blur.hint')}</p>
|
|
439
|
+
</div>
|
|
440
|
+
{/if}
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
<style>
|
|
444
|
+
.blur-tool-overlay {
|
|
445
|
+
position: absolute;
|
|
446
|
+
top: 0;
|
|
447
|
+
left: 0;
|
|
448
|
+
width: 100%;
|
|
449
|
+
height: 100%;
|
|
450
|
+
cursor: crosshair;
|
|
451
|
+
user-select: none;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.blur-tool-svg {
|
|
455
|
+
width: 100%;
|
|
456
|
+
height: 100%;
|
|
457
|
+
pointer-events: none;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.blur-tool-svg rect {
|
|
461
|
+
pointer-events: all;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.blur-tool-panel {
|
|
465
|
+
position: absolute;
|
|
466
|
+
top: 1rem;
|
|
467
|
+
right: 1rem;
|
|
468
|
+
background: rgba(30, 30, 30, 0.95);
|
|
469
|
+
border: 1px solid #444;
|
|
470
|
+
border-radius: 8px;
|
|
471
|
+
padding: 1rem;
|
|
472
|
+
min-width: 250px;
|
|
473
|
+
backdrop-filter: blur(10px);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.panel-header {
|
|
477
|
+
display: flex;
|
|
478
|
+
justify-content: space-between;
|
|
479
|
+
align-items: center;
|
|
480
|
+
margin-bottom: 1rem;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.panel-header h3 {
|
|
484
|
+
margin: 0;
|
|
485
|
+
font-size: 1.1rem;
|
|
486
|
+
color: #fff;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.close-btn {
|
|
490
|
+
display: flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
justify-content: center;
|
|
493
|
+
padding: 0.25rem;
|
|
494
|
+
background: transparent;
|
|
495
|
+
border: none;
|
|
496
|
+
color: #999;
|
|
497
|
+
cursor: pointer;
|
|
498
|
+
border-radius: 4px;
|
|
499
|
+
transition: all 0.2s;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.close-btn:hover {
|
|
503
|
+
background: #444;
|
|
504
|
+
color: #fff;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.panel-content {
|
|
508
|
+
display: flex;
|
|
509
|
+
flex-direction: column;
|
|
510
|
+
gap: 1rem;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.control-group {
|
|
514
|
+
display: flex;
|
|
515
|
+
flex-direction: column;
|
|
516
|
+
gap: 0.5rem;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.control-group label {
|
|
520
|
+
display: flex;
|
|
521
|
+
justify-content: space-between;
|
|
522
|
+
align-items: center;
|
|
523
|
+
font-size: 0.9rem;
|
|
524
|
+
color: #ccc;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.control-group .value {
|
|
528
|
+
color: var(--primary-color, #63b97b);
|
|
529
|
+
font-weight: 600;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.control-group input[type='range'] {
|
|
533
|
+
width: 100%;
|
|
534
|
+
height: 6px;
|
|
535
|
+
background: #444;
|
|
536
|
+
border-radius: 3px;
|
|
537
|
+
outline: none;
|
|
538
|
+
cursor: pointer;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.control-group input[type='range']::-webkit-slider-thumb {
|
|
542
|
+
appearance: none;
|
|
543
|
+
width: 16px;
|
|
544
|
+
height: 16px;
|
|
545
|
+
background: var(--primary-color, #63b97b);
|
|
546
|
+
border-radius: 50%;
|
|
547
|
+
cursor: pointer;
|
|
548
|
+
transition: all 0.2s;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.control-group input[type='range']::-webkit-slider-thumb:hover {
|
|
552
|
+
background: var(--primary-color, #63b97b);
|
|
553
|
+
transform: scale(1.1);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.control-group input[type='range']::-moz-range-thumb {
|
|
557
|
+
width: 16px;
|
|
558
|
+
height: 16px;
|
|
559
|
+
background: var(--primary-color, #63b97b);
|
|
560
|
+
border: none;
|
|
561
|
+
border-radius: 50%;
|
|
562
|
+
cursor: pointer;
|
|
563
|
+
transition: all 0.2s;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.control-group input[type='range']::-moz-range-thumb:hover {
|
|
567
|
+
background: var(--primary-color, #63b97b);
|
|
568
|
+
transform: scale(1.1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.panel-actions {
|
|
572
|
+
display: flex;
|
|
573
|
+
justify-content: flex-end;
|
|
574
|
+
gap: 0.5rem;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.btn {
|
|
578
|
+
padding: 0.5rem 1rem;
|
|
579
|
+
border: none;
|
|
580
|
+
border-radius: 4px;
|
|
581
|
+
cursor: pointer;
|
|
582
|
+
font-size: 0.9rem;
|
|
583
|
+
transition: all 0.2s;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.btn-danger {
|
|
587
|
+
background: #cc3333;
|
|
588
|
+
color: #fff;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.btn-danger:hover {
|
|
592
|
+
background: #dd4444;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.panel-hint {
|
|
596
|
+
padding: 1rem;
|
|
597
|
+
background: #2a2a2a;
|
|
598
|
+
border-radius: 4px;
|
|
599
|
+
color: #999;
|
|
600
|
+
font-size: 0.9rem;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.panel-hint p {
|
|
604
|
+
margin: 0;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
@media (max-width: 767px) {
|
|
608
|
+
.blur-tool-svg rect[fill="rgba(100, 150, 255, 0.9)"] {
|
|
609
|
+
width: 20px !important;
|
|
610
|
+
height: 20px !important;
|
|
611
|
+
stroke-width: 3 !important;
|
|
612
|
+
}
|
|
613
|
+
}</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BlurArea, Viewport, TransformState, CropArea } from '../types';
|
|
2
|
+
interface Props {
|
|
3
|
+
canvas: HTMLCanvasElement | null;
|
|
4
|
+
image: HTMLImageElement | null;
|
|
5
|
+
viewport: Viewport;
|
|
6
|
+
transform: TransformState;
|
|
7
|
+
blurAreas: BlurArea[];
|
|
8
|
+
cropArea?: CropArea | null;
|
|
9
|
+
onUpdate: (blurAreas: BlurArea[]) => void;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onViewportChange?: (viewport: Partial<Viewport>) => void;
|
|
12
|
+
}
|
|
13
|
+
declare const BlurTool: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type BlurTool = ReturnType<typeof BlurTool>;
|
|
15
|
+
export default BlurTool;
|