tokimeki-image-editor 0.1.1 → 0.1.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.
- package/dist/components/AdjustTool.svelte +317 -0
- package/dist/components/AdjustTool.svelte.d.ts +9 -0
- package/dist/components/BlurTool.svelte +613 -0
- package/dist/components/BlurTool.svelte.d.ts +15 -0
- package/dist/components/Canvas.svelte +214 -0
- package/dist/components/Canvas.svelte.d.ts +17 -0
- package/dist/components/CropTool.svelte +942 -0
- package/dist/components/CropTool.svelte.d.ts +14 -0
- package/dist/components/ExportTool.svelte +191 -0
- package/dist/components/ExportTool.svelte.d.ts +10 -0
- package/dist/components/FilterTool.svelte +492 -0
- package/dist/components/FilterTool.svelte.d.ts +12 -0
- package/dist/components/ImageEditor.svelte +735 -0
- package/dist/components/ImageEditor.svelte.d.ts +12 -0
- package/dist/components/RotateTool.svelte +157 -0
- package/dist/components/RotateTool.svelte.d.ts +9 -0
- package/dist/components/StampTool.svelte +678 -0
- package/dist/components/StampTool.svelte.d.ts +15 -0
- package/dist/components/Toolbar.svelte +136 -0
- package/dist/components/Toolbar.svelte.d.ts +10 -0
- package/dist/config/stamps.d.ts +2 -0
- package/dist/config/stamps.js +22 -0
- package/dist/i18n/index.d.ts +1 -0
- package/dist/i18n/index.js +9 -0
- package/dist/i18n/locales/en.json +68 -0
- package/dist/i18n/locales/ja.json +68 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +1 -0
- package/dist/utils/adjustments.d.ts +26 -0
- package/dist/utils/adjustments.js +525 -0
- package/dist/utils/canvas.d.ts +30 -0
- package/dist/utils/canvas.js +293 -0
- package/dist/utils/filters.d.ts +18 -0
- package/dist/utils/filters.js +114 -0
- package/dist/utils/history.d.ts +15 -0
- package/dist/utils/history.js +67 -0
- package/package.json +1 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script lang="ts">import { onMount } from 'svelte';
|
|
2
|
+
import { drawImage, preloadStampImage } from '../utils/canvas';
|
|
3
|
+
let { canvas = $bindable(null), width, height, image, viewport, transform, adjustments, cropArea = null, blurAreas = [], stampAreas = [], onZoom } = $props();
|
|
4
|
+
let canvasElement = $state(null);
|
|
5
|
+
let isPanning = $state(false);
|
|
6
|
+
let lastPanPosition = $state({ x: 0, y: 0 });
|
|
7
|
+
let imageLoadCounter = $state(0); // Trigger redraw when images load
|
|
8
|
+
let initialPinchDistance = $state(0);
|
|
9
|
+
let initialZoom = $state(1);
|
|
10
|
+
let renderRequested = $state(false);
|
|
11
|
+
let pendingRenderFrame = null;
|
|
12
|
+
onMount(() => {
|
|
13
|
+
if (canvasElement) {
|
|
14
|
+
canvas = canvasElement;
|
|
15
|
+
// Add touch event listeners with passive: false to allow preventDefault
|
|
16
|
+
canvasElement.addEventListener('touchstart', handleTouchStart, { passive: false });
|
|
17
|
+
canvasElement.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
18
|
+
canvasElement.addEventListener('touchend', handleTouchEnd, { passive: false });
|
|
19
|
+
}
|
|
20
|
+
return () => {
|
|
21
|
+
// Cleanup event listeners
|
|
22
|
+
if (canvasElement) {
|
|
23
|
+
canvasElement.removeEventListener('touchstart', handleTouchStart);
|
|
24
|
+
canvasElement.removeEventListener('touchmove', handleTouchMove);
|
|
25
|
+
canvasElement.removeEventListener('touchend', handleTouchEnd);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
// Request a render using requestAnimationFrame
|
|
30
|
+
function requestRender() {
|
|
31
|
+
if (renderRequested)
|
|
32
|
+
return;
|
|
33
|
+
renderRequested = true;
|
|
34
|
+
if (pendingRenderFrame !== null) {
|
|
35
|
+
cancelAnimationFrame(pendingRenderFrame);
|
|
36
|
+
}
|
|
37
|
+
pendingRenderFrame = requestAnimationFrame(() => {
|
|
38
|
+
performRender();
|
|
39
|
+
renderRequested = false;
|
|
40
|
+
pendingRenderFrame = null;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Perform the actual render
|
|
44
|
+
function performRender() {
|
|
45
|
+
if (!canvasElement || !image)
|
|
46
|
+
return;
|
|
47
|
+
canvasElement.width = width;
|
|
48
|
+
canvasElement.height = height;
|
|
49
|
+
drawImage(canvasElement, image, viewport, transform, adjustments, cropArea, blurAreas, stampAreas);
|
|
50
|
+
}
|
|
51
|
+
// Preload stamp images
|
|
52
|
+
$effect(() => {
|
|
53
|
+
if (!stampAreas)
|
|
54
|
+
return;
|
|
55
|
+
stampAreas.forEach(stamp => {
|
|
56
|
+
if (stamp.stampType === 'image' || stamp.stampType === 'svg') {
|
|
57
|
+
preloadStampImage(stamp.stampContent).then(() => {
|
|
58
|
+
// Trigger redraw when image loads
|
|
59
|
+
imageLoadCounter++;
|
|
60
|
+
}).catch(console.error);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
// Draw canvas - use requestAnimationFrame for optimal performance
|
|
65
|
+
$effect(() => {
|
|
66
|
+
if (canvasElement && image) {
|
|
67
|
+
requestRender();
|
|
68
|
+
}
|
|
69
|
+
// Include all dependencies
|
|
70
|
+
width;
|
|
71
|
+
height;
|
|
72
|
+
viewport;
|
|
73
|
+
transform;
|
|
74
|
+
adjustments;
|
|
75
|
+
cropArea;
|
|
76
|
+
blurAreas;
|
|
77
|
+
stampAreas;
|
|
78
|
+
imageLoadCounter;
|
|
79
|
+
});
|
|
80
|
+
function handleMouseDown(e) {
|
|
81
|
+
// Left mouse button (0) or Middle mouse button (1) for panning
|
|
82
|
+
if (e.button === 0 || e.button === 1) {
|
|
83
|
+
isPanning = true;
|
|
84
|
+
lastPanPosition = { x: e.clientX, y: e.clientY };
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function handleMouseMove(e) {
|
|
89
|
+
if (isPanning && image && canvasElement) {
|
|
90
|
+
const deltaX = e.clientX - lastPanPosition.x;
|
|
91
|
+
const deltaY = e.clientY - lastPanPosition.y;
|
|
92
|
+
// Calculate actual image dimensions after crop and scale
|
|
93
|
+
const imgWidth = cropArea ? cropArea.width : image.width;
|
|
94
|
+
const imgHeight = cropArea ? cropArea.height : image.height;
|
|
95
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
96
|
+
const scaledWidth = imgWidth * totalScale;
|
|
97
|
+
const scaledHeight = imgHeight * totalScale;
|
|
98
|
+
// Allow 20% overflow outside canvas
|
|
99
|
+
const overflowMargin = 0.2;
|
|
100
|
+
const maxOffsetX = (scaledWidth / 2) - (canvasElement.width / 2) + (canvasElement.width * overflowMargin);
|
|
101
|
+
const maxOffsetY = (scaledHeight / 2) - (canvasElement.height / 2) + (canvasElement.height * overflowMargin);
|
|
102
|
+
// Apply limits
|
|
103
|
+
const newOffsetX = viewport.offsetX + deltaX;
|
|
104
|
+
const newOffsetY = viewport.offsetY + deltaY;
|
|
105
|
+
viewport.offsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, newOffsetX));
|
|
106
|
+
viewport.offsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, newOffsetY));
|
|
107
|
+
lastPanPosition = { x: e.clientX, y: e.clientY };
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function handleMouseUp() {
|
|
112
|
+
isPanning = false;
|
|
113
|
+
}
|
|
114
|
+
function handleTouchStart(e) {
|
|
115
|
+
if (e.touches.length === 1) {
|
|
116
|
+
// Single finger panning
|
|
117
|
+
isPanning = true;
|
|
118
|
+
lastPanPosition = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
}
|
|
121
|
+
else if (e.touches.length === 2) {
|
|
122
|
+
// Pinch zoom - stop panning
|
|
123
|
+
isPanning = false;
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function handleTouchMove(e) {
|
|
128
|
+
if (e.touches.length === 1 && isPanning && image && canvasElement) {
|
|
129
|
+
// Single finger panning
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
const touch = e.touches[0];
|
|
132
|
+
const deltaX = touch.clientX - lastPanPosition.x;
|
|
133
|
+
const deltaY = touch.clientY - lastPanPosition.y;
|
|
134
|
+
// Calculate actual image dimensions after crop and scale
|
|
135
|
+
const imgWidth = cropArea ? cropArea.width : image.width;
|
|
136
|
+
const imgHeight = cropArea ? cropArea.height : image.height;
|
|
137
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
138
|
+
const scaledWidth = imgWidth * totalScale;
|
|
139
|
+
const scaledHeight = imgHeight * totalScale;
|
|
140
|
+
// Allow 20% overflow outside canvas
|
|
141
|
+
const overflowMargin = 0.2;
|
|
142
|
+
const maxOffsetX = (scaledWidth / 2) - (canvasElement.width / 2) + (canvasElement.width * overflowMargin);
|
|
143
|
+
const maxOffsetY = (scaledHeight / 2) - (canvasElement.height / 2) + (canvasElement.height * overflowMargin);
|
|
144
|
+
// Apply limits
|
|
145
|
+
const newOffsetX = viewport.offsetX + deltaX;
|
|
146
|
+
const newOffsetY = viewport.offsetY + deltaY;
|
|
147
|
+
viewport.offsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, newOffsetX));
|
|
148
|
+
viewport.offsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, newOffsetY));
|
|
149
|
+
lastPanPosition = { x: touch.clientX, y: touch.clientY };
|
|
150
|
+
}
|
|
151
|
+
else if (e.touches.length === 2) {
|
|
152
|
+
// Pinch zoom
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
const touch1 = e.touches[0];
|
|
155
|
+
const touch2 = e.touches[1];
|
|
156
|
+
const distance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
|
|
157
|
+
if (initialPinchDistance === 0) {
|
|
158
|
+
initialPinchDistance = distance;
|
|
159
|
+
initialZoom = viewport.zoom;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const scale = distance / initialPinchDistance;
|
|
163
|
+
const newZoom = Math.max(0.1, Math.min(5, initialZoom * scale));
|
|
164
|
+
const delta = newZoom - viewport.zoom;
|
|
165
|
+
const centerX = (touch1.clientX + touch2.clientX) / 2;
|
|
166
|
+
const centerY = (touch1.clientY + touch2.clientY) / 2;
|
|
167
|
+
if (onZoom) {
|
|
168
|
+
onZoom(delta, centerX, centerY);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function handleTouchEnd(e) {
|
|
174
|
+
if (e.touches.length === 0) {
|
|
175
|
+
isPanning = false;
|
|
176
|
+
initialPinchDistance = 0;
|
|
177
|
+
}
|
|
178
|
+
else if (e.touches.length === 1) {
|
|
179
|
+
// Switched from pinch to pan
|
|
180
|
+
initialPinchDistance = 0;
|
|
181
|
+
isPanning = true;
|
|
182
|
+
lastPanPosition = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
</script>
|
|
186
|
+
|
|
187
|
+
<svelte:window
|
|
188
|
+
onmousemove={handleMouseMove}
|
|
189
|
+
onmouseup={handleMouseUp}
|
|
190
|
+
/>
|
|
191
|
+
|
|
192
|
+
<canvas
|
|
193
|
+
bind:this={canvasElement}
|
|
194
|
+
width={width}
|
|
195
|
+
height={height}
|
|
196
|
+
class="editor-canvas"
|
|
197
|
+
class:panning={isPanning}
|
|
198
|
+
style="max-width: 100%; max-height: {height}px;"
|
|
199
|
+
onmousedown={handleMouseDown}
|
|
200
|
+
></canvas>
|
|
201
|
+
|
|
202
|
+
<style>
|
|
203
|
+
.editor-canvas {
|
|
204
|
+
display: block;
|
|
205
|
+
background: #000;
|
|
206
|
+
cursor: grab;
|
|
207
|
+
touch-action: none;
|
|
208
|
+
user-select: none;
|
|
209
|
+
-webkit-user-select: none;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.editor-canvas.panning {
|
|
213
|
+
cursor: grabbing;
|
|
214
|
+
}</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Viewport, TransformState, CropArea, AdjustmentsState, BlurArea, StampArea } from '../types';
|
|
2
|
+
interface Props {
|
|
3
|
+
canvas?: HTMLCanvasElement | null;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
image: HTMLImageElement | null;
|
|
7
|
+
viewport: Viewport;
|
|
8
|
+
transform: TransformState;
|
|
9
|
+
adjustments: AdjustmentsState;
|
|
10
|
+
cropArea?: CropArea | null;
|
|
11
|
+
blurAreas?: BlurArea[];
|
|
12
|
+
stampAreas?: StampArea[];
|
|
13
|
+
onZoom?: (delta: number, centerX?: number, centerY?: number) => void;
|
|
14
|
+
}
|
|
15
|
+
declare const Canvas: import("svelte").Component<Props, {}, "canvas">;
|
|
16
|
+
type Canvas = ReturnType<typeof Canvas>;
|
|
17
|
+
export default Canvas;
|