tokimeki-image-editor 0.1.8 → 0.1.10
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 +333 -299
- package/dist/components/Canvas.svelte +274 -115
- package/dist/components/Canvas.svelte.d.ts +1 -0
- package/dist/components/FilterTool.svelte +408 -298
- package/dist/components/ImageEditor.svelte +426 -423
- package/dist/i18n/locales/en.json +79 -68
- package/dist/i18n/locales/ja.json +79 -68
- package/dist/shaders/blur.wgsl +59 -0
- package/dist/shaders/composite.wgsl +46 -0
- package/dist/shaders/grain.wgsl +225 -0
- package/dist/shaders/image-editor.wgsl +296 -0
- package/dist/types.d.ts +3 -1
- package/dist/utils/adjustments.d.ts +2 -1
- package/dist/utils/adjustments.js +100 -13
- package/dist/utils/canvas.d.ts +7 -2
- package/dist/utils/canvas.js +48 -5
- package/dist/utils/filters.js +109 -2
- package/dist/utils/webgpu-render.d.ts +26 -0
- package/dist/utils/webgpu-render.js +1192 -0
- package/package.json +43 -42
|
@@ -1,24 +1,76 @@
|
|
|
1
|
-
<script lang="ts">import { onMount } from 'svelte';
|
|
2
|
-
import { drawImage, preloadStampImage } from '../utils/canvas';
|
|
3
|
-
|
|
1
|
+
<script lang="ts">import { onMount, onDestroy } from 'svelte';
|
|
2
|
+
import { drawImage, preloadStampImage, applyStamps } from '../utils/canvas';
|
|
3
|
+
import { initWebGPUCanvas, uploadImageToGPU, renderWithAdjustments, cleanupWebGPU, isWebGPUInitialized } from '../utils/webgpu-render';
|
|
4
|
+
let { canvas = $bindable(null), width, height, image, viewport, transform, adjustments, cropArea = null, blurAreas = [], stampAreas = [], onZoom, onViewportChange } = $props();
|
|
5
|
+
// Constants
|
|
6
|
+
const PAN_OVERFLOW_MARGIN = 0.2; // Allow 20% overflow when panning
|
|
7
|
+
// State
|
|
4
8
|
let canvasElement = $state(null);
|
|
9
|
+
let overlayCanvasElement = $state(null);
|
|
10
|
+
let isInitializing = $state(true); // Prevent 2D rendering before WebGPU check
|
|
11
|
+
let useWebGPU = $state(false);
|
|
12
|
+
let webgpuReady = $state(false);
|
|
13
|
+
let currentImage = null;
|
|
14
|
+
// 2D Canvas fallback state
|
|
5
15
|
let isPanning = $state(false);
|
|
6
16
|
let lastPanPosition = $state({ x: 0, y: 0 });
|
|
7
|
-
let imageLoadCounter = $state(0); //
|
|
17
|
+
let imageLoadCounter = $state(0); // Incremented to trigger re-render when stamp images load
|
|
8
18
|
let initialPinchDistance = $state(0);
|
|
9
19
|
let initialZoom = $state(1);
|
|
10
20
|
let renderRequested = $state(false);
|
|
11
21
|
let pendingRenderFrame = null;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
canvasElement.addEventListener('touchend', handleTouchEnd, { passive: false });
|
|
22
|
+
let needsAnotherRender = $state(false);
|
|
23
|
+
// Helper functions
|
|
24
|
+
function ensureCanvasSize(canvas, w, h) {
|
|
25
|
+
if (canvas.width !== w || canvas.height !== h) {
|
|
26
|
+
canvas.width = w;
|
|
27
|
+
canvas.height = h;
|
|
19
28
|
}
|
|
29
|
+
}
|
|
30
|
+
function calculatePanOffset(deltaX, deltaY) {
|
|
31
|
+
if (!image || !canvasElement)
|
|
32
|
+
return null;
|
|
33
|
+
const imgWidth = cropArea ? cropArea.width : image.width;
|
|
34
|
+
const imgHeight = cropArea ? cropArea.height : image.height;
|
|
35
|
+
const totalScale = viewport.scale * viewport.zoom;
|
|
36
|
+
const scaledWidth = imgWidth * totalScale;
|
|
37
|
+
const scaledHeight = imgHeight * totalScale;
|
|
38
|
+
const maxOffsetX = (scaledWidth / 2) - (canvasElement.width / 2) + (canvasElement.width * PAN_OVERFLOW_MARGIN);
|
|
39
|
+
const maxOffsetY = (scaledHeight / 2) - (canvasElement.height / 2) + (canvasElement.height * PAN_OVERFLOW_MARGIN);
|
|
40
|
+
const newOffsetX = viewport.offsetX + deltaX;
|
|
41
|
+
const newOffsetY = viewport.offsetY + deltaY;
|
|
42
|
+
const clampedOffsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, newOffsetX));
|
|
43
|
+
const clampedOffsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, newOffsetY));
|
|
44
|
+
return { clampedOffsetX, clampedOffsetY };
|
|
45
|
+
}
|
|
46
|
+
onMount(async () => {
|
|
47
|
+
if (!canvasElement)
|
|
48
|
+
return;
|
|
49
|
+
canvas = canvasElement;
|
|
50
|
+
// Try WebGPU first
|
|
51
|
+
if (navigator.gpu) {
|
|
52
|
+
console.log('Attempting WebGPU initialization...');
|
|
53
|
+
const success = await initWebGPUCanvas(canvasElement);
|
|
54
|
+
if (success) {
|
|
55
|
+
useWebGPU = true;
|
|
56
|
+
webgpuReady = true;
|
|
57
|
+
console.log('✅ Using WebGPU rendering');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log('⚠️ WebGPU not available, using 2D Canvas fallback');
|
|
61
|
+
useWebGPU = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log('⚠️ WebGPU not available, using 2D Canvas fallback');
|
|
66
|
+
useWebGPU = false;
|
|
67
|
+
}
|
|
68
|
+
isInitializing = false;
|
|
69
|
+
// Setup touch event listeners (common for both modes)
|
|
70
|
+
canvasElement.addEventListener('touchstart', handleTouchStart, { passive: false });
|
|
71
|
+
canvasElement.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
72
|
+
canvasElement.addEventListener('touchend', handleTouchEnd, { passive: false });
|
|
20
73
|
return () => {
|
|
21
|
-
// Cleanup event listeners
|
|
22
74
|
if (canvasElement) {
|
|
23
75
|
canvasElement.removeEventListener('touchstart', handleTouchStart);
|
|
24
76
|
canvasElement.removeEventListener('touchmove', handleTouchMove);
|
|
@@ -26,47 +78,56 @@ onMount(() => {
|
|
|
26
78
|
}
|
|
27
79
|
};
|
|
28
80
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return;
|
|
33
|
-
renderRequested = true;
|
|
34
|
-
if (pendingRenderFrame !== null) {
|
|
35
|
-
cancelAnimationFrame(pendingRenderFrame);
|
|
81
|
+
onDestroy(() => {
|
|
82
|
+
if (useWebGPU) {
|
|
83
|
+
cleanupWebGPU();
|
|
36
84
|
}
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
85
|
+
});
|
|
86
|
+
// WebGPU: Upload image when it changes
|
|
52
87
|
$effect(() => {
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
});
|
|
88
|
+
if (useWebGPU && webgpuReady && image && image !== currentImage) {
|
|
89
|
+
currentImage = image;
|
|
90
|
+
uploadImageToGPU(image).then((success) => {
|
|
91
|
+
if (success) {
|
|
92
|
+
renderWebGPU();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
63
96
|
});
|
|
64
|
-
//
|
|
97
|
+
// WebGPU: Render when parameters change
|
|
65
98
|
$effect(() => {
|
|
66
|
-
if (
|
|
99
|
+
if (useWebGPU && webgpuReady && currentImage && canvasElement) {
|
|
100
|
+
renderWebGPU();
|
|
101
|
+
}
|
|
102
|
+
// Track object properties explicitly for reactivity
|
|
103
|
+
adjustments.brightness;
|
|
104
|
+
adjustments.contrast;
|
|
105
|
+
adjustments.saturation;
|
|
106
|
+
viewport.offsetX;
|
|
107
|
+
viewport.offsetY;
|
|
108
|
+
viewport.zoom;
|
|
109
|
+
viewport.scale;
|
|
110
|
+
transform.rotation;
|
|
111
|
+
transform.flipHorizontal;
|
|
112
|
+
transform.flipVertical;
|
|
113
|
+
transform.scale;
|
|
114
|
+
// Track cropArea object itself and its properties
|
|
115
|
+
cropArea;
|
|
116
|
+
cropArea?.x;
|
|
117
|
+
cropArea?.y;
|
|
118
|
+
cropArea?.width;
|
|
119
|
+
cropArea?.height;
|
|
120
|
+
blurAreas;
|
|
121
|
+
stampAreas;
|
|
122
|
+
width;
|
|
123
|
+
height;
|
|
124
|
+
});
|
|
125
|
+
// 2D Canvas: Render when parameters change
|
|
126
|
+
$effect(() => {
|
|
127
|
+
if (!isInitializing && !useWebGPU && canvasElement && image) {
|
|
67
128
|
requestRender();
|
|
68
129
|
}
|
|
69
|
-
//
|
|
130
|
+
// Dependencies
|
|
70
131
|
width;
|
|
71
132
|
height;
|
|
72
133
|
viewport;
|
|
@@ -77,8 +138,81 @@ $effect(() => {
|
|
|
77
138
|
stampAreas;
|
|
78
139
|
imageLoadCounter;
|
|
79
140
|
});
|
|
141
|
+
// Preload stamp images
|
|
142
|
+
$effect(() => {
|
|
143
|
+
if (stampAreas) {
|
|
144
|
+
stampAreas.forEach(stamp => {
|
|
145
|
+
if (stamp.stampType === 'image' || stamp.stampType === 'svg') {
|
|
146
|
+
preloadStampImage(stamp.stampContent).then(() => {
|
|
147
|
+
if (!useWebGPU) {
|
|
148
|
+
imageLoadCounter++;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Trigger re-render for WebGPU mode
|
|
152
|
+
renderWebGPU();
|
|
153
|
+
}
|
|
154
|
+
}).catch(console.error);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
function renderWebGPU() {
|
|
160
|
+
if (!canvasElement || !webgpuReady || !currentImage)
|
|
161
|
+
return;
|
|
162
|
+
ensureCanvasSize(canvasElement, width, height);
|
|
163
|
+
renderWithAdjustments(adjustments, viewport, transform, width, height, currentImage.width, currentImage.height, cropArea, blurAreas);
|
|
164
|
+
// Render stamps on overlay canvas
|
|
165
|
+
if (overlayCanvasElement && stampAreas.length > 0) {
|
|
166
|
+
ensureCanvasSize(overlayCanvasElement, width, height);
|
|
167
|
+
// Clear overlay canvas
|
|
168
|
+
const ctx = overlayCanvasElement.getContext('2d');
|
|
169
|
+
if (ctx) {
|
|
170
|
+
ctx.clearRect(0, 0, width, height);
|
|
171
|
+
applyStamps(overlayCanvasElement, currentImage, viewport, stampAreas, cropArea);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (overlayCanvasElement) {
|
|
175
|
+
// Clear overlay if no stamps
|
|
176
|
+
const ctx = overlayCanvasElement.getContext('2d');
|
|
177
|
+
if (ctx) {
|
|
178
|
+
ctx.clearRect(0, 0, width, height);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// === 2D Canvas Fallback Implementation ===
|
|
183
|
+
function requestRender() {
|
|
184
|
+
if (renderRequested) {
|
|
185
|
+
needsAnotherRender = true;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
renderRequested = true;
|
|
189
|
+
needsAnotherRender = false;
|
|
190
|
+
if (pendingRenderFrame !== null) {
|
|
191
|
+
cancelAnimationFrame(pendingRenderFrame);
|
|
192
|
+
}
|
|
193
|
+
pendingRenderFrame = requestAnimationFrame(() => {
|
|
194
|
+
performRender()
|
|
195
|
+
.catch(error => {
|
|
196
|
+
console.error('Render error:', error);
|
|
197
|
+
})
|
|
198
|
+
.finally(() => {
|
|
199
|
+
renderRequested = false;
|
|
200
|
+
pendingRenderFrame = null;
|
|
201
|
+
if (needsAnotherRender) {
|
|
202
|
+
needsAnotherRender = false;
|
|
203
|
+
requestRender();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async function performRender() {
|
|
209
|
+
if (!canvasElement || !image)
|
|
210
|
+
return;
|
|
211
|
+
canvasElement.width = width;
|
|
212
|
+
canvasElement.height = height;
|
|
213
|
+
await drawImage(canvasElement, image, viewport, transform, adjustments, cropArea, blurAreas, stampAreas);
|
|
214
|
+
}
|
|
80
215
|
function handleMouseDown(e) {
|
|
81
|
-
// Left mouse button (0) or Middle mouse button (1) for panning
|
|
82
216
|
if (e.button === 0 || e.button === 1) {
|
|
83
217
|
isPanning = true;
|
|
84
218
|
lastPanPosition = { x: e.clientX, y: e.clientY };
|
|
@@ -86,24 +220,13 @@ function handleMouseDown(e) {
|
|
|
86
220
|
}
|
|
87
221
|
}
|
|
88
222
|
function handleMouseMove(e) {
|
|
89
|
-
if (isPanning
|
|
223
|
+
if (isPanning) {
|
|
90
224
|
const deltaX = e.clientX - lastPanPosition.x;
|
|
91
225
|
const deltaY = e.clientY - lastPanPosition.y;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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));
|
|
226
|
+
const result = calculatePanOffset(deltaX, deltaY);
|
|
227
|
+
if (result && onViewportChange) {
|
|
228
|
+
onViewportChange({ offsetX: result.clampedOffsetX, offsetY: result.clampedOffsetY });
|
|
229
|
+
}
|
|
107
230
|
lastPanPosition = { x: e.clientX, y: e.clientY };
|
|
108
231
|
e.preventDefault();
|
|
109
232
|
}
|
|
@@ -113,43 +236,28 @@ function handleMouseUp() {
|
|
|
113
236
|
}
|
|
114
237
|
function handleTouchStart(e) {
|
|
115
238
|
if (e.touches.length === 1) {
|
|
116
|
-
// Single finger panning
|
|
117
239
|
isPanning = true;
|
|
118
240
|
lastPanPosition = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
119
241
|
e.preventDefault();
|
|
120
242
|
}
|
|
121
243
|
else if (e.touches.length === 2) {
|
|
122
|
-
// Pinch zoom - stop panning
|
|
123
244
|
isPanning = false;
|
|
124
245
|
e.preventDefault();
|
|
125
246
|
}
|
|
126
247
|
}
|
|
127
248
|
function handleTouchMove(e) {
|
|
128
|
-
if (e.touches.length === 1 && isPanning
|
|
129
|
-
// Single finger panning
|
|
249
|
+
if (e.touches.length === 1 && isPanning) {
|
|
130
250
|
e.preventDefault();
|
|
131
251
|
const touch = e.touches[0];
|
|
132
252
|
const deltaX = touch.clientX - lastPanPosition.x;
|
|
133
253
|
const deltaY = touch.clientY - lastPanPosition.y;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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));
|
|
254
|
+
const result = calculatePanOffset(deltaX, deltaY);
|
|
255
|
+
if (result && onViewportChange) {
|
|
256
|
+
onViewportChange({ offsetX: result.clampedOffsetX, offsetY: result.clampedOffsetY });
|
|
257
|
+
}
|
|
149
258
|
lastPanPosition = { x: touch.clientX, y: touch.clientY };
|
|
150
259
|
}
|
|
151
260
|
else if (e.touches.length === 2) {
|
|
152
|
-
// Pinch zoom
|
|
153
261
|
e.preventDefault();
|
|
154
262
|
const touch1 = e.touches[0];
|
|
155
263
|
const touch2 = e.touches[1];
|
|
@@ -176,39 +284,90 @@ function handleTouchEnd(e) {
|
|
|
176
284
|
initialPinchDistance = 0;
|
|
177
285
|
}
|
|
178
286
|
else if (e.touches.length === 1) {
|
|
179
|
-
// Switched from pinch to pan
|
|
180
287
|
initialPinchDistance = 0;
|
|
181
288
|
isPanning = true;
|
|
182
289
|
lastPanPosition = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
183
290
|
}
|
|
184
291
|
}
|
|
185
|
-
</script>
|
|
186
|
-
|
|
187
|
-
<svelte:window
|
|
188
|
-
onmousemove={handleMouseMove}
|
|
189
|
-
onmouseup={handleMouseUp}
|
|
190
|
-
/>
|
|
191
|
-
|
|
192
|
-
<canvas
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
292
|
+
</script>
|
|
293
|
+
|
|
294
|
+
<svelte:window
|
|
295
|
+
onmousemove={handleMouseMove}
|
|
296
|
+
onmouseup={handleMouseUp}
|
|
297
|
+
/>
|
|
298
|
+
|
|
299
|
+
<div class="canvas-container">
|
|
300
|
+
<canvas
|
|
301
|
+
bind:this={canvasElement}
|
|
302
|
+
width={width}
|
|
303
|
+
height={height}
|
|
304
|
+
class="editor-canvas"
|
|
305
|
+
class:panning={isPanning}
|
|
306
|
+
style="max-width: 100%; max-height: {height}px;"
|
|
307
|
+
onmousedown={handleMouseDown}
|
|
308
|
+
></canvas>
|
|
309
|
+
|
|
310
|
+
{#if useWebGPU}
|
|
311
|
+
<canvas
|
|
312
|
+
bind:this={overlayCanvasElement}
|
|
313
|
+
width={width}
|
|
314
|
+
height={height}
|
|
315
|
+
class="overlay-canvas"
|
|
316
|
+
style="max-width: 100%; max-height: {height}px; pointer-events: none;"
|
|
317
|
+
></canvas>
|
|
318
|
+
{/if}
|
|
319
|
+
|
|
320
|
+
{#if useWebGPU && webgpuReady}
|
|
321
|
+
<div class="gpu-indicator">
|
|
322
|
+
<span class="gpu-badge">WebGPU</span>
|
|
323
|
+
</div>
|
|
324
|
+
{/if}
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<style>
|
|
328
|
+
.canvas-container {
|
|
329
|
+
position: relative;
|
|
330
|
+
display: inline-block;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.editor-canvas {
|
|
334
|
+
display: block;
|
|
335
|
+
background: #000;
|
|
336
|
+
cursor: grab;
|
|
337
|
+
touch-action: none;
|
|
338
|
+
user-select: none;
|
|
339
|
+
-webkit-user-select: none;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.editor-canvas.panning {
|
|
343
|
+
cursor: grabbing;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.overlay-canvas {
|
|
347
|
+
position: absolute;
|
|
348
|
+
top: 0;
|
|
349
|
+
left: 0;
|
|
350
|
+
display: block;
|
|
351
|
+
pointer-events: none;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.gpu-indicator {
|
|
355
|
+
position: absolute;
|
|
356
|
+
top: 10px;
|
|
357
|
+
right: 10px;
|
|
358
|
+
pointer-events: none;
|
|
359
|
+
z-index: 10;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.gpu-badge {
|
|
363
|
+
display: inline-block;
|
|
364
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
365
|
+
color: white;
|
|
366
|
+
padding: 4px 12px;
|
|
367
|
+
border-radius: 12px;
|
|
368
|
+
font-size: 11px;
|
|
369
|
+
font-weight: 600;
|
|
370
|
+
letter-spacing: 0.5px;
|
|
371
|
+
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
|
372
|
+
text-transform: uppercase;
|
|
373
|
+
}</style>
|
|
@@ -11,6 +11,7 @@ interface Props {
|
|
|
11
11
|
blurAreas?: BlurArea[];
|
|
12
12
|
stampAreas?: StampArea[];
|
|
13
13
|
onZoom?: (delta: number, centerX?: number, centerY?: number) => void;
|
|
14
|
+
onViewportChange?: (viewportUpdate: Partial<Viewport>) => void;
|
|
14
15
|
}
|
|
15
16
|
declare const Canvas: import("svelte").Component<Props, {}, "canvas">;
|
|
16
17
|
type Canvas = ReturnType<typeof Canvas>;
|