tokimeki-image-editor 0.2.2 → 0.2.4

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.
@@ -15,10 +15,11 @@ const colorPresets = ['#FF6B6B', '#FFA94D', '#FFD93D', '#6BCB77', '#4D96FF', '#9
15
15
  // Drawing state
16
16
  let isDrawing = $state(false);
17
17
  let currentAnnotation = $state(null);
18
- // Panning state (Space + drag)
18
+ // Panning state (Space + drag on desktop, 2-finger drag on mobile)
19
19
  let isSpaceHeld = $state(false);
20
20
  let isPanning = $state(false);
21
21
  let panStart = $state(null);
22
+ let isTwoFingerTouch = $state(false);
22
23
  // Helper to get coordinates from mouse or touch event
23
24
  function getEventCoords(event) {
24
25
  if ('touches' in event && event.touches.length > 0) {
@@ -289,12 +290,79 @@ function pointToSegmentDistance(px, py, a, b) {
289
290
  function handleClearAll() {
290
291
  onUpdate([]);
291
292
  }
292
- const handleTouchStart = handleMouseDown;
293
- const handleTouchMove = handleMouseMove;
293
+ function handleTouchStart(event) {
294
+ // Two-finger touch starts panning
295
+ if (event.touches.length === 2) {
296
+ event.preventDefault();
297
+ isTwoFingerTouch = true;
298
+ // Use the midpoint of the two touches
299
+ const touch1 = event.touches[0];
300
+ const touch2 = event.touches[1];
301
+ const midX = (touch1.clientX + touch2.clientX) / 2;
302
+ const midY = (touch1.clientY + touch2.clientY) / 2;
303
+ // Cancel any current drawing
304
+ if (isDrawing) {
305
+ isDrawing = false;
306
+ currentAnnotation = null;
307
+ }
308
+ isPanning = true;
309
+ panStart = {
310
+ x: midX,
311
+ y: midY,
312
+ offsetX: viewport.offsetX,
313
+ offsetY: viewport.offsetY
314
+ };
315
+ return;
316
+ }
317
+ // Single finger - normal drawing (only if not already in two-finger mode)
318
+ if (event.touches.length === 1 && !isTwoFingerTouch) {
319
+ handleMouseDown(event);
320
+ }
321
+ }
322
+ function handleTouchMove(event) {
323
+ // Two-finger panning
324
+ if (event.touches.length === 2 && isPanning && panStart && onViewportChange) {
325
+ event.preventDefault();
326
+ const touch1 = event.touches[0];
327
+ const touch2 = event.touches[1];
328
+ const midX = (touch1.clientX + touch2.clientX) / 2;
329
+ const midY = (touch1.clientY + touch2.clientY) / 2;
330
+ const dx = midX - panStart.x;
331
+ const dy = midY - panStart.y;
332
+ onViewportChange({
333
+ offsetX: panStart.offsetX + dx,
334
+ offsetY: panStart.offsetY + dy
335
+ });
336
+ return;
337
+ }
338
+ // Single finger drawing (only if not in two-finger mode)
339
+ if (event.touches.length === 1 && !isTwoFingerTouch) {
340
+ handleMouseMove(event);
341
+ }
342
+ }
294
343
  function handleTouchEnd(event) {
344
+ // When all fingers are lifted
295
345
  if (event.touches.length === 0) {
346
+ if (isPanning) {
347
+ isPanning = false;
348
+ panStart = null;
349
+ }
350
+ isTwoFingerTouch = false;
296
351
  handleMouseUp();
297
352
  }
353
+ // When going from 2 fingers to 1, stay in pan mode but don't draw
354
+ else if (event.touches.length === 1 && isTwoFingerTouch) {
355
+ // Update pan start to the remaining finger position
356
+ if (isPanning && onViewportChange) {
357
+ const touch = event.touches[0];
358
+ panStart = {
359
+ x: touch.clientX,
360
+ y: touch.clientY,
361
+ offsetX: viewport.offsetX,
362
+ offsetY: viewport.offsetY
363
+ };
364
+ }
365
+ }
298
366
  }
299
367
  // Generate smooth SVG path using quadratic bezier curves
300
368
  function generateSmoothPath(points) {
@@ -350,7 +418,7 @@ let currentAnnotationCanvas = $derived.by(() => {
350
418
  <div
351
419
  bind:this={containerElement}
352
420
  class="annotation-tool-overlay"
353
- class:panning={isSpaceHeld}
421
+ class:panning={isSpaceHeld || isTwoFingerTouch}
354
422
  onmousedown={handleMouseDown}
355
423
  role="button"
356
424
  tabindex="-1"
@@ -553,8 +553,14 @@ function handleKeyDown(event) {
553
553
  display: flex;
554
554
  align-items: center;
555
555
  width: 100%;
556
+ overflow-x: auto;
557
+ scrollbar-width: none;
556
558
  }
557
559
 
560
+ .editor-header::-webkit-scrollbar {
561
+ display: none;
562
+ }
563
+
558
564
  .editor-body {
559
565
  display: flex;
560
566
  flex-direction: column;
@@ -1,36 +1,39 @@
1
1
  <script lang="ts">import { _ } from 'svelte-i18n';
2
2
  import { X } from 'lucide-svelte';
3
3
  let { title, onClose, children, actions } = $props();
4
- // Bottom sheet state (mobile)
5
- let sheetHeight = $state(180);
4
+ // Bottom sheet state (mobile) - using transform for GPU acceleration
5
+ const SHEET_MAX_HEIGHT = 400;
6
+ const SHEET_MIN_VISIBLE = 60;
7
+ let sheetOffset = $state(SHEET_MAX_HEIGHT - 180); // How much to hide (0 = fully shown)
6
8
  let isSheetDragging = $state(false);
7
9
  let sheetDragStart = $state(null);
8
- const SHEET_MIN_HEIGHT = 60;
9
- const SHEET_MAX_HEIGHT = 400;
10
+ let panelElement = $state(null);
10
11
  function getEventCoords(event) {
11
12
  if ('touches' in event && event.touches.length > 0) {
12
- return { clientX: event.touches[0].clientX, clientY: event.touches[0].clientY };
13
+ return { clientY: event.touches[0].clientY };
13
14
  }
14
- else if ('clientX' in event) {
15
- return { clientX: event.clientX, clientY: event.clientY };
15
+ else if ('clientY' in event) {
16
+ return { clientY: event.clientY };
16
17
  }
17
- return { clientX: 0, clientY: 0 };
18
+ return { clientY: 0 };
18
19
  }
19
20
  function handleSheetDragStart(event) {
20
21
  event.preventDefault();
21
22
  event.stopPropagation();
22
23
  const coords = getEventCoords(event);
23
24
  isSheetDragging = true;
24
- sheetDragStart = { y: coords.clientY, height: sheetHeight };
25
+ sheetDragStart = { y: coords.clientY, offset: sheetOffset };
25
26
  }
26
27
  function handleSheetDragMove(event) {
27
28
  if (!isSheetDragging || !sheetDragStart)
28
29
  return;
29
30
  event.preventDefault();
30
31
  const coords = getEventCoords(event);
31
- const deltaY = sheetDragStart.y - coords.clientY;
32
- const newHeight = Math.max(SHEET_MIN_HEIGHT, Math.min(SHEET_MAX_HEIGHT, sheetDragStart.height + deltaY));
33
- sheetHeight = newHeight;
32
+ const deltaY = coords.clientY - sheetDragStart.y;
33
+ // Clamp offset: 0 (fully visible) to (SHEET_MAX_HEIGHT - SHEET_MIN_VISIBLE)
34
+ const maxOffset = SHEET_MAX_HEIGHT - SHEET_MIN_VISIBLE;
35
+ const newOffset = Math.max(0, Math.min(maxOffset, sheetDragStart.offset + deltaY));
36
+ sheetOffset = newOffset;
34
37
  }
35
38
  function handleSheetDragEnd() {
36
39
  isSheetDragging = false;
@@ -46,8 +49,9 @@ function handleSheetDragEnd() {
46
49
  />
47
50
 
48
51
  <div
52
+ bind:this={panelElement}
49
53
  class="tool-panel"
50
- style="--sheet-height: {sheetHeight}px"
54
+ style="--sheet-offset: {sheetOffset}px; --sheet-max-height: {SHEET_MAX_HEIGHT}px"
51
55
  >
52
56
  <!-- Drag handle for mobile bottom sheet -->
53
57
  <div
@@ -99,20 +103,23 @@ function handleSheetDragEnd() {
99
103
 
100
104
  @media (max-width: 767px) {
101
105
  .tool-panel {
102
- position: absolute;
106
+ position: fixed;
103
107
  left: 0;
104
108
  right: 0;
105
109
  top: auto;
106
110
  bottom: 0;
107
111
  width: auto;
108
112
  min-width: auto;
109
- height: var(--sheet-height, 180px);
110
- max-height: 70vh;
113
+ height: var(--sheet-max-height, 400px);
111
114
  border-radius: 16px 16px 0 0;
112
- z-index: 1001;
115
+ z-index: 9999;
113
116
  overflow-y: auto;
117
+ overscroll-behavior: contain;
114
118
  padding-top: 0;
115
- transition: height 0.05s ease-out
119
+ padding-bottom: 1.5rem;
120
+ transform: translateY(var(--sheet-offset, 0px));
121
+ will-change: transform;
122
+ backdrop-filter: none
116
123
  }
117
124
  }
118
125
 
@@ -128,7 +135,9 @@ function handleSheetDragEnd() {
128
135
  align-items: center;
129
136
  padding: 12px 0 8px;
130
137
  cursor: grab;
131
- touch-action: none
138
+ touch-action: pan-x;
139
+ user-select: none;
140
+ -webkit-user-select: none
132
141
  }
133
142
 
134
143
  .sheet-drag-handle:active {
@@ -122,13 +122,20 @@ let { mode, hasImage, canUndo, canRedo, isStandalone = false, onModeChange, onUn
122
122
  align-items: center;
123
123
  justify-content: center;
124
124
  overflow-x: auto;
125
+ scrollbar-width: none;
125
126
  }
126
127
 
128
+ .toolbar::-webkit-scrollbar {
129
+ display: none;
130
+ }
131
+
127
132
  @media (max-width: 767px) {
128
133
  .toolbar {
129
134
  justify-content: flex-start;
130
- align-items: stretch;
131
- gap: .5rem
135
+ align-items: center;
136
+ gap: .5rem;
137
+ padding: 0 .5rem;
138
+ -webkit-overflow-scrolling: touch
132
139
  }
133
140
  }
134
141
 
@@ -136,6 +143,7 @@ let { mode, hasImage, canUndo, canRedo, isStandalone = false, onModeChange, onUn
136
143
  display: flex;
137
144
  gap: .25rem;
138
145
  align-items: center;
146
+ flex-shrink: 0;
139
147
  }
140
148
 
141
149
  .history-controls {
@@ -146,10 +154,8 @@ let { mode, hasImage, canUndo, canRedo, isStandalone = false, onModeChange, onUn
146
154
  @media (max-width: 767px) {
147
155
 
148
156
  .history-controls {
149
- border-right: none;
150
- padding-right: 0;
151
- border-bottom: 1px solid #444;
152
- padding-bottom: .5rem
157
+ border-right: 1px solid #444;
158
+ padding-right: .5rem
153
159
  }
154
160
  }
155
161
 
@@ -157,6 +163,13 @@ let { mode, hasImage, canUndo, canRedo, isStandalone = false, onModeChange, onUn
157
163
  gap: .5rem;
158
164
  }
159
165
 
166
+ @media (max-width: 767px) {
167
+
168
+ .mode-controls {
169
+ gap: .25rem
170
+ }
171
+ }
172
+
160
173
  .toolbar-btn {
161
174
  display: flex;
162
175
  align-items: center;
@@ -169,6 +182,7 @@ let { mode, hasImage, canUndo, canRedo, isStandalone = false, onModeChange, onUn
169
182
  cursor: pointer;
170
183
  transition: all 0.2s;
171
184
  font-size: 0.9rem;
185
+ flex-shrink: 0;
172
186
  }
173
187
 
174
188
  @media (max-width: 767px) {
@@ -176,9 +190,11 @@ let { mode, hasImage, canUndo, canRedo, isStandalone = false, onModeChange, onUn
176
190
  .toolbar-btn {
177
191
  flex-direction: column;
178
192
  justify-content: center;
179
- font-size: .6rem;
180
- gap: .3rem;
181
- width: 64px
193
+ font-size: .55rem;
194
+ gap: .2rem;
195
+ padding: .4rem .5rem;
196
+ min-width: 48px;
197
+ width: auto
182
198
  }
183
199
  }
184
200
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokimeki-image-editor",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "A image editor for svelte.",
5
5
  "type": "module",
6
6
  "license": "MIT",