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.
Files changed (39) hide show
  1. package/dist/components/AdjustTool.svelte +317 -0
  2. package/dist/components/AdjustTool.svelte.d.ts +9 -0
  3. package/dist/components/BlurTool.svelte +613 -0
  4. package/dist/components/BlurTool.svelte.d.ts +15 -0
  5. package/dist/components/Canvas.svelte +214 -0
  6. package/dist/components/Canvas.svelte.d.ts +17 -0
  7. package/dist/components/CropTool.svelte +942 -0
  8. package/dist/components/CropTool.svelte.d.ts +14 -0
  9. package/dist/components/ExportTool.svelte +191 -0
  10. package/dist/components/ExportTool.svelte.d.ts +10 -0
  11. package/dist/components/FilterTool.svelte +492 -0
  12. package/dist/components/FilterTool.svelte.d.ts +12 -0
  13. package/dist/components/ImageEditor.svelte +735 -0
  14. package/dist/components/ImageEditor.svelte.d.ts +12 -0
  15. package/dist/components/RotateTool.svelte +157 -0
  16. package/dist/components/RotateTool.svelte.d.ts +9 -0
  17. package/dist/components/StampTool.svelte +678 -0
  18. package/dist/components/StampTool.svelte.d.ts +15 -0
  19. package/dist/components/Toolbar.svelte +136 -0
  20. package/dist/components/Toolbar.svelte.d.ts +10 -0
  21. package/dist/config/stamps.d.ts +2 -0
  22. package/dist/config/stamps.js +22 -0
  23. package/dist/i18n/index.d.ts +1 -0
  24. package/dist/i18n/index.js +9 -0
  25. package/dist/i18n/locales/en.json +68 -0
  26. package/dist/i18n/locales/ja.json +68 -0
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.js +5 -0
  29. package/dist/types.d.ts +97 -0
  30. package/dist/types.js +1 -0
  31. package/dist/utils/adjustments.d.ts +26 -0
  32. package/dist/utils/adjustments.js +525 -0
  33. package/dist/utils/canvas.d.ts +30 -0
  34. package/dist/utils/canvas.js +293 -0
  35. package/dist/utils/filters.d.ts +18 -0
  36. package/dist/utils/filters.js +114 -0
  37. package/dist/utils/history.d.ts +15 -0
  38. package/dist/utils/history.js +67 -0
  39. 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;