@x33025/sveltely 0.1.1 → 0.1.3

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.
Files changed (156) hide show
  1. package/dist/components/Library/AnimatedNumber/AnimatedNumber.demo.svelte +1 -1
  2. package/dist/components/Library/AsyncButton/AsyncButton.svelte +42 -16
  3. package/dist/components/Library/Button/Button.demo.svelte +5 -3
  4. package/dist/components/Library/Button/Button.demo.svelte.d.ts +1 -0
  5. package/dist/components/Library/Button/Button.svelte +21 -13
  6. package/dist/components/Library/Calendar/Calendar.demo.svelte +2 -14
  7. package/dist/components/Library/Calendar/Calendar.svelte +69 -65
  8. package/dist/components/Library/Checkbox/Checkbox.svelte +13 -14
  9. package/dist/components/Library/ChipInput/ChipInput.demo.svelte +1 -1
  10. package/dist/components/Library/ChipInput/ChipInput.svelte +7 -4
  11. package/dist/components/Library/Divider/Divider.svelte +10 -0
  12. package/dist/components/Library/Divider/Divider.svelte.d.ts +26 -0
  13. package/dist/components/Library/Divider/index.d.ts +1 -0
  14. package/dist/components/Library/Divider/index.js +1 -0
  15. package/dist/components/Library/Dropdown/Action.svelte +60 -0
  16. package/dist/components/Library/Dropdown/Action.svelte.d.ts +11 -0
  17. package/dist/components/Library/Dropdown/Divider.svelte +5 -0
  18. package/dist/components/Library/Dropdown/Divider.svelte.d.ts +19 -0
  19. package/dist/components/Library/Dropdown/Dropdown.demo.svelte +182 -65
  20. package/dist/components/Library/Dropdown/Dropdown.demo.svelte.d.ts +2 -1
  21. package/dist/components/Library/Dropdown/Dropdown.svelte +95 -250
  22. package/dist/components/Library/Dropdown/Dropdown.svelte.d.ts +17 -16
  23. package/dist/components/Library/Dropdown/Item.svelte +68 -0
  24. package/dist/components/Library/Dropdown/Item.svelte.d.ts +31 -0
  25. package/dist/components/Library/Dropdown/Section.svelte +34 -0
  26. package/dist/components/Library/Dropdown/Section.svelte.d.ts +8 -0
  27. package/dist/components/Library/Dropdown/context.d.ts +34 -0
  28. package/dist/components/Library/Dropdown/context.js +6 -0
  29. package/dist/components/Library/Dropdown/index.d.ts +13 -2
  30. package/dist/components/Library/Dropdown/index.js +11 -1
  31. package/dist/components/Library/Floating/Floating.svelte +44 -7
  32. package/dist/components/Library/ForEach/ForEach.svelte +14 -0
  33. package/dist/components/Library/ForEach/ForEach.svelte.d.ts +28 -0
  34. package/dist/components/Library/ForEach/index.d.ts +1 -0
  35. package/dist/components/Library/ForEach/index.js +1 -0
  36. package/dist/components/Library/Grid/Grid.svelte +74 -0
  37. package/dist/components/Library/Grid/Grid.svelte.d.ts +13 -0
  38. package/dist/components/Library/Grid/index.d.ts +1 -0
  39. package/dist/components/Library/Grid/index.js +1 -0
  40. package/dist/components/Library/GridItem/GridItem.svelte +65 -0
  41. package/dist/components/Library/GridItem/GridItem.svelte.d.ts +14 -0
  42. package/dist/components/Library/GridItem/index.d.ts +1 -0
  43. package/dist/components/Library/GridItem/index.js +1 -0
  44. package/dist/components/Library/HStack/HStack.svelte +45 -0
  45. package/dist/components/Library/HStack/HStack.svelte.d.ts +9 -0
  46. package/dist/components/Library/HStack/index.d.ts +1 -0
  47. package/dist/components/Library/HStack/index.js +1 -0
  48. package/dist/components/Library/Image/Image.demo.svelte +18 -0
  49. package/dist/components/Library/Image/Image.demo.svelte.d.ts +23 -0
  50. package/dist/components/Library/Image/Image.svelte +57 -0
  51. package/dist/components/Library/Image/Image.svelte.d.ts +17 -0
  52. package/dist/components/Library/Image/ImagePlaceholder.svelte +202 -0
  53. package/dist/components/Library/Image/ImagePlaceholder.svelte.d.ts +7 -0
  54. package/dist/components/Library/Image/index.d.ts +1 -0
  55. package/dist/components/Library/Image/index.js +1 -0
  56. package/dist/components/Library/ImageMask/BrushPreview.svelte +119 -0
  57. package/dist/components/Library/ImageMask/BrushPreview.svelte.d.ts +11 -0
  58. package/dist/components/Library/ImageMask/ImageMask.demo.svelte +117 -0
  59. package/dist/components/Library/ImageMask/ImageMask.demo.svelte.d.ts +10 -0
  60. package/dist/components/Library/ImageMask/ImageMask.svelte +46 -0
  61. package/dist/components/Library/ImageMask/ImageMask.svelte.d.ts +20 -0
  62. package/dist/components/Library/ImageMask/MaskLayer.svelte +341 -0
  63. package/dist/components/Library/ImageMask/MaskLayer.svelte.d.ts +12 -0
  64. package/dist/components/Library/ImageMask/contour.d.ts +11 -0
  65. package/dist/components/Library/ImageMask/contour.js +152 -0
  66. package/dist/components/Library/ImageMask/index.d.ts +2 -0
  67. package/dist/components/Library/ImageMask/index.js +1 -0
  68. package/dist/components/Library/ImageMask/marchingAnts.d.ts +8 -0
  69. package/dist/components/Library/ImageMask/marchingAnts.js +29 -0
  70. package/dist/components/Library/ImageMask/maskSurface.d.ts +5 -0
  71. package/dist/components/Library/ImageMask/maskSurface.js +94 -0
  72. package/dist/components/Library/ImageMask/types.d.ts +23 -0
  73. package/dist/components/Library/Label/Label.demo.svelte +28 -0
  74. package/dist/components/Library/Label/Label.demo.svelte.d.ts +9 -0
  75. package/dist/components/Library/Label/Label.svelte +175 -0
  76. package/dist/components/Library/Label/Label.svelte.d.ts +18 -0
  77. package/dist/components/Library/Label/index.d.ts +1 -0
  78. package/dist/components/Library/Label/index.js +1 -0
  79. package/dist/components/Library/NavigationStack/NavigationStack.svelte +17 -7
  80. package/dist/components/Library/NavigationStack/Toolbar.svelte +7 -2
  81. package/dist/components/Library/NumberField/NumberField.demo.svelte +21 -0
  82. package/dist/components/Library/NumberField/NumberField.demo.svelte.d.ts +8 -0
  83. package/dist/components/Library/NumberField/NumberField.svelte +199 -0
  84. package/dist/components/Library/NumberField/NumberField.svelte.d.ts +21 -0
  85. package/dist/components/Library/NumberField/index.d.ts +1 -0
  86. package/dist/components/Library/NumberField/index.js +1 -0
  87. package/dist/components/Library/Pagination/Pagination.svelte +16 -20
  88. package/dist/components/Library/Popover/Popover.demo.svelte +2 -2
  89. package/dist/components/Library/Popover/Popover.svelte +7 -4
  90. package/dist/components/Library/ScrollView/ScrollView.svelte +165 -12
  91. package/dist/components/Library/ScrollView/ScrollView.svelte.d.ts +32 -4
  92. package/dist/components/Library/ScrollView/index.d.ts +1 -0
  93. package/dist/components/Library/{SearchInput/SearchInput.demo.svelte → SearchField/SearchField.demo.svelte} +4 -4
  94. package/dist/components/Library/SearchField/SearchField.demo.svelte.d.ts +8 -0
  95. package/dist/components/Library/{SearchInput/SearchInput.svelte → SearchField/SearchField.svelte} +26 -30
  96. package/dist/components/Library/{SearchInput/SearchInput.svelte.d.ts → SearchField/SearchField.svelte.d.ts} +3 -3
  97. package/dist/components/Library/SearchField/index.d.ts +1 -0
  98. package/dist/components/Library/SearchField/index.js +1 -0
  99. package/dist/components/Library/SegmentedPicker/SegmentedPicker.demo.svelte +1 -1
  100. package/dist/components/Library/SegmentedPicker/SegmentedPicker.svelte +9 -9
  101. package/dist/components/Library/Sheet/Sheet.demo.svelte +1 -1
  102. package/dist/components/Library/Sheet/Sheet.svelte +8 -5
  103. package/dist/components/Library/Slider/Slider.demo.svelte +1 -1
  104. package/dist/components/Library/Slider/Slider.svelte +11 -10
  105. package/dist/components/Library/Spacer/Spacer.svelte +7 -0
  106. package/dist/components/Library/Spacer/Spacer.svelte.d.ts +26 -0
  107. package/dist/components/Library/Spacer/index.d.ts +1 -0
  108. package/dist/components/Library/Spacer/index.js +1 -0
  109. package/dist/components/Library/Spinner/Spinner.demo.svelte +1 -1
  110. package/dist/components/Library/Switch/Switch.svelte +6 -11
  111. package/dist/components/Library/TextField/TextField.demo.svelte +14 -0
  112. package/dist/components/Library/TextField/TextField.demo.svelte.d.ts +8 -0
  113. package/dist/components/Library/TextField/TextField.svelte +154 -0
  114. package/dist/components/Library/TextField/TextField.svelte.d.ts +19 -0
  115. package/dist/components/Library/TextField/index.d.ts +1 -0
  116. package/dist/components/Library/TextField/index.js +1 -0
  117. package/dist/components/Library/{TokenSearchInput/TokenSearchInput.demo.svelte → TokenSearchField/TokenSearchField.demo.svelte} +4 -4
  118. package/dist/components/Library/TokenSearchField/TokenSearchField.demo.svelte.d.ts +9 -0
  119. package/dist/components/Library/{TokenSearchInput/TokenSearchInput.svelte → TokenSearchField/TokenSearchField.svelte} +70 -66
  120. package/dist/components/Library/{TokenSearchInput/TokenSearchInput.svelte.d.ts → TokenSearchField/TokenSearchField.svelte.d.ts} +3 -3
  121. package/dist/components/Library/TokenSearchField/index.d.ts +1 -0
  122. package/dist/components/Library/TokenSearchField/index.js +1 -0
  123. package/dist/components/Library/VStack/VStack.svelte +45 -0
  124. package/dist/components/Library/VStack/VStack.svelte.d.ts +9 -0
  125. package/dist/components/Library/VStack/index.d.ts +1 -0
  126. package/dist/components/Library/VStack/index.js +1 -0
  127. package/dist/components/Library/WheelPicker/WheelColumn.svelte +4 -10
  128. package/dist/components/Library/WheelPicker/WheelPicker.svelte +5 -9
  129. package/dist/components/Local/ComponentGrid.svelte +15 -31
  130. package/dist/components/Local/HeroCard.svelte +30 -38
  131. package/dist/components/Local/HeroCard.svelte.d.ts +0 -2
  132. package/dist/components/Local/StyleControls.svelte +58 -27
  133. package/dist/components/Local/StyleControls.svelte.d.ts +3 -1
  134. package/dist/index.d.ts +26 -2
  135. package/dist/index.js +19 -2
  136. package/dist/style/index.css +35 -20
  137. package/dist/style/label.d.ts +6 -0
  138. package/dist/style/label.js +4 -0
  139. package/dist/style/layout.d.ts +57 -0
  140. package/dist/style/layout.js +128 -0
  141. package/dist/style/media.d.ts +12 -0
  142. package/dist/style/media.js +8 -0
  143. package/dist/style/scroll.d.ts +7 -0
  144. package/dist/style/scroll.js +5 -0
  145. package/dist/style/text-editor.d.ts +34 -0
  146. package/dist/style/text-editor.js +29 -0
  147. package/dist/style.css +112 -58
  148. package/package.json +1 -1
  149. package/dist/components/Library/Dropdown/types.d.ts +0 -27
  150. package/dist/components/Library/SearchInput/SearchInput.demo.svelte.d.ts +0 -8
  151. package/dist/components/Library/SearchInput/index.d.ts +0 -1
  152. package/dist/components/Library/SearchInput/index.js +0 -1
  153. package/dist/components/Library/TokenSearchInput/TokenSearchInput.demo.svelte.d.ts +0 -9
  154. package/dist/components/Library/TokenSearchInput/index.d.ts +0 -1
  155. package/dist/components/Library/TokenSearchInput/index.js +0 -1
  156. /package/dist/components/Library/{Dropdown → ImageMask}/types.js +0 -0
