tokimeki-image-editor 0.2.0 → 0.2.2

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.
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">import { _ } from 'svelte-i18n';
2
- import { X } from 'lucide-svelte';
2
+ import ToolPanel from './ToolPanel.svelte';
3
3
  let { adjustments, onChange, onClose } = $props();
4
4
  function handleChange(key, value) {
5
5
  onChange({ [key]: value });
@@ -27,14 +27,9 @@ function handleWheel(e) {
27
27
  </script>
28
28
 
29
29
  <div class="adjust-tool" onwheel={handleWheel}>
30
- <div class="tool-header">
31
- <h3>{$_('editor.adjust')}</h3>
32
- <button class="close-btn" onclick={onClose} title={$_('editor.close')}>
33
- <X size={20} />
34
- </button>
35
- </div>
36
-
37
- <div class="adjustments-grid">
30
+ <ToolPanel title={$_('editor.adjust')} {onClose}>
31
+ {#snippet children()}
32
+ <div class="adjustments-grid">
38
33
  <!-- Exposure -->
39
34
  <div class="adjustment-control">
40
35
  <label for="exposure">
@@ -194,60 +189,25 @@ function handleWheel(e) {
194
189
  oninput={(e) => handleChange('grain', Number(e.currentTarget.value))}
195
190
  />
196
191
  </div>
197
- </div>
198
-
199
- <div class="tool-actions">
200
- <button class="btn btn-secondary" onclick={resetAll}>
201
- {$_('editor.reset')}
202
- </button>
203
- </div>
192
+ </div>
193
+ {/snippet}
194
+
195
+ {#snippet actions()}
196
+ <button class="btn btn-secondary" onclick={resetAll}>
197
+ {$_('editor.reset')}
198
+ </button>
199
+ {/snippet}
200
+ </ToolPanel>
204
201
  </div>
205
202
 
206
203
  <style>
207
- .adjust-tool {
208
- display: flex;
209
- flex-direction: column;
210
- gap: 1rem;
211
- }
212
-
213
- .tool-header {
214
- display: flex;
215
- justify-content: space-between;
216
- align-items: center;
217
- }
218
-
219
- .tool-header h3 {
220
- margin: 0;
221
- font-size: 1.1rem;
222
- color: #fff;
223
- }
224
-
225
- .close-btn {
226
- display: flex;
227
- align-items: center;
228
- justify-content: center;
229
- padding: 0.25rem;
230
- background: transparent;
231
- border: none;
232
- color: #999;
233
- cursor: pointer;
234
- border-radius: 4px;
235
- transition: all 0.2s;
236
- }
237
-
238
- .close-btn:hover {
239
- background: #444;
240
- color: #fff;
241
- }
242
-
243
204
  .adjustments-grid {
244
205
  display: grid;
245
206
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
246
207
  gap: 1rem;
247
208
  }
248
209
 
249
- @media (max-width: 767px) {
250
-
210
+ @media (max-width: 767px) {
251
211
  .adjustments-grid {
252
212
  grid-template-columns: 1fr;
253
213
  gap: 0.75rem;
@@ -330,12 +290,6 @@ function handleWheel(e) {
330
290
  transform: scale(1.1);
331
291
  }
332
292
 
333
- .tool-actions {
334
- display: flex;
335
- justify-content: flex-end;
336
- gap: 0.5rem;
337
- }
338
-
339
293
  .btn {
340
294
  padding: 0.5rem 1rem;
341
295
  border: none;
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">import { onMount } from 'svelte';
2
2
  import { _ } from 'svelte-i18n';
3
3
  import { screenToImageCoords } from '../utils/canvas';
4
- import { X, Pencil, Eraser, ArrowRight, Square } from 'lucide-svelte';
4
+ import { Pencil, Eraser, ArrowRight, Square } from 'lucide-svelte';
5
+ import ToolPanel from './ToolPanel.svelte';
5
6
  let { canvas, image, viewport, transform, annotations, cropArea, onUpdate, onClose, onViewportChange } = $props();
6
7
  let containerElement = $state(null);
7
8
  // Tool settings
@@ -14,6 +15,10 @@ const colorPresets = ['#FF6B6B', '#FFA94D', '#FFD93D', '#6BCB77', '#4D96FF', '#9
14
15
  // Drawing state
15
16
  let isDrawing = $state(false);
16
17
  let currentAnnotation = $state(null);
18
+ // Panning state (Space + drag)
19
+ let isSpaceHeld = $state(false);
20
+ let isPanning = $state(false);
21
+ let panStart = $state(null);
17
22
  // Helper to get coordinates from mouse or touch event
18
23
  function getEventCoords(event) {
19
24
  if ('touches' in event && event.touches.length > 0) {
@@ -82,16 +87,41 @@ onMount(() => {
82
87
  }
83
88
  };
84
89
  });
90
+ // Keyboard handlers for panning (Space + drag)
91
+ function handleKeyDown(event) {
92
+ if (event.code === 'Space' && !isSpaceHeld) {
93
+ isSpaceHeld = true;
94
+ event.preventDefault();
95
+ }
96
+ }
97
+ function handleKeyUp(event) {
98
+ if (event.code === 'Space') {
99
+ isSpaceHeld = false;
100
+ isPanning = false;
101
+ panStart = null;
102
+ }
103
+ }
85
104
  function handleMouseDown(event) {
86
105
  if (!canvas || !image)
87
106
  return;
88
107
  if ('button' in event && event.button !== 0)
89
108
  return;
90
109
  const coords = getEventCoords(event);
110
+ event.preventDefault();
111
+ // Start panning if space is held
112
+ if (isSpaceHeld) {
113
+ isPanning = true;
114
+ panStart = {
115
+ x: coords.clientX,
116
+ y: coords.clientY,
117
+ offsetX: viewport.offsetX,
118
+ offsetY: viewport.offsetY
119
+ };
120
+ return;
121
+ }
91
122
  const imagePoint = toImageCoords(coords.clientX, coords.clientY);
92
123
  if (!imagePoint)
93
124
  return;
94
- event.preventDefault();
95
125
  if (currentTool === 'eraser') {
96
126
  // Find and remove annotation at click point
97
127
  const canvasPoint = { x: coords.clientX, y: coords.clientY };
@@ -136,9 +166,20 @@ function handleMouseDown(event) {
136
166
  // This absorbs micro-movements of the mouse
137
167
  const MIN_POINT_DISTANCE = 3;
138
168
  function handleMouseMove(event) {
169
+ const coords = getEventCoords(event);
170
+ // Handle panning
171
+ if (isPanning && panStart && onViewportChange) {
172
+ event.preventDefault();
173
+ const dx = coords.clientX - panStart.x;
174
+ const dy = coords.clientY - panStart.y;
175
+ onViewportChange({
176
+ offsetX: panStart.offsetX + dx,
177
+ offsetY: panStart.offsetY + dy
178
+ });
179
+ return;
180
+ }
139
181
  if (!isDrawing || !currentAnnotation || !canvas || !image)
140
182
  return;
141
- const coords = getEventCoords(event);
142
183
  const imagePoint = toImageCoords(coords.clientX, coords.clientY);
143
184
  if (!imagePoint)
144
185
  return;
@@ -165,6 +206,12 @@ function handleMouseMove(event) {
165
206
  }
166
207
  }
167
208
  function handleMouseUp(event) {
209
+ // Stop panning
210
+ if (isPanning) {
211
+ isPanning = false;
212
+ panStart = null;
213
+ return;
214
+ }
168
215
  if (!isDrawing || !currentAnnotation) {
169
216
  isDrawing = false;
170
217
  return;
@@ -295,12 +342,15 @@ let currentAnnotationCanvas = $derived.by(() => {
295
342
  <svelte:window
296
343
  onmousemove={handleMouseMove}
297
344
  onmouseup={handleMouseUp}
345
+ onkeydown={handleKeyDown}
346
+ onkeyup={handleKeyUp}
298
347
  />
299
348
 
300
349
  <!-- Overlay -->
301
350
  <div
302
351
  bind:this={containerElement}
303
352
  class="annotation-tool-overlay"
353
+ class:panning={isSpaceHeld}
304
354
  onmousedown={handleMouseDown}
305
355
  role="button"
306
356
  tabindex="-1"
@@ -442,15 +492,8 @@ let currentAnnotationCanvas = $derived.by(() => {
442
492
  </div>
443
493
 
444
494
  <!-- Control panel -->
445
- <div class="annotation-tool-panel">
446
- <div class="panel-header">
447
- <h3>{$_('editor.annotate')}</h3>
448
- <button class="close-btn" onclick={onClose} title={$_('editor.close')}>
449
- <X size={20} />
450
- </button>
451
- </div>
452
-
453
- <div class="panel-content">
495
+ <ToolPanel title={$_('editor.annotate')} {onClose}>
496
+ {#snippet children()}
454
497
  <!-- Tool selection -->
455
498
  <div class="tool-group">
456
499
  <span class="group-label">{$_('annotate.tool')}</span>
@@ -544,15 +587,14 @@ let currentAnnotationCanvas = $derived.by(() => {
544
587
  </button>
545
588
  </label>
546
589
  </div>
590
+ {/snippet}
547
591
 
548
- <!-- Actions -->
549
- <div class="panel-actions">
550
- <button class="btn btn-danger" onclick={handleClearAll} disabled={annotations.length === 0}>
551
- {$_('annotate.clearAll')}
552
- </button>
553
- </div>
554
- </div>
555
- </div>
592
+ {#snippet actions()}
593
+ <button class="btn btn-danger" onclick={handleClearAll} disabled={annotations.length === 0}>
594
+ {$_('annotate.clearAll')}
595
+ </button>
596
+ {/snippet}
597
+ </ToolPanel>
556
598
 
557
599
  <style>
558
600
  .annotation-tool-overlay {
@@ -565,90 +607,51 @@ let currentAnnotationCanvas = $derived.by(() => {
565
607
  user-select: none;
566
608
  }
567
609
 
610
+ .annotation-tool-overlay.panning {
611
+ cursor: grab;
612
+ }
613
+
614
+ .annotation-tool-overlay.panning:active {
615
+ cursor: grabbing;
616
+ }
617
+
568
618
  .annotation-tool-svg {
569
619
  width: 100%;
570
620
  height: 100%;
571
621
  pointer-events: none;
572
622
  }
573
623
 
574
- .annotation-tool-panel {
575
- position: absolute;
576
- top: 1rem;
577
- right: 1rem;
578
- background: rgba(30, 30, 30, 0.95);
579
- border: 1px solid #444;
580
- border-radius: 8px;
581
- padding: 1rem;
582
- min-width: 250px;
583
- backdrop-filter: blur(10px);
584
- }
585
-
586
- @media (max-width: 767px) {
587
-
588
- .annotation-tool-panel {
589
- position: absolute;
590
- left: 0;
591
- right: 0;
592
- top: auto;
593
- bottom: 0;
594
- width: auto;
595
- min-width: auto;
596
- max-height: 50vh;
597
- border-radius: 16px 16px 0 0;
598
- z-index: 1001;
599
- overflow-y: auto
600
- }
601
- }
602
-
603
- .panel-header {
604
- display: flex;
605
- justify-content: space-between;
606
- align-items: center;
607
- margin-bottom: 1rem;
608
- }
609
-
610
- .panel-header h3 {
611
- margin: 0;
612
- font-size: 1.1rem;
613
- color: #fff;
614
- }
615
-
616
- .close-btn {
617
- display: flex;
618
- align-items: center;
619
- justify-content: center;
620
- padding: 0.25rem;
621
- background: transparent;
622
- border: none;
623
- color: #999;
624
- cursor: pointer;
625
- border-radius: 4px;
626
- transition: all 0.2s;
627
- }
628
-
629
- .close-btn:hover {
630
- background: #444;
631
- color: #fff;
632
- }
633
-
634
- .panel-content {
624
+ .tool-group,
625
+ .control-group {
635
626
  display: flex;
636
627
  flex-direction: column;
637
- gap: 1rem;
628
+ gap: 0.5rem;
638
629
  }
639
630
 
631
+ @media (max-width: 767px) {
632
+
640
633
  .tool-group,
641
634
  .control-group {
642
- display: flex;
643
- flex-direction: column;
644
- gap: 0.5rem;
635
+ flex-direction: row;
636
+ align-items: center;
637
+ gap: 0.75rem
645
638
  }
639
+ }
646
640
 
647
641
  .group-label {
648
642
  font-size: 0.9rem;
649
643
  color: #ccc;
650
644
  }
651
645
 
646
+ @media (max-width: 767px) {
647
+
648
+ .group-label {
649
+ font-size: 0.75rem;
650
+ white-space: nowrap;
651
+ min-width: 50px
652
+ }
653
+ }
654
+
652
655
  .tool-buttons {
653
656
  display: flex;
654
657
  gap: 0.5rem;
@@ -731,8 +734,15 @@ let currentAnnotationCanvas = $derived.by(() => {
731
734
  color: #ccc;
732
735
  }
733
736
 
737
+ @media (max-width: 767px) {
738
+
739
+ .control-group label {
740
+ font-size: 0.75rem;
741
+ gap: 0.5rem
742
+ }
743
+ }
744
+
734
745
  .control-group .value {
735
- color: var(--primary-color, #63b97b);
736
746
  font-weight: 600;
737
747
  }
738
748
 
@@ -745,6 +755,14 @@ let currentAnnotationCanvas = $derived.by(() => {
745
755
  cursor: pointer;
746
756
  }
747
757
 
758
+ @media (max-width: 767px) {
759
+
760
+ .control-group input[type='range'] {
761
+ width: 80px;
762
+ flex-shrink: 0
763
+ }
764
+ }
765
+
748
766
  .control-group input[type='range']::-webkit-slider-thumb {
749
767
  appearance: none;
750
768
  width: 16px;
@@ -769,13 +787,6 @@ let currentAnnotationCanvas = $derived.by(() => {
769
787
  transition: all 0.2s;
770
788
  }
771
789
 
772
- .panel-actions {
773
- display: flex;
774
- justify-content: flex-end;
775
- gap: 0.5rem;
776
- margin-top: 0.5rem;
777
- }
778
-
779
790
  .btn {
780
791
  padding: 0.5rem 1rem;
781
792
  border: none;
@@ -785,6 +796,14 @@ let currentAnnotationCanvas = $derived.by(() => {
785
796
  transition: all 0.2s;
786
797
  }
787
798
 
799
+ @media (max-width: 767px) {
800
+
801
+ .btn {
802
+ padding: 0.4rem 0.75rem;
803
+ font-size: 0.75rem
804
+ }
805
+ }
806
+
788
807
  .btn:disabled {
789
808
  opacity: 0.5;
790
809
  cursor: not-allowed;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">import { onMount } from 'svelte';
2
2
  import { _ } from 'svelte-i18n';
3
3
  import { screenToImageCoords, imageToCanvasCoords } from '../utils/canvas';
4
- import { X } from 'lucide-svelte';
4
+ import ToolPanel from './ToolPanel.svelte';
5
5
  let { canvas, image, viewport, transform, blurAreas, cropArea, onUpdate, onClose, onViewportChange } = $props();
6
6
  let containerElement = $state(null);
7
7
  // Helper to get coordinates from mouse or touch event
@@ -402,16 +402,9 @@ function handleTouchEnd(event) {
402
402
  </div>
403
403
 
404
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">
405
+ <ToolPanel title={$_('editor.blur')} {onClose}>
406
+ {#snippet children()}
407
+ {#if selectedArea}
415
408
  <div class="control-group">
416
409
  <label for="blur-strength">
417
410
  <span>{$_('blur.strength')}</span>
@@ -426,19 +419,21 @@ function handleTouchEnd(event) {
426
419
  oninput={(e) => handleBlurStrengthChange(Number(e.currentTarget.value))}
427
420
  />
428
421
  </div>
429
-
430
- <div class="panel-actions">
431
- <button class="btn btn-danger" onclick={handleDeleteArea}>
432
- {$_('editor.delete')}
433
- </button>
422
+ {:else}
423
+ <div class="panel-hint">
424
+ <p>{$_('blur.hint')}</p>
434
425
  </div>
435
- </div>
436
- {:else}
437
- <div class="panel-hint">
438
- <p>{$_('blur.hint')}</p>
439
- </div>
440
- {/if}
441
- </div>
426
+ {/if}
427
+ {/snippet}
428
+
429
+ {#snippet actions()}
430
+ {#if selectedArea}
431
+ <button class="btn btn-danger" onclick={handleDeleteArea}>
432
+ {$_('editor.delete')}
433
+ </button>
434
+ {/if}
435
+ {/snippet}
436
+ </ToolPanel>
442
437
 
443
438
  <style>
444
439
  .blur-tool-overlay {
@@ -461,55 +456,6 @@ function handleTouchEnd(event) {
461
456
  pointer-events: all;
462
457
  }
463
458
 
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
459
  .control-group {
514
460
  display: flex;
515
461
  flex-direction: column;
@@ -568,12 +514,6 @@ function handleTouchEnd(event) {
568
514
  transform: scale(1.1);
569
515
  }
570
516
 
571
- .panel-actions {
572
- display: flex;
573
- justify-content: flex-end;
574
- gap: 0.5rem;
575
- }
576
-
577
517
  .btn {
578
518
  padding: 0.5rem 1rem;
579
519
  border: none;