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,678 @@
1
+ <script lang="ts">import { onMount } from 'svelte';
2
+ import { _ } from 'svelte-i18n';
3
+ import { STAMP_ASSETS } from '../config/stamps';
4
+ import { preloadStampImage } from '../utils/canvas';
5
+ import { RotateCw, Trash2 } from 'lucide-svelte';
6
+ let { canvas, image, viewport, transform, stampAreas, cropArea, onUpdate, onClose, onViewportChange } = $props();
7
+ let overlayElement = $state(null);
8
+ // Helper to get coordinates from mouse or touch event
9
+ function getEventCoords(event) {
10
+ if ('touches' in event && event.touches.length > 0) {
11
+ return { clientX: event.touches[0].clientX, clientY: event.touches[0].clientY };
12
+ }
13
+ else if ('clientX' in event) {
14
+ return { clientX: event.clientX, clientY: event.clientY };
15
+ }
16
+ return { clientX: 0, clientY: 0 };
17
+ }
18
+ onMount(() => {
19
+ if (overlayElement) {
20
+ // Add touch event listeners with passive: false to allow preventDefault
21
+ overlayElement.addEventListener('touchstart', handleCanvasTouchStart, { passive: false });
22
+ overlayElement.addEventListener('touchmove', handleTouchMove, { passive: false });
23
+ overlayElement.addEventListener('touchend', handleTouchEnd, { passive: false });
24
+ }
25
+ return () => {
26
+ if (overlayElement) {
27
+ overlayElement.removeEventListener('touchstart', handleCanvasTouchStart);
28
+ overlayElement.removeEventListener('touchmove', handleTouchMove);
29
+ overlayElement.removeEventListener('touchend', handleTouchEnd);
30
+ }
31
+ };
32
+ });
33
+ // Default stamp size as percentage of the smaller dimension of the image
34
+ const DEFAULT_STAMP_SIZE_PERCENT = 0.1; // 10%
35
+ let selectedStampAsset = $state(null);
36
+ let selectedStampId = $state(null);
37
+ let isDragging = $state(false);
38
+ let isResizing = $state(false);
39
+ let isRotating = $state(false);
40
+ let resizeHandle = $state(null);
41
+ let dragStart = $state({ x: 0, y: 0 });
42
+ let initialStamp = $state(null);
43
+ let initialRotation = $state(0);
44
+ let initialAngle = $state(0);
45
+ let rotationCenter = $state({ x: 0, y: 0 });
46
+ // Viewport panning
47
+ let isPanning = $state(false);
48
+ let lastPanPosition = $state({ x: 0, y: 0 });
49
+ // Convert stamp areas to canvas coordinates for rendering
50
+ let canvasStampAreas = $derived.by(() => {
51
+ if (!canvas || !image)
52
+ return [];
53
+ return stampAreas.map(area => {
54
+ const sourceWidth = cropArea ? cropArea.width : image.width;
55
+ const sourceHeight = cropArea ? cropArea.height : image.height;
56
+ const offsetX = cropArea ? cropArea.x : 0;
57
+ const offsetY = cropArea ? cropArea.y : 0;
58
+ const relativeX = area.x - offsetX;
59
+ const relativeY = area.y - offsetY;
60
+ const totalScale = viewport.scale * viewport.zoom;
61
+ const centerX = canvas.width / 2;
62
+ const centerY = canvas.height / 2;
63
+ const canvasCenterX = (relativeX - sourceWidth / 2) * totalScale + centerX + viewport.offsetX;
64
+ const canvasCenterY = (relativeY - sourceHeight / 2) * totalScale + centerY + viewport.offsetY;
65
+ const canvasWidth = area.width * totalScale;
66
+ const canvasHeight = area.height * totalScale;
67
+ return {
68
+ ...area,
69
+ canvasCenterX,
70
+ canvasCenterY,
71
+ canvasWidth,
72
+ canvasHeight
73
+ };
74
+ });
75
+ });
76
+ function handleCanvasMouseDown(event) {
77
+ if (!canvas || !image)
78
+ return;
79
+ const coords = getEventCoords(event);
80
+ const rect = canvas.getBoundingClientRect();
81
+ const mouseX = coords.clientX - rect.left;
82
+ const mouseY = coords.clientY - rect.top;
83
+ // Check if clicking on rotation handle (se corner)
84
+ for (const canvasStamp of canvasStampAreas) {
85
+ const stamp = stampAreas.find(s => s.id === canvasStamp.id);
86
+ if (!stamp)
87
+ continue;
88
+ const rotHandle = getRotationHandlePosition(canvasStamp);
89
+ const dist = Math.hypot(mouseX - rotHandle.x, mouseY - rotHandle.y);
90
+ if (dist <= 10) {
91
+ isRotating = true;
92
+ selectedStampId = stamp.id;
93
+ dragStart = { x: coords.clientX, y: coords.clientY };
94
+ initialStamp = { ...stamp };
95
+ initialRotation = stamp.rotation || 0;
96
+ rotationCenter = { x: canvasStamp.canvasCenterX, y: canvasStamp.canvasCenterY };
97
+ // Calculate initial angle from center to mouse position
98
+ initialAngle = Math.atan2(mouseY - rotationCenter.y, mouseX - rotationCenter.x) * (180 / Math.PI);
99
+ event.preventDefault();
100
+ return;
101
+ }
102
+ }
103
+ // Check if clicking on resize handle (nw, ne, sw corners only)
104
+ for (const canvasStamp of canvasStampAreas) {
105
+ const stamp = stampAreas.find(s => s.id === canvasStamp.id);
106
+ if (!stamp)
107
+ continue;
108
+ const handle = getResizeHandle(mouseX, mouseY, canvasStamp);
109
+ if (handle) {
110
+ isResizing = true;
111
+ resizeHandle = handle;
112
+ selectedStampId = stamp.id;
113
+ dragStart = { x: coords.clientX, y: coords.clientY };
114
+ initialStamp = { ...stamp };
115
+ event.preventDefault();
116
+ return;
117
+ }
118
+ }
119
+ // Check if clicking on a stamp
120
+ for (const canvasStamp of canvasStampAreas) {
121
+ if (isPointInStamp(mouseX, mouseY, canvasStamp)) {
122
+ const stamp = stampAreas.find(s => s.id === canvasStamp.id);
123
+ if (stamp) {
124
+ selectedStampId = stamp.id;
125
+ isDragging = true;
126
+ dragStart = { x: coords.clientX, y: coords.clientY };
127
+ initialStamp = { ...stamp };
128
+ event.preventDefault();
129
+ return;
130
+ }
131
+ }
132
+ }
133
+ // If clicking outside any stamp, deselect and start panning
134
+ selectedStampId = null;
135
+ isPanning = true;
136
+ lastPanPosition = { x: coords.clientX, y: coords.clientY };
137
+ event.preventDefault();
138
+ }
139
+ function handleMouseMove(event) {
140
+ if (!canvas || !image)
141
+ return;
142
+ const coords = getEventCoords(event);
143
+ // Handle panning
144
+ if (isPanning && onViewportChange) {
145
+ const deltaX = coords.clientX - lastPanPosition.x;
146
+ const deltaY = coords.clientY - lastPanPosition.y;
147
+ const imgWidth = image.width;
148
+ const imgHeight = image.height;
149
+ const totalScale = viewport.scale * viewport.zoom;
150
+ const scaledWidth = imgWidth * totalScale;
151
+ const scaledHeight = imgHeight * totalScale;
152
+ const overflowMargin = 0.2;
153
+ const maxOffsetX = (scaledWidth / 2) - (canvas.width / 2) + (canvas.width * overflowMargin);
154
+ const maxOffsetY = (scaledHeight / 2) - (canvas.height / 2) + (canvas.height * overflowMargin);
155
+ const newOffsetX = viewport.offsetX + deltaX;
156
+ const newOffsetY = viewport.offsetY + deltaY;
157
+ const clampedOffsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, newOffsetX));
158
+ const clampedOffsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, newOffsetY));
159
+ onViewportChange({
160
+ offsetX: clampedOffsetX,
161
+ offsetY: clampedOffsetY
162
+ });
163
+ lastPanPosition = { x: coords.clientX, y: coords.clientY };
164
+ event.preventDefault();
165
+ return;
166
+ }
167
+ // Handle dragging
168
+ if (isDragging && initialStamp && selectedStampId) {
169
+ const deltaX = coords.clientX - dragStart.x;
170
+ const deltaY = coords.clientY - dragStart.y;
171
+ const totalScale = viewport.scale * viewport.zoom;
172
+ const imgDeltaX = deltaX / totalScale;
173
+ const imgDeltaY = deltaY / totalScale;
174
+ const newX = initialStamp.x + imgDeltaX;
175
+ const newY = initialStamp.y + imgDeltaY;
176
+ const updatedAreas = stampAreas.map(area => area.id === selectedStampId
177
+ ? { ...area, x: newX, y: newY }
178
+ : area);
179
+ onUpdate(updatedAreas);
180
+ event.preventDefault();
181
+ return;
182
+ }
183
+ // Handle resizing
184
+ if (isResizing && initialStamp && resizeHandle && selectedStampId) {
185
+ const deltaX = coords.clientX - dragStart.x;
186
+ const deltaY = coords.clientY - dragStart.y;
187
+ const totalScale = viewport.scale * viewport.zoom;
188
+ const imgDeltaX = deltaX / totalScale;
189
+ const imgDeltaY = deltaY / totalScale;
190
+ // Calculate new size maintaining aspect ratio
191
+ const aspectRatio = initialStamp.width / initialStamp.height;
192
+ // Determine resize direction based on handle
193
+ let sizeDelta = 0;
194
+ switch (resizeHandle) {
195
+ case 'nw':
196
+ // Resize from top-left corner (inverse)
197
+ sizeDelta = -(imgDeltaX + imgDeltaY) / 2;
198
+ break;
199
+ case 'ne':
200
+ // Resize from top-right corner
201
+ sizeDelta = (imgDeltaX - imgDeltaY) / 2;
202
+ break;
203
+ case 'sw':
204
+ // Resize from bottom-left corner
205
+ sizeDelta = (-imgDeltaX + imgDeltaY) / 2;
206
+ break;
207
+ }
208
+ let newWidth = initialStamp.width + sizeDelta;
209
+ let newHeight = newWidth / aspectRatio;
210
+ // Enforce minimum size
211
+ if (newWidth >= 20 && newHeight >= 20) {
212
+ const updatedAreas = stampAreas.map(area => area.id === selectedStampId
213
+ ? { ...area, width: newWidth, height: newHeight }
214
+ : area);
215
+ onUpdate(updatedAreas);
216
+ }
217
+ event.preventDefault();
218
+ return;
219
+ }
220
+ // Handle rotation
221
+ if (isRotating && initialStamp && selectedStampId) {
222
+ const rect = canvas.getBoundingClientRect();
223
+ const mouseX = coords.clientX - rect.left;
224
+ const mouseY = coords.clientY - rect.top;
225
+ // Calculate current angle from center to mouse position
226
+ const currentAngle = Math.atan2(mouseY - rotationCenter.y, mouseX - rotationCenter.x) * (180 / Math.PI);
227
+ // Calculate angle delta and apply to initial rotation
228
+ const angleDelta = currentAngle - initialAngle;
229
+ const newRotation = initialRotation + angleDelta;
230
+ const updatedAreas = stampAreas.map(area => area.id === selectedStampId
231
+ ? { ...area, rotation: newRotation }
232
+ : area);
233
+ onUpdate(updatedAreas);
234
+ event.preventDefault();
235
+ return;
236
+ }
237
+ }
238
+ function handleMouseUp(event) {
239
+ isDragging = false;
240
+ isResizing = false;
241
+ isRotating = false;
242
+ isPanning = false;
243
+ resizeHandle = null;
244
+ initialStamp = null;
245
+ }
246
+ // Unified touch handlers
247
+ const handleCanvasTouchStart = handleCanvasMouseDown;
248
+ const handleTouchMove = handleMouseMove;
249
+ function handleTouchEnd(event) {
250
+ if (event.touches.length === 0) {
251
+ handleMouseUp();
252
+ }
253
+ }
254
+ function getResizeHandle(mouseX, mouseY, canvasStamp) {
255
+ const handles = getResizeHandles(canvasStamp);
256
+ for (const [handle, pos] of Object.entries(handles)) {
257
+ const dist = Math.hypot(mouseX - pos.x, mouseY - pos.y);
258
+ if (dist <= 8)
259
+ return handle;
260
+ }
261
+ return null;
262
+ }
263
+ function getResizeHandles(canvasStamp) {
264
+ const { canvasCenterX, canvasCenterY, canvasWidth, canvasHeight, rotation } = canvasStamp;
265
+ const hw = canvasWidth / 2;
266
+ const hh = canvasHeight / 2;
267
+ const rad = (rotation || 0) * Math.PI / 180;
268
+ const cos = Math.cos(rad);
269
+ const sin = Math.sin(rad);
270
+ const rotate = (x, y) => ({
271
+ x: canvasCenterX + x * cos - y * sin,
272
+ y: canvasCenterY + x * sin + y * cos
273
+ });
274
+ // Only return corner handles (4 corners)
275
+ return {
276
+ nw: rotate(-hw, -hh),
277
+ ne: rotate(hw, -hh),
278
+ sw: rotate(-hw, hh),
279
+ se: rotate(hw, hh)
280
+ };
281
+ }
282
+ function getRotationHandlePosition(canvasStamp) {
283
+ const handles = getResizeHandles(canvasStamp);
284
+ return {
285
+ x: handles.se.x,
286
+ y: handles.se.y
287
+ };
288
+ }
289
+ function isPointInStamp(mouseX, mouseY, canvasStamp) {
290
+ const { canvasCenterX, canvasCenterY, canvasWidth, canvasHeight, rotation } = canvasStamp;
291
+ const rad = -(rotation || 0) * Math.PI / 180;
292
+ const cos = Math.cos(rad);
293
+ const sin = Math.sin(rad);
294
+ const dx = mouseX - canvasCenterX;
295
+ const dy = mouseY - canvasCenterY;
296
+ const localX = dx * cos - dy * sin;
297
+ const localY = dx * sin + dy * cos;
298
+ return Math.abs(localX) <= canvasWidth / 2 && Math.abs(localY) <= canvasHeight / 2;
299
+ }
300
+ function handleDeleteStamp() {
301
+ if (selectedStampId) {
302
+ const updatedAreas = stampAreas.filter(area => area.id !== selectedStampId);
303
+ onUpdate(updatedAreas);
304
+ selectedStampId = null;
305
+ }
306
+ }
307
+ function selectStampAsset(asset) {
308
+ if (!canvas || !image)
309
+ return;
310
+ selectedStampAsset = asset;
311
+ // Calculate center position in image coordinates
312
+ const sourceWidth = cropArea ? cropArea.width : image.width;
313
+ const sourceHeight = cropArea ? cropArea.height : image.height;
314
+ const offsetX = cropArea ? cropArea.x : 0;
315
+ const offsetY = cropArea ? cropArea.y : 0;
316
+ // Center of the visible area (in image coordinates)
317
+ const centerX = sourceWidth / 2 + offsetX;
318
+ const centerY = sourceHeight / 2 + offsetY;
319
+ // Default size: based on the smaller dimension of the image
320
+ const minDimension = Math.min(sourceWidth, sourceHeight);
321
+ const defaultSize = minDimension * DEFAULT_STAMP_SIZE_PERCENT;
322
+ // For emojis, use 1:1 aspect ratio
323
+ if (asset.type === 'emoji') {
324
+ const aspectRatio = 1;
325
+ const newStamp = {
326
+ id: `stamp-${Date.now()}`,
327
+ x: centerX,
328
+ y: centerY,
329
+ width: defaultSize,
330
+ height: defaultSize / aspectRatio,
331
+ rotation: 0,
332
+ stampAssetId: asset.id,
333
+ stampType: asset.type,
334
+ stampContent: asset.content
335
+ };
336
+ onUpdate([...stampAreas, newStamp]);
337
+ selectedStampId = newStamp.id;
338
+ }
339
+ else {
340
+ // For images and SVGs, load the image first to get actual aspect ratio
341
+ preloadStampImage(asset.content).then((img) => {
342
+ const aspectRatio = img.width / img.height;
343
+ // Calculate width and height based on aspect ratio
344
+ // Use defaultSize as the width for landscape, height for portrait
345
+ let width, height;
346
+ if (aspectRatio >= 1) {
347
+ // Landscape or square
348
+ width = defaultSize;
349
+ height = width / aspectRatio;
350
+ }
351
+ else {
352
+ // Portrait
353
+ height = defaultSize;
354
+ width = height * aspectRatio;
355
+ }
356
+ const newStamp = {
357
+ id: `stamp-${Date.now()}`,
358
+ x: centerX,
359
+ y: centerY,
360
+ width,
361
+ height,
362
+ rotation: 0,
363
+ stampAssetId: asset.id,
364
+ stampType: asset.type,
365
+ stampContent: asset.content
366
+ };
367
+ onUpdate([...stampAreas, newStamp]);
368
+ selectedStampId = newStamp.id;
369
+ }).catch((error) => {
370
+ console.error('Failed to load stamp image:', error);
371
+ // Fallback to square aspect ratio on error
372
+ const aspectRatio = 1;
373
+ const newStamp = {
374
+ id: `stamp-${Date.now()}`,
375
+ x: centerX,
376
+ y: centerY,
377
+ width: defaultSize,
378
+ height: defaultSize / aspectRatio,
379
+ rotation: 0,
380
+ stampAssetId: asset.id,
381
+ stampType: asset.type,
382
+ stampContent: asset.content
383
+ };
384
+ onUpdate([...stampAreas, newStamp]);
385
+ selectedStampId = newStamp.id;
386
+ });
387
+ }
388
+ }
389
+ </script>
390
+
391
+ <svelte:window
392
+ onmousemove={handleMouseMove}
393
+ onmouseup={handleMouseUp}
394
+ />
395
+
396
+ <div class="stamp-tool">
397
+ <div class="stamp-palette">
398
+ <h3>{$_('editor.selectStamp') || 'Select Stamp'}</h3>
399
+ <div class="stamp-grid">
400
+ {#each STAMP_ASSETS as asset}
401
+ <button
402
+ class="stamp-item"
403
+ class:selected={selectedStampAsset?.id === asset.id}
404
+ onclick={() => selectStampAsset(asset)}
405
+ title={asset.id}
406
+ >
407
+ {#if asset.type === 'emoji'}
408
+ <span class="emoji">{asset.content}</span>
409
+ {:else}
410
+ <img src={asset.preview || asset.content} alt={asset.id} />
411
+ {/if}
412
+ </button>
413
+ {/each}
414
+ </div>
415
+ </div>
416
+
417
+ <div
418
+ bind:this={overlayElement}
419
+ class="stamp-canvas-overlay"
420
+ onmousedown={handleCanvasMouseDown}
421
+ role="button"
422
+ tabindex="-1"
423
+ >
424
+
425
+ <!-- Render stamp selection boxes -->
426
+ {#if canvas}
427
+ <svg class="stamp-svg">
428
+ {#each canvasStampAreas as canvasStamp}
429
+ {@const isSelected = selectedStampId === canvasStamp.id}
430
+ {@const handles = getResizeHandles(canvasStamp)}
431
+ {@const rotHandle = getRotationHandlePosition(canvasStamp)}
432
+
433
+ <g transform="rotate({canvasStamp.rotation || 0} {canvasStamp.canvasCenterX} {canvasStamp.canvasCenterY})">
434
+ <rect
435
+ x={canvasStamp.canvasCenterX - canvasStamp.canvasWidth / 2}
436
+ y={canvasStamp.canvasCenterY - canvasStamp.canvasHeight / 2}
437
+ width={canvasStamp.canvasWidth}
438
+ height={canvasStamp.canvasHeight}
439
+ fill="none"
440
+ stroke={isSelected ? 'var(--primary-color, #63b97b)' : '#ffffff'}
441
+ stroke-width="2"
442
+ stroke-dasharray={isSelected ? '0' : '5,5'}
443
+ />
444
+ </g>
445
+
446
+ {#if isSelected}
447
+ <!-- Resize handles (nw, ne, sw only - se is for rotation) -->
448
+ {#each ['nw', 'ne', 'sw'] as handleKey}
449
+ {@const handle = handles[handleKey]}
450
+ {@const cursor = handleKey === 'nw' ? 'nwse-resize' : 'nesw-resize'}
451
+ <circle
452
+ cx={handle.x}
453
+ cy={handle.y}
454
+ r="6"
455
+ fill="var(--primary-color, #63b97b)"
456
+ stroke="#fff"
457
+ stroke-width="2"
458
+ style="pointer-events: all; cursor: {cursor};"
459
+ />
460
+ {/each}
461
+
462
+ <!-- Rotation handle (se corner) -->
463
+ <circle
464
+ cx={rotHandle.x}
465
+ cy={rotHandle.y}
466
+ r="8"
467
+ fill="#00cc00"
468
+ stroke="#fff"
469
+ stroke-width="2"
470
+ style="pointer-events: all; cursor: grab;"
471
+ />
472
+ <g transform="translate({rotHandle.x}, {rotHandle.y})">
473
+ <foreignObject x="-8" y="-8" width="16" height="16" style="pointer-events: none;">
474
+ <div style="color: white; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center;">
475
+ <RotateCw size={12} />
476
+ </div>
477
+ </foreignObject>
478
+ </g>
479
+ {/if}
480
+ {/each}
481
+ </svg>
482
+ {/if}
483
+ </div>
484
+
485
+ <div class="stamp-controls">
486
+ {#if selectedStampId}
487
+ <button class="control-btn delete" onclick={handleDeleteStamp}>
488
+ <Trash2 size={16} />
489
+ <span>{$_('editor.delete')}</span>
490
+ </button>
491
+ {/if}
492
+ <button class="control-btn" onclick={onClose}>
493
+ {$_('editor.close')}
494
+ </button>
495
+ </div>
496
+ </div>
497
+
498
+ <style>
499
+ .stamp-tool {
500
+ position: absolute;
501
+ top: 0;
502
+ left: 0;
503
+ width: 100%;
504
+ height: 100%;
505
+ pointer-events: none;
506
+ }
507
+
508
+ @media (max-width: 767px) {
509
+
510
+ }
511
+
512
+ .stamp-palette {
513
+ position: absolute;
514
+ top: 1rem;
515
+ right: 1rem;
516
+ background: rgba(30, 30, 30, 0.95);
517
+ border: 1px solid #444;
518
+ border-radius: 8px;
519
+ padding: 1rem;
520
+ width: 280px;
521
+ max-height: 400px;
522
+ overflow-y: auto;
523
+ pointer-events: all;
524
+ backdrop-filter: blur(10px);
525
+ z-index: 1;
526
+ }
527
+
528
+ @media (max-width: 767px) {
529
+
530
+ .stamp-palette {
531
+ top: auto;
532
+ bottom: 0;
533
+ right: 0;
534
+ left: 0;
535
+ width: auto
536
+ }
537
+ }
538
+
539
+ .stamp-palette h3 {
540
+ margin: 0 0 1rem 0;
541
+ font-size: 1rem;
542
+ color: #fff;
543
+ }
544
+
545
+ @media (max-width: 767px) {
546
+
547
+ .stamp-palette h3 {
548
+ display: none
549
+ }
550
+ }
551
+
552
+ .stamp-grid {
553
+ display: grid;
554
+ grid-template-columns: repeat(4, 1fr);
555
+ gap: 0.5rem;
556
+ }
557
+
558
+ @media (max-width: 767px) {
559
+
560
+ .stamp-grid {
561
+ display: flex
562
+ }
563
+ }
564
+
565
+ .stamp-item {
566
+ width: 60px;
567
+ height: 60px;
568
+ background: #333;
569
+ border: 2px solid #444;
570
+ border-radius: 4px;
571
+ cursor: pointer;
572
+ transition: all 0.2s;
573
+ display: flex;
574
+ align-items: center;
575
+ justify-content: center;
576
+ padding: 0;
577
+ }
578
+
579
+ @media (max-width: 767px) {
580
+
581
+ .stamp-item {
582
+ flex-shrink: 0
583
+ }
584
+ }
585
+
586
+ .stamp-item:hover {
587
+ background: #444;
588
+ border-color: #666;
589
+ }
590
+
591
+ .stamp-item.selected {
592
+ background: var(--primary-color, #63b97b);
593
+ border-color: var(--primary-color, #63b97b);
594
+ }
595
+
596
+ .stamp-item .emoji {
597
+ font-size: 2rem;
598
+ }
599
+
600
+ .stamp-item img {
601
+ max-width: 90%;
602
+ max-height: 90%;
603
+ }
604
+
605
+ .stamp-canvas-overlay {
606
+ position: absolute;
607
+ top: 0;
608
+ left: 0;
609
+ width: 100%;
610
+ height: 100%;
611
+ pointer-events: all;
612
+ user-select: none;
613
+ cursor: grab;
614
+ }
615
+
616
+ .stamp-canvas-overlay:active {
617
+ cursor: grabbing;
618
+ }
619
+
620
+ .stamp-svg {
621
+ width: 100%;
622
+ height: 100%;
623
+ pointer-events: none;
624
+ }
625
+
626
+ .stamp-svg circle,
627
+ .stamp-svg rect {
628
+ pointer-events: all;
629
+ }
630
+
631
+ .stamp-controls {
632
+ position: absolute;
633
+ bottom: 20px;
634
+ right: 20px;
635
+ display: flex;
636
+ gap: 0.5rem;
637
+ pointer-events: all;
638
+ }
639
+
640
+ .control-btn {
641
+ display: flex;
642
+ align-items: center;
643
+ gap: 0.5rem;
644
+ padding: 0.5rem 1rem;
645
+ background: #333;
646
+ color: #fff;
647
+ border: 1px solid #444;
648
+ border-radius: 4px;
649
+ cursor: pointer;
650
+ transition: all 0.2s;
651
+ }
652
+
653
+ .control-btn:hover {
654
+ background: #444;
655
+ border-color: #555;
656
+ }
657
+
658
+ .control-btn.delete {
659
+ background: #cc0000;
660
+ border-color: #dd0000;
661
+ }
662
+
663
+ .control-btn.delete:hover {
664
+ background: #dd0000;
665
+ border-color: #ee0000;
666
+ }
667
+
668
+ /* Larger touch targets for mobile */
669
+ @media (max-width: 767px) {
670
+ .stamp-svg circle {
671
+ r: 12 !important;
672
+ stroke-width: 3 !important;
673
+ }
674
+
675
+ .stamp-svg circle[fill="#00cc00"] {
676
+ r: 14 !important;
677
+ }
678
+ }</style>
@@ -0,0 +1,15 @@
1
+ import type { StampArea, Viewport, TransformState, CropArea } from '../types';
2
+ interface Props {
3
+ canvas: HTMLCanvasElement | null;
4
+ image: HTMLImageElement | null;
5
+ viewport: Viewport;
6
+ transform: TransformState;
7
+ stampAreas: StampArea[];
8
+ cropArea?: CropArea | null;
9
+ onUpdate: (stampAreas: StampArea[]) => void;
10
+ onClose: () => void;
11
+ onViewportChange?: (viewport: Partial<Viewport>) => void;
12
+ }
13
+ declare const StampTool: import("svelte").Component<Props, {}, "">;
14
+ type StampTool = ReturnType<typeof StampTool>;
15
+ export default StampTool;