@@ -0,0 +1,341 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import BrushPreview from './BrushPreview.svelte';
4
+ import { buildContourPaths } from './contour';
5
+ import { drawMarchingAnts } from './marchingAnts';
6
+ import {
7
+ configureMaskContext,
8
+ drawMaskStrokeSegment,
9
+ exportMaskValue,
10
+ rebuildMaskSurface
11
+ } from './maskSurface';
12
+ import type { ContourPath, ImageMaskValue, MaskPoint, MaskStroke, MaskTool } from './types';
13
+
14
+ let {
15
+ mask = $bindable<ImageMaskValue | null>(null),
16
+ tool = 'paint',
17
+ brushSize = 24,
18
+ exportSize = 2048,
19
+ disabled = false,
20
+ clearRevision = 0
21
+ }: {
22
+ mask?: ImageMaskValue | null;
23
+ tool?: MaskTool;
24
+ brushSize?: number;
25
+ exportSize?: number;
26
+ disabled?: boolean;
27
+ clearRevision?: number;
28
+ } = $props();
29
+
30
+ let canvas = $state<HTMLCanvasElement | null>(null);
31
+ let context: CanvasRenderingContext2D | null = null;
32
+ let surfaceCanvas: HTMLCanvasElement | null = null;
33
+ let surfaceContext: CanvasRenderingContext2D | null = null;
34
+ let outlineCanvas: HTMLCanvasElement | null = null;
35
+ let outlineContext: CanvasRenderingContext2D | null = null;
36
+ let exportCanvas: HTMLCanvasElement | null = null;
37
+ let exportContext: CanvasRenderingContext2D | null = null;
38
+ let resizeObserver: ResizeObserver | null = null;
39
+ let isDrawing = false;
40
+ let lastPoint: MaskPoint | null = null;
41
+ let animationFrame = 0;
42
+ let dashOffset = 0;
43
+ let viewportWidth = 1;
44
+ let viewportHeight = 1;
45
+ let dpr = 1;
46
+ let isSyncingMask = false;
47
+ let contours: ContourPath[] = [];
48
+ let pointerPoint = $state<MaskPoint | null>(null);
49
+ let pointerVisible = $state(false);
50
+ let brushSizeLabelVisible = $state(false);
51
+ let previousBrushSize: number | null = null;
52
+ let previousClearRevision: number | null = null;
53
+ let brushSizeLabelTimer: ReturnType<typeof setTimeout> | null = null;
54
+ const strokes: MaskStroke[] = [];
55
+
56
+ function resizeCanvas() {
57
+ if (!canvas) return;
58
+ const rect = canvas.getBoundingClientRect();
59
+ const nextWidth = Math.max(1, rect.width);
60
+ const nextHeight = Math.max(1, rect.height);
61
+ const nextDpr = window.devicePixelRatio || 1;
62
+ const nextCanvasWidth = Math.max(1, Math.round(nextWidth * nextDpr));
63
+ const nextCanvasHeight = Math.max(1, Math.round(nextHeight * nextDpr));
64
+
65
+ if (
66
+ canvas.width === nextCanvasWidth &&
67
+ canvas.height === nextCanvasHeight &&
68
+ context &&
69
+ surfaceCanvas &&
70
+ surfaceContext
71
+ ) {
72
+ return;
73
+ }
74
+
75
+ viewportWidth = nextWidth;
76
+ viewportHeight = nextHeight;
77
+ dpr = nextDpr;
78
+ canvas.width = nextCanvasWidth;
79
+ canvas.height = nextCanvasHeight;
80
+ surfaceCanvas ??= document.createElement('canvas');
81
+ surfaceCanvas.width = nextCanvasWidth;
82
+ surfaceCanvas.height = nextCanvasHeight;
83
+ outlineCanvas ??= document.createElement('canvas');
84
+ outlineCanvas.width = nextCanvasWidth;
85
+ outlineCanvas.height = nextCanvasHeight;
86
+ exportCanvas ??= document.createElement('canvas');
87
+ context = canvas.getContext('2d');
88
+ surfaceContext = surfaceCanvas.getContext('2d');
89
+ outlineContext = outlineCanvas.getContext('2d');
90
+ exportContext = exportCanvas.getContext('2d');
91
+ if (context) configureMaskContext(context, dpr);
92
+ if (surfaceContext) configureMaskContext(surfaceContext, dpr);
93
+ if (outlineContext) configureMaskContext(outlineContext, dpr);
94
+ rebuildMaskSurface(surfaceContext, strokes, viewportWidth, viewportHeight);
95
+ rebuildContours();
96
+ drawFrame();
97
+ }
98
+
99
+ function pointFor(event: PointerEvent) {
100
+ if (!canvas) return null;
101
+ const rect = canvas.getBoundingClientRect();
102
+ return {
103
+ x: event.clientX - rect.left,
104
+ y: event.clientY - rect.top
105
+ };
106
+ }
107
+
108
+ function hideBrushSizeLabel() {
109
+ brushSizeLabelVisible = false;
110
+ if (brushSizeLabelTimer) {
111
+ clearTimeout(brushSizeLabelTimer);
112
+ brushSizeLabelTimer = null;
113
+ }
114
+ }
115
+
116
+ function rebuildContours() {
117
+ if (!surfaceCanvas || !surfaceContext) {
118
+ contours = [];
119
+ return;
120
+ }
121
+ contours = buildContourPaths({
122
+ surfaceCanvas,
123
+ surfaceContext,
124
+ viewportWidth,
125
+ viewportHeight,
126
+ dpr,
127
+ step: Math.max(0.5, 1 / Math.max(1, dpr)),
128
+ simplify: 0.18
129
+ });
130
+ }
131
+
132
+ function drawOutlineSurface() {
133
+ drawMarchingAnts({
134
+ context: outlineContext,
135
+ contours,
136
+ viewportWidth,
137
+ viewportHeight,
138
+ dashOffset
139
+ });
140
+ }
141
+
142
+ function drawFrame() {
143
+ if (!context) return;
144
+ drawOutlineSurface();
145
+ context.clearRect(0, 0, viewportWidth, viewportHeight);
146
+ if (!surfaceCanvas) return;
147
+ const primaryColor = canvas
148
+ ? getComputedStyle(canvas).getPropertyValue('--sveltely-active-color').trim() ||
149
+ 'var(--sveltely-active-color)'
150
+ : 'var(--sveltely-active-color)';
151
+ context.save();
152
+ context.drawImage(surfaceCanvas, 0, 0, viewportWidth, viewportHeight);
153
+ context.globalCompositeOperation = 'source-in';
154
+ context.fillStyle = primaryColor;
155
+ context.globalAlpha = 0.32;
156
+ context.fillRect(0, 0, viewportWidth, viewportHeight);
157
+ context.restore();
158
+
159
+ if (outlineCanvas) {
160
+ context.save();
161
+ context.drawImage(outlineCanvas, 0, 0, viewportWidth, viewportHeight);
162
+ context.globalCompositeOperation = 'source-in';
163
+ context.fillStyle = primaryColor;
164
+ context.globalAlpha = 0.9;
165
+ context.fillRect(0, 0, viewportWidth, viewportHeight);
166
+ context.restore();
167
+ }
168
+ context.setLineDash([]);
169
+ }
170
+
171
+ function animateOutline() {
172
+ dashOffset = (dashOffset + 0.55) % 14;
173
+ drawFrame();
174
+ animationFrame = requestAnimationFrame(animateOutline);
175
+ }
176
+
177
+ function emitMask() {
178
+ const nextMask = exportMaskValue(exportCanvas, exportContext, surfaceCanvas, exportSize);
179
+ if (!nextMask) return;
180
+ isSyncingMask = true;
181
+ mask = nextMask;
182
+ queueMicrotask(() => {
183
+ isSyncingMask = false;
184
+ });
185
+ }
186
+
187
+ function clear() {
188
+ strokes.length = 0;
189
+ rebuildMaskSurface(surfaceContext, strokes, viewportWidth, viewportHeight);
190
+ contours = [];
191
+ drawFrame();
192
+ if (mask !== null) {
193
+ isSyncingMask = true;
194
+ mask = null;
195
+ queueMicrotask(() => {
196
+ isSyncingMask = false;
197
+ });
198
+ }
199
+ }
200
+
201
+ function drawLine(from: MaskPoint, to: MaskPoint) {
202
+ const currentStroke = strokes.at(-1);
203
+ if (!currentStroke) return;
204
+ const previous = currentStroke.points.at(-2) ?? null;
205
+ if (currentStroke.points.length === 0) {
206
+ currentStroke.points.push(from);
207
+ if (from.x !== to.x || from.y !== to.y) currentStroke.points.push(to);
208
+ } else {
209
+ currentStroke.points.push(to);
210
+ }
211
+ drawMaskStrokeSegment(surfaceContext, previous, from, to, currentStroke);
212
+ drawFrame();
213
+ }
214
+
215
+ function handlePointerDown(event: PointerEvent) {
216
+ if (!canvas || disabled) return;
217
+ event.preventDefault();
218
+ hideBrushSizeLabel();
219
+ canvas.setPointerCapture(event.pointerId);
220
+ isDrawing = true;
221
+ const point = pointFor(event);
222
+ if (!point) return;
223
+ pointerPoint = point;
224
+ pointerVisible = true;
225
+ strokes.push({ tool, brushSize, points: [] });
226
+ lastPoint = point;
227
+ drawLine(point, point);
228
+ }
229
+
230
+ function handlePointerMove(event: PointerEvent) {
231
+ const events = event.getCoalescedEvents?.() ?? [event];
232
+ const lastEvent = events.at(-1) ?? event;
233
+ const latestPoint = pointFor(lastEvent);
234
+ if (latestPoint) {
235
+ pointerPoint = latestPoint;
236
+ pointerVisible = !disabled;
237
+ hideBrushSizeLabel();
238
+ }
239
+ if (!isDrawing || disabled) return;
240
+ event.preventDefault();
241
+ if (!lastPoint) return;
242
+ for (const nextEvent of events) {
243
+ const point = pointFor(nextEvent);
244
+ if (!point) continue;
245
+ drawLine(lastPoint, point);
246
+ lastPoint = point;
247
+ }
248
+ }
249
+
250
+ function handlePointerUp(event: PointerEvent) {
251
+ if (!isDrawing) return;
252
+ event.preventDefault();
253
+ isDrawing = false;
254
+ lastPoint = null;
255
+ rebuildContours();
256
+ emitMask();
257
+ }
258
+
259
+ function handlePointerEnter(event: PointerEvent) {
260
+ if (disabled) return;
261
+ hideBrushSizeLabel();
262
+ const point = pointFor(event);
263
+ if (point) pointerPoint = point;
264
+ pointerVisible = true;
265
+ }
266
+
267
+ function handlePointerLeave() {
268
+ if (isDrawing) return;
269
+ pointerVisible = false;
270
+ }
271
+
272
+ onMount(() => {
273
+ resizeCanvas();
274
+ resizeObserver = new ResizeObserver(resizeCanvas);
275
+ if (canvas) resizeObserver.observe(canvas);
276
+ animationFrame = requestAnimationFrame(animateOutline);
277
+ window.addEventListener('resize', resizeCanvas);
278
+
279
+ return () => {
280
+ cancelAnimationFrame(animationFrame);
281
+ if (brushSizeLabelTimer) clearTimeout(brushSizeLabelTimer);
282
+ resizeObserver?.disconnect();
283
+ window.removeEventListener('resize', resizeCanvas);
284
+ };
285
+ });
286
+
287
+ $effect(() => {
288
+ if (previousClearRevision === null) {
289
+ previousClearRevision = clearRevision;
290
+ return;
291
+ }
292
+ if (clearRevision === previousClearRevision) return;
293
+ previousClearRevision = clearRevision;
294
+ clear();
295
+ });
296
+
297
+ $effect(() => {
298
+ if (!isSyncingMask && mask === null && strokes.length > 0) clear();
299
+ });
300
+
301
+ $effect(() => {
302
+ if (previousBrushSize === null) {
303
+ previousBrushSize = brushSize;
304
+ return;
305
+ }
306
+ if (brushSize === previousBrushSize) return;
307
+ previousBrushSize = brushSize;
308
+ brushSizeLabelVisible = true;
309
+ if (brushSizeLabelTimer) clearTimeout(brushSizeLabelTimer);
310
+ brushSizeLabelTimer = setTimeout(() => {
311
+ brushSizeLabelVisible = false;
312
+ brushSizeLabelTimer = null;
313
+ }, 900);
314
+ });
315
+ </script>
316
+
317
+ <canvas
318
+ bind:this={canvas}
319
+ class="absolute inset-0 z-10 size-full touch-none"
320
+ aria-hidden="true"
321
+ onpointerdown={handlePointerDown}
322
+ onpointerenter={handlePointerEnter}
323
+ onpointerleave={handlePointerLeave}
324
+ onpointermove={handlePointerMove}
325
+ onpointerup={handlePointerUp}
326
+ onpointercancel={handlePointerUp}
327
+ ></canvas>
328
+
329
+ <BrushPreview
330
+ point={pointerPoint}
331
+ visible={pointerVisible}
332
+ {brushSize}
333
+ {tool}
334
+ sizeLabelVisible={brushSizeLabelVisible}
335
+ />
336
+
337
+ <style>
338
+ canvas {
339
+ cursor: none;
340
+ }
341
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { ImageMaskValue, MaskTool } from './types';
2
+ type $$ComponentProps = {
3
+ mask?: ImageMaskValue | null;
4
+ tool?: MaskTool;
5
+ brushSize?: number;
6
+ exportSize?: number;
7
+ disabled?: boolean;
8
+ clearRevision?: number;
9
+ };
10
+ declare const MaskLayer: import("svelte").Component<$$ComponentProps, {}, "mask">;
11
+ type MaskLayer = ReturnType<typeof MaskLayer>;
12
+ export default MaskLayer;
@@ -0,0 +1,11 @@
1
+ import type { ContourPath } from './types';
2
+ export declare function buildContourPaths({ surfaceCanvas, surfaceContext, viewportWidth, viewportHeight, dpr, step, simplify, threshold }: {
3
+ surfaceCanvas: HTMLCanvasElement;
4
+ surfaceContext: CanvasRenderingContext2D;
5
+ viewportWidth: number;
6
+ viewportHeight: number;
7
+ dpr: number;
8
+ step?: number;
9
+ simplify?: number;
10
+ threshold?: number;
11
+ }): ContourPath[];
@@ -0,0 +1,152 @@
1
+ function pointKey(point) {
2
+ return `${point.x.toFixed(3)}:${point.y.toFixed(3)}`;
3
+ }
4
+ function distanceToSegmentSquared(point, start, end) {
5
+ const dx = end.x - start.x;
6
+ const dy = end.y - start.y;
7
+ if (dx === 0 && dy === 0)
8
+ return (point.x - start.x) ** 2 + (point.y - start.y) ** 2;
9
+ const t = Math.max(0, Math.min(1, ((point.x - start.x) * dx + (point.y - start.y) * dy) / (dx * dx + dy * dy)));
10
+ const projected = { x: start.x + t * dx, y: start.y + t * dy };
11
+ return (point.x - projected.x) ** 2 + (point.y - projected.y) ** 2;
12
+ }
13
+ function simplifyPoints(points, epsilon) {
14
+ if (points.length <= 2)
15
+ return points;
16
+ const keep = new Array(points.length).fill(false);
17
+ const stack = [[0, points.length - 1]];
18
+ const epsilonSquared = epsilon * epsilon;
19
+ keep[0] = true;
20
+ keep[points.length - 1] = true;
21
+ while (stack.length > 0) {
22
+ const [start, end] = stack.pop();
23
+ let maxDistance = 0;
24
+ let indexToKeep = start;
25
+ for (let index = start + 1; index < end; index += 1) {
26
+ const distance = distanceToSegmentSquared(points[index], points[start], points[end]);
27
+ if (distance > maxDistance) {
28
+ maxDistance = distance;
29
+ indexToKeep = index;
30
+ }
31
+ }
32
+ if (maxDistance > epsilonSquared) {
33
+ keep[indexToKeep] = true;
34
+ stack.push([start, indexToKeep], [indexToKeep, end]);
35
+ }
36
+ }
37
+ return points.filter((_, index) => keep[index]);
38
+ }
39
+ export function buildContourPaths({ surfaceCanvas, surfaceContext, viewportWidth, viewportHeight, dpr, step = Math.max(0.25, 1 / Math.max(1, dpr)), simplify = 0.12, threshold = 127 }) {
40
+ const columns = Math.max(2, Math.ceil(viewportWidth / step) + 2);
41
+ const rows = Math.max(2, Math.ceil(viewportHeight / step) + 2);
42
+ const image = surfaceContext.getImageData(0, 0, surfaceCanvas.width, surfaceCanvas.height);
43
+ const alphaAt = (x, y) => {
44
+ const clampedX = Math.min(surfaceCanvas.width - 1, Math.max(0, x));
45
+ const clampedY = Math.min(surfaceCanvas.height - 1, Math.max(0, y));
46
+ const x0 = Math.floor(clampedX);
47
+ const y0 = Math.floor(clampedY);
48
+ const x1 = Math.min(surfaceCanvas.width - 1, x0 + 1);
49
+ const y1 = Math.min(surfaceCanvas.height - 1, y0 + 1);
50
+ const tx = clampedX - x0;
51
+ const ty = clampedY - y0;
52
+ const offset = (sampleX, sampleY) => image.data[(sampleY * surfaceCanvas.width + sampleX) * 4 + 3];
53
+ const top = offset(x0, y0) * (1 - tx) + offset(x1, y0) * tx;
54
+ const bottom = offset(x0, y1) * (1 - tx) + offset(x1, y1) * tx;
55
+ return top * (1 - ty) + bottom * ty;
56
+ };
57
+ const alpha = Array.from({ length: rows }, (_, y) => Array.from({ length: columns }, (_, x) => {
58
+ return alphaAt(x * step * dpr, y * step * dpr);
59
+ }));
60
+ const segments = [];
61
+ const interpolate = (from, to) => {
62
+ if (from === to)
63
+ return 0.5;
64
+ return Math.max(0, Math.min(1, (threshold - from) / (to - from)));
65
+ };
66
+ const edgePoint = (x, y, edge) => {
67
+ const topLeft = alpha[y][x];
68
+ const topRight = alpha[y][x + 1];
69
+ const bottomRight = alpha[y + 1][x + 1];
70
+ const bottomLeft = alpha[y + 1][x];
71
+ if (edge === 'top') {
72
+ const t = interpolate(topLeft, topRight);
73
+ return { x: (x + t) * step, y: y * step };
74
+ }
75
+ if (edge === 'right') {
76
+ const t = interpolate(topRight, bottomRight);
77
+ return { x: (x + 1) * step, y: (y + t) * step };
78
+ }
79
+ if (edge === 'bottom') {
80
+ const t = interpolate(bottomLeft, bottomRight);
81
+ return { x: (x + t) * step, y: (y + 1) * step };
82
+ }
83
+ const t = interpolate(topLeft, bottomLeft);
84
+ return { x: x * step, y: (y + t) * step };
85
+ };
86
+ const addSegment = (x, y, from, to) => segments.push([edgePoint(x, y, from), edgePoint(x, y, to)]);
87
+ for (let y = 0; y < rows - 1; y += 1) {
88
+ for (let x = 0; x < columns - 1; x += 1) {
89
+ const value = (alpha[y][x] >= threshold ? 8 : 0) |
90
+ (alpha[y][x + 1] >= threshold ? 4 : 0) |
91
+ (alpha[y + 1][x + 1] >= threshold ? 2 : 0) |
92
+ (alpha[y + 1][x] >= threshold ? 1 : 0);
93
+ if (value === 1 || value === 14)
94
+ addSegment(x, y, 'left', 'bottom');
95
+ else if (value === 2 || value === 13)
96
+ addSegment(x, y, 'bottom', 'right');
97
+ else if (value === 3 || value === 12)
98
+ addSegment(x, y, 'left', 'right');
99
+ else if (value === 4 || value === 11)
100
+ addSegment(x, y, 'top', 'right');
101
+ else if (value === 6 || value === 9)
102
+ addSegment(x, y, 'top', 'bottom');
103
+ else if (value === 7 || value === 8)
104
+ addSegment(x, y, 'left', 'top');
105
+ else if (value === 5) {
106
+ addSegment(x, y, 'top', 'left');
107
+ addSegment(x, y, 'bottom', 'right');
108
+ }
109
+ else if (value === 10) {
110
+ addSegment(x, y, 'left', 'bottom');
111
+ addSegment(x, y, 'top', 'right');
112
+ }
113
+ }
114
+ }
115
+ const adjacency = new Map();
116
+ segments.forEach(([start, end], index) => {
117
+ const startKey = pointKey(start);
118
+ const endKey = pointKey(end);
119
+ adjacency.set(startKey, [...(adjacency.get(startKey) ?? []), index]);
120
+ adjacency.set(endKey, [...(adjacency.get(endKey) ?? []), index]);
121
+ });
122
+ const unused = new Set(segments.map((_, index) => index));
123
+ const contours = [];
124
+ const takeConnectedSegment = (key) => (adjacency.get(key) ?? []).find((index) => unused.has(index));
125
+ while (unused.size > 0) {
126
+ const firstSegmentIndex = unused.values().next().value;
127
+ unused.delete(firstSegmentIndex);
128
+ const [start, end] = segments[firstSegmentIndex];
129
+ const points = [start, end];
130
+ let currentKey = pointKey(end);
131
+ let closed = false;
132
+ while (true) {
133
+ const nextSegmentIndex = takeConnectedSegment(currentKey);
134
+ if (nextSegmentIndex === undefined)
135
+ break;
136
+ unused.delete(nextSegmentIndex);
137
+ const [nextStart, nextEnd] = segments[nextSegmentIndex];
138
+ const nextPoint = pointKey(nextStart) === currentKey ? nextEnd : nextStart;
139
+ const nextKey = pointKey(nextPoint);
140
+ if (nextKey === pointKey(points[0])) {
141
+ closed = true;
142
+ break;
143
+ }
144
+ points.push(nextPoint);
145
+ currentKey = nextKey;
146
+ }
147
+ const prepared = simplifyPoints(points, simplify);
148
+ if (prepared.length > 2)
149
+ contours.push({ points: prepared, closed });
150
+ }
151
+ return contours;
152
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from './ImageMask.svelte';
2
+ export type { ImageMaskValue } from './types';
@@ -0,0 +1 @@
1
+ export { default } from './ImageMask.svelte';
@@ -0,0 +1,8 @@
1
+ import type { ContourPath } from './types';
2
+ export declare function drawMarchingAnts({ context, contours, viewportWidth, viewportHeight, dashOffset }: {
3
+ context: CanvasRenderingContext2D | null;
4
+ contours: ContourPath[];
5
+ viewportWidth: number;
6
+ viewportHeight: number;
7
+ dashOffset: number;
8
+ }): void;
@@ -0,0 +1,29 @@
1
+ function contourPath(context, contour) {
2
+ const { points, closed } = contour;
3
+ if (points.length === 0)
4
+ return;
5
+ context.beginPath();
6
+ context.moveTo(points[0].x, points[0].y);
7
+ for (const point of points.slice(1)) {
8
+ context.lineTo(point.x, point.y);
9
+ }
10
+ if (closed)
11
+ context.closePath();
12
+ }
13
+ export function drawMarchingAnts({ context, contours, viewportWidth, viewportHeight, dashOffset }) {
14
+ if (!context)
15
+ return;
16
+ context.clearRect(0, 0, viewportWidth, viewportHeight);
17
+ context.save();
18
+ context.lineCap = 'round';
19
+ context.lineJoin = 'round';
20
+ context.lineWidth = 2;
21
+ context.strokeStyle = 'rgb(0 0 0)';
22
+ context.setLineDash([8, 6]);
23
+ context.lineDashOffset = -dashOffset;
24
+ for (const contour of contours) {
25
+ contourPath(context, contour);
26
+ context.stroke();
27
+ }
28
+ context.restore();
29
+ }
@@ -0,0 +1,5 @@
1
+ import type { ImageMaskValue, MaskPoint, MaskStroke } from './types';
2
+ export declare function configureMaskContext(context: CanvasRenderingContext2D, dpr: number): void;
3
+ export declare function rebuildMaskSurface(context: CanvasRenderingContext2D | null, strokes: MaskStroke[], width: number, height: number): void;
4
+ export declare function drawMaskStrokeSegment(context: CanvasRenderingContext2D | null, previous: MaskPoint | null, from: MaskPoint, to: MaskPoint, stroke: Pick<MaskStroke, 'tool' | 'brushSize'>): void;
5
+ export declare function exportMaskValue(exportCanvas: HTMLCanvasElement | null, exportContext: CanvasRenderingContext2D | null, surfaceCanvas: HTMLCanvasElement | null, exportSize: number): ImageMaskValue | null;
@@ -0,0 +1,94 @@
1
+ export function configureMaskContext(context, dpr) {
2
+ context.setTransform(dpr, 0, 0, dpr, 0, 0);
3
+ context.lineCap = 'round';
4
+ context.lineJoin = 'round';
5
+ }
6
+ function smoothPath(context, points) {
7
+ if (points.length === 0)
8
+ return;
9
+ if (points.length < 3) {
10
+ context.beginPath();
11
+ context.moveTo(points[0].x, points[0].y);
12
+ for (const point of points.slice(1))
13
+ context.lineTo(point.x, point.y);
14
+ return;
15
+ }
16
+ context.beginPath();
17
+ context.moveTo(points[0].x, points[0].y);
18
+ for (let index = 0; index < points.length - 1; index += 1) {
19
+ const current = points[index];
20
+ const next = points[index + 1];
21
+ const previous = points[index - 1] ?? current;
22
+ const afterNext = points[index + 2] ?? next;
23
+ context.bezierCurveTo(current.x + (next.x - previous.x) / 6, current.y + (next.y - previous.y) / 6, next.x - (afterNext.x - current.x) / 6, next.y - (afterNext.y - current.y) / 6, next.x, next.y);
24
+ }
25
+ }
26
+ export function rebuildMaskSurface(context, strokes, width, height) {
27
+ if (!context)
28
+ return;
29
+ context.clearRect(0, 0, width, height);
30
+ for (const stroke of strokes) {
31
+ smoothPath(context, stroke.points);
32
+ context.lineWidth = stroke.brushSize;
33
+ context.globalCompositeOperation =
34
+ stroke.tool === 'erase' ? 'destination-out' : 'source-over';
35
+ context.strokeStyle = 'rgb(0 0 0)';
36
+ context.stroke();
37
+ }
38
+ context.globalCompositeOperation = 'source-over';
39
+ }
40
+ export function drawMaskStrokeSegment(context, previous, from, to, stroke) {
41
+ if (!context)
42
+ return;
43
+ context.save();
44
+ context.lineCap = 'round';
45
+ context.lineJoin = 'round';
46
+ context.lineWidth = stroke.brushSize;
47
+ context.globalCompositeOperation = stroke.tool === 'erase' ? 'destination-out' : 'source-over';
48
+ context.strokeStyle = 'rgb(0 0 0)';
49
+ if (from.x === to.x && from.y === to.y) {
50
+ context.beginPath();
51
+ context.arc(from.x, from.y, stroke.brushSize / 2, 0, Math.PI * 2);
52
+ context.fillStyle = 'rgb(0 0 0)';
53
+ context.fill();
54
+ context.restore();
55
+ return;
56
+ }
57
+ const start = previous
58
+ ? { x: (previous.x + from.x) / 2, y: (previous.y + from.y) / 2 }
59
+ : from;
60
+ const end = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
61
+ context.beginPath();
62
+ context.moveTo(start.x, start.y);
63
+ context.quadraticCurveTo(from.x, from.y, end.x, end.y);
64
+ context.stroke();
65
+ context.restore();
66
+ }
67
+ export function exportMaskValue(exportCanvas, exportContext, surfaceCanvas, exportSize) {
68
+ if (!exportCanvas || !exportContext || !surfaceCanvas)
69
+ return null;
70
+ const outputSize = Math.max(1, Math.round(exportSize));
71
+ exportCanvas.width = outputSize;
72
+ exportCanvas.height = outputSize;
73
+ exportContext.save();
74
+ exportContext.setTransform(1, 0, 0, 1, 0, 0);
75
+ exportContext.globalCompositeOperation = 'source-over';
76
+ exportContext.clearRect(0, 0, outputSize, outputSize);
77
+ exportContext.drawImage(surfaceCanvas, 0, 0, outputSize, outputSize);
78
+ exportContext.globalCompositeOperation = 'source-in';
79
+ exportContext.fillStyle = 'rgb(255 255 255)';
80
+ exportContext.fillRect(0, 0, outputSize, outputSize);
81
+ exportContext.globalCompositeOperation = 'destination-over';
82
+ exportContext.fillStyle = 'rgb(0 0 0)';
83
+ exportContext.fillRect(0, 0, outputSize, outputSize);
84
+ exportContext.restore();
85
+ return {
86
+ dataUrl: exportCanvas.toDataURL('image/png'),
87
+ width: outputSize,
88
+ height: outputSize,
89
+ mimeType: 'image/png',
90
+ format: 'black-white',
91
+ maskedColor: 'white',
92
+ unmaskedColor: 'black'
93
+ };
94
+ }
@@ -0,0 +1,23 @@
1
+ export type ImageMaskValue = {
2
+ dataUrl: string;
3
+ width: number;
4
+ height: number;
5
+ mimeType: 'image/png';
6
+ format: 'black-white';
7
+ maskedColor: 'white';
8
+ unmaskedColor: 'black';
9
+ };
10
+ export type MaskPoint = {
11
+ x: number;
12
+ y: number;
13
+ };
14
+ export type MaskTool = 'paint' | 'erase';
15
+ export type MaskStroke = {
16
+ tool: MaskTool;
17
+ points: MaskPoint[];
18
+ brushSize: number;
19
+ };
20
+ export type ContourPath = {
21
+ points: MaskPoint[];
22
+ closed: boolean;
23
+ };