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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
13
|
+
return { clientY: event.touches[0].clientY };
|
|
13
14
|
}
|
|
14
|
-
else if ('
|
|
15
|
-
return {
|
|
15
|
+
else if ('clientY' in event) {
|
|
16
|
+
return { clientY: event.clientY };
|
|
16
17
|
}
|
|
17
|
-
return {
|
|
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,
|
|
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 =
|
|
32
|
-
|
|
33
|
-
|
|
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: {
|
|
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:
|
|
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,
|
|
110
|
-
max-height: 70vh;
|
|
113
|
+
height: var(--sheet-max-height, 400px);
|
|
111
114
|
border-radius: 16px 16px 0 0;
|
|
112
|
-
z-index:
|
|
115
|
+
z-index: 9999;
|
|
113
116
|
overflow-y: auto;
|
|
117
|
+
overscroll-behavior: contain;
|
|
114
118
|
padding-top: 0;
|
|
115
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
150
|
-
padding-right:
|
|
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: .
|
|
180
|
-
gap: .
|
|
181
|
-
|
|
193
|
+
font-size: .55rem;
|
|
194
|
+
gap: .2rem;
|
|
195
|
+
padding: .4rem .5rem;
|
|
196
|
+
min-width: 48px;
|
|
197
|
+
width: auto
|
|
182
198
|
}
|
|
183
199
|
}
|
|
184
200
|
|