@x33025/sveltely 0.1.0 → 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.
Files changed (102) hide show
  1. package/dist/components/Library/Button/Button.demo.svelte +5 -3
  2. package/dist/components/Library/Button/Button.demo.svelte.d.ts +1 -0
  3. package/dist/components/Library/Calendar/Calendar.demo.svelte +2 -14
  4. package/dist/components/Library/Calendar/Calendar.svelte +54 -50
  5. package/dist/components/Library/Divider/Divider.svelte +10 -0
  6. package/dist/components/Library/Divider/Divider.svelte.d.ts +26 -0
  7. package/dist/components/Library/Divider/index.d.ts +1 -0
  8. package/dist/components/Library/Divider/index.js +1 -0
  9. package/dist/components/Library/Dropdown/Dropdown.demo.svelte +37 -2
  10. package/dist/components/Library/Dropdown/Dropdown.svelte +55 -34
  11. package/dist/components/Library/Dropdown/Dropdown.svelte.d.ts +1 -1
  12. package/dist/components/Library/Dropdown/index.d.ts +1 -1
  13. package/dist/components/Library/Dropdown/types.d.ts +4 -1
  14. package/dist/components/Library/Floating/Floating.svelte +35 -1
  15. package/dist/components/Library/ForEach/ForEach.svelte +14 -0
  16. package/dist/components/Library/ForEach/ForEach.svelte.d.ts +28 -0
  17. package/dist/components/Library/ForEach/index.d.ts +1 -0
  18. package/dist/components/Library/ForEach/index.js +1 -0
  19. package/dist/components/Library/Grid/Grid.svelte +74 -0
  20. package/dist/components/Library/Grid/Grid.svelte.d.ts +13 -0
  21. package/dist/components/Library/Grid/index.d.ts +1 -0
  22. package/dist/components/Library/Grid/index.js +1 -0
  23. package/dist/components/Library/GridItem/GridItem.svelte +65 -0
  24. package/dist/components/Library/GridItem/GridItem.svelte.d.ts +14 -0
  25. package/dist/components/Library/GridItem/index.d.ts +1 -0
  26. package/dist/components/Library/GridItem/index.js +1 -0
  27. package/dist/components/Library/HStack/HStack.svelte +45 -0
  28. package/dist/components/Library/HStack/HStack.svelte.d.ts +9 -0
  29. package/dist/components/Library/HStack/index.d.ts +1 -0
  30. package/dist/components/Library/HStack/index.js +1 -0
  31. package/dist/components/Library/Image/Image.demo.svelte +18 -0
  32. package/dist/components/Library/Image/Image.demo.svelte.d.ts +23 -0
  33. package/dist/components/Library/Image/Image.svelte +57 -0
  34. package/dist/components/Library/Image/Image.svelte.d.ts +17 -0
  35. package/dist/components/Library/Image/ImagePlaceholder.svelte +202 -0
  36. package/dist/components/Library/Image/ImagePlaceholder.svelte.d.ts +7 -0
  37. package/dist/components/Library/Image/index.d.ts +1 -0
  38. package/dist/components/Library/Image/index.js +1 -0
  39. package/dist/components/Library/ImageMask/BrushPreview.svelte +119 -0
  40. package/dist/components/Library/ImageMask/BrushPreview.svelte.d.ts +11 -0
  41. package/dist/components/Library/ImageMask/ImageMask.demo.svelte +117 -0
  42. package/dist/components/Library/ImageMask/ImageMask.demo.svelte.d.ts +10 -0
  43. package/dist/components/Library/ImageMask/ImageMask.svelte +46 -0
  44. package/dist/components/Library/ImageMask/ImageMask.svelte.d.ts +20 -0
  45. package/dist/components/Library/ImageMask/MaskLayer.svelte +341 -0
  46. package/dist/components/Library/ImageMask/MaskLayer.svelte.d.ts +12 -0
  47. package/dist/components/Library/ImageMask/contour.d.ts +11 -0
  48. package/dist/components/Library/ImageMask/contour.js +152 -0
  49. package/dist/components/Library/ImageMask/index.d.ts +2 -0
  50. package/dist/components/Library/ImageMask/index.js +1 -0
  51. package/dist/components/Library/ImageMask/marchingAnts.d.ts +8 -0
  52. package/dist/components/Library/ImageMask/marchingAnts.js +29 -0
  53. package/dist/components/Library/ImageMask/maskSurface.d.ts +5 -0
  54. package/dist/components/Library/ImageMask/maskSurface.js +94 -0
  55. package/dist/components/Library/ImageMask/types.d.ts +23 -0
  56. package/dist/components/Library/ImageMask/types.js +1 -0
  57. package/dist/components/Library/Label/Label.demo.svelte +28 -0
  58. package/dist/components/Library/Label/Label.demo.svelte.d.ts +9 -0
  59. package/dist/components/Library/Label/Label.svelte +177 -0
  60. package/dist/components/Library/Label/Label.svelte.d.ts +18 -0
  61. package/dist/components/Library/Label/index.d.ts +1 -0
  62. package/dist/components/Library/Label/index.js +1 -0
  63. package/dist/components/Library/NumberField/NumberField.demo.svelte +21 -0
  64. package/dist/components/Library/NumberField/NumberField.demo.svelte.d.ts +8 -0
  65. package/dist/components/Library/NumberField/NumberField.svelte +194 -0
  66. package/dist/components/Library/NumberField/NumberField.svelte.d.ts +21 -0
  67. package/dist/components/Library/NumberField/index.d.ts +1 -0
  68. package/dist/components/Library/NumberField/index.js +1 -0
  69. package/dist/components/Library/ScrollView/ScrollView.svelte +25 -9
  70. package/dist/components/Library/ScrollView/ScrollView.svelte.d.ts +4 -4
  71. package/dist/components/Library/Spacer/Spacer.svelte +7 -0
  72. package/dist/components/Library/Spacer/Spacer.svelte.d.ts +26 -0
  73. package/dist/components/Library/Spacer/index.d.ts +1 -0
  74. package/dist/components/Library/Spacer/index.js +1 -0
  75. package/dist/components/Library/TextField/TextField.demo.svelte +14 -0
  76. package/dist/components/Library/TextField/TextField.demo.svelte.d.ts +8 -0
  77. package/dist/components/Library/TextField/TextField.svelte +149 -0
  78. package/dist/components/Library/TextField/TextField.svelte.d.ts +19 -0
  79. package/dist/components/Library/TextField/index.d.ts +1 -0
  80. package/dist/components/Library/TextField/index.js +1 -0
  81. package/dist/components/Library/VStack/VStack.svelte +45 -0
  82. package/dist/components/Library/VStack/VStack.svelte.d.ts +9 -0
  83. package/dist/components/Library/VStack/index.d.ts +1 -0
  84. package/dist/components/Library/VStack/index.js +1 -0
  85. package/dist/components/Local/ComponentGrid.svelte +15 -31
  86. package/dist/components/Local/HeroCard.svelte +26 -36
  87. package/dist/components/Local/HeroCard.svelte.d.ts +0 -2
  88. package/dist/index.d.ts +23 -0
  89. package/dist/index.js +17 -0
  90. package/dist/style/index.css +28 -17
  91. package/dist/style/label.d.ts +6 -0
  92. package/dist/style/label.js +4 -0
  93. package/dist/style/layout.d.ts +57 -0
  94. package/dist/style/layout.js +128 -0
  95. package/dist/style/media.d.ts +12 -0
  96. package/dist/style/media.js +8 -0
  97. package/dist/style/scroll.d.ts +7 -0
  98. package/dist/style/scroll.js +5 -0
  99. package/dist/style/text-editor.d.ts +34 -0
  100. package/dist/style/text-editor.js +29 -0
  101. package/dist/style.css +58 -35
  102. package/package.json +1 -1
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ import type { MaskPoint, MaskTool } from './types';
3
+
4
+ let {
5
+ point = null,
6
+ visible = false,
7
+ brushSize = 24,
8
+ tool = 'paint',
9
+ sizeLabelVisible = false
10
+ }: {
11
+ point?: MaskPoint | null;
12
+ visible?: boolean;
13
+ brushSize?: number;
14
+ tool?: MaskTool;
15
+ sizeLabelVisible?: boolean;
16
+ } = $props();
17
+
18
+ const brushPreviewStyle = $derived.by(() => {
19
+ if (!point) return '';
20
+ return [
21
+ `width: ${brushSize}px;`,
22
+ `height: ${brushSize}px;`,
23
+ `transform: translate(${point.x - brushSize / 2}px, ${point.y - brushSize / 2}px);`
24
+ ].join(' ');
25
+ });
26
+ const brushSizeLabel = $derived(`${Math.round(brushSize)}px`);
27
+ const brushSizePreviewStyle = $derived(`width: ${brushSize}px; height: ${brushSize}px;`);
28
+ </script>
29
+
30
+ {#if visible && point}
31
+ <div
32
+ class="brush-preview"
33
+ class:brush-preview-erase={tool === 'erase'}
34
+ style={brushPreviewStyle}
35
+ ></div>
36
+ {/if}
37
+
38
+ {#if sizeLabelVisible}
39
+ <div
40
+ class="brush-size-preview"
41
+ class:brush-size-preview-erase={tool === 'erase'}
42
+ aria-hidden="true"
43
+ >
44
+ <div class="brush-size-preview-circle" style={brushSizePreviewStyle}></div>
45
+ <span class="brush-size-preview-label">{brushSizeLabel}</span>
46
+ </div>
47
+ {/if}
48
+
49
+ <style>
50
+ .brush-preview {
51
+ position: absolute;
52
+ top: 0;
53
+ left: 0;
54
+ z-index: 11;
55
+ box-sizing: border-box;
56
+ pointer-events: none;
57
+ border: 1.5px solid rgb(255 255 255 / 0.95);
58
+ border-radius: 9999px;
59
+ background: color-mix(in oklab, var(--sveltely-primary-color) 16%, transparent);
60
+ box-shadow:
61
+ 0 0 0 1px color-mix(in oklab, var(--sveltely-primary-color) 90%, transparent),
62
+ 0 1px 4px rgb(0 0 0 / 0.22);
63
+ will-change: transform, width, height;
64
+ }
65
+
66
+ .brush-preview-erase {
67
+ background: rgb(255 255 255 / 0.18);
68
+ box-shadow:
69
+ 0 0 0 1px rgb(24 24 27 / 0.8),
70
+ 0 1px 4px rgb(0 0 0 / 0.22);
71
+ }
72
+
73
+ .brush-size-preview {
74
+ position: absolute;
75
+ top: 50%;
76
+ left: 50%;
77
+ z-index: 12;
78
+ display: inline-flex;
79
+ flex-direction: column;
80
+ align-items: center;
81
+ justify-content: center;
82
+ gap: 0.5rem;
83
+ pointer-events: none;
84
+ transform: translate(-50%, -50%);
85
+ }
86
+
87
+ .brush-size-preview-circle {
88
+ box-sizing: border-box;
89
+ border-radius: 9999px;
90
+ border: 1.5px solid rgb(255 255 255 / 0.95);
91
+ background: color-mix(in oklab, var(--sveltely-primary-color) 20%, transparent);
92
+ box-shadow:
93
+ 0 0 0 1px color-mix(in oklab, var(--sveltely-primary-color) 95%, transparent),
94
+ 0 8px 24px rgb(0 0 0 / 0.24);
95
+ }
96
+
97
+ .brush-size-preview-label {
98
+ border-radius: 9999px;
99
+ background: rgb(255 255 255 / 0.88);
100
+ color: var(--color-zinc-950);
101
+ font-size: 0.75rem;
102
+ font-weight: 650;
103
+ line-height: 1;
104
+ padding: 0.25rem 0.375rem;
105
+ box-shadow: 0 1px 6px rgb(0 0 0 / 0.16);
106
+ text-shadow:
107
+ 0 1px 0 rgb(255 255 255 / 0.9),
108
+ 0 -1px 0 rgb(255 255 255 / 0.9),
109
+ 1px 0 0 rgb(255 255 255 / 0.9),
110
+ -1px 0 0 rgb(255 255 255 / 0.9);
111
+ }
112
+
113
+ .brush-size-preview-erase .brush-size-preview-circle {
114
+ background: rgb(255 255 255 / 0.2);
115
+ box-shadow:
116
+ 0 0 0 1px color-mix(in oklab, var(--sveltely-primary-color) 85%, transparent),
117
+ 0 8px 24px rgb(0 0 0 / 0.24);
118
+ }
119
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { MaskPoint, MaskTool } from './types';
2
+ type $$ComponentProps = {
3
+ point?: MaskPoint | null;
4
+ visible?: boolean;
5
+ brushSize?: number;
6
+ tool?: MaskTool;
7
+ sizeLabelVisible?: boolean;
8
+ };
9
+ declare const BrushPreview: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type BrushPreview = ReturnType<typeof BrushPreview>;
11
+ export default BrushPreview;
@@ -0,0 +1,117 @@
1
+ <script module lang="ts">
2
+ export const demo = {
3
+ name: 'ImageMask',
4
+ description: 'Image mask editor that exposes the exported mask through bind:mask.',
5
+ columnSpan: 2,
6
+ rowSpan: 2
7
+ };
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ import { Brush, Eraser, X } from '@lucide/svelte';
12
+ import Slider from '../Slider';
13
+ import ImageMask from './ImageMask.svelte';
14
+ import type { ImageMaskValue, MaskTool } from './types';
15
+
16
+ let mask = $state<ImageMaskValue | null>(null);
17
+ let brushSize = $state(24);
18
+ let tool = $state<MaskTool>('paint');
19
+ let clearRevision = $state(0);
20
+ </script>
21
+
22
+ <div class="grid items-start gap-3 md:grid-cols-[minmax(0,18rem)_minmax(8rem,1fr)]">
23
+ <ImageMask
24
+ src="https://images.unsplash.com/photo-1542291026-7eec264c27ff?auto=format&fit=crop&w=700&q=80"
25
+ alt="Red sneaker"
26
+ bind:mask
27
+ bind:brushSize
28
+ bind:tool
29
+ bind:clearRevision
30
+ fit="cover"
31
+ class="aspect-square"
32
+ />
33
+ <div class="vstack gap-2 text-sm text-zinc-600">
34
+ <div class="font-medium text-zinc-900">Mask</div>
35
+ <div class="aspect-square overflow-hidden rounded-md border border-zinc-200 bg-black">
36
+ {#if mask}
37
+ <img src={mask.dataUrl} alt="Exported mask" class="size-full object-contain" />
38
+ {/if}
39
+ </div>
40
+ <label class="vstack gap-2 bg-white p-3">
41
+ <span class="font-medium text-zinc-900">Brush size {brushSize}px</span>
42
+ <Slider bind:value={brushSize} min={6} max={72} step={1} />
43
+ <div class="image-mask-controls">
44
+ <button
45
+ type="button"
46
+ class:active={tool === 'paint'}
47
+ aria-label="Paint mask"
48
+ onclick={() => {
49
+ tool = 'paint';
50
+ }}
51
+ >
52
+ <Brush size={16} />
53
+ </button>
54
+ <button
55
+ type="button"
56
+ class:active={tool === 'erase'}
57
+ aria-label="Erase mask"
58
+ disabled={!mask}
59
+ onclick={() => {
60
+ tool = 'erase';
61
+ }}
62
+ >
63
+ <Eraser size={16} />
64
+ </button>
65
+ <div class="h-5 w-px bg-zinc-200"></div>
66
+ <button
67
+ type="button"
68
+ aria-label="Clear mask"
69
+ disabled={!mask}
70
+ onclick={() => {
71
+ mask = null;
72
+ clearRevision += 1;
73
+ tool = 'paint';
74
+ }}
75
+ >
76
+ <X size={16} />
77
+ </button>
78
+ </div>
79
+ </label>
80
+ </div>
81
+ </div>
82
+
83
+ <style>
84
+ .image-mask-controls {
85
+ display: inline-flex;
86
+ width: fit-content;
87
+ align-items: center;
88
+ gap: 0.25rem;
89
+ background: white;
90
+ padding: 0.25rem;
91
+ }
92
+
93
+ .image-mask-controls button {
94
+ display: inline-flex;
95
+ width: 2rem;
96
+ height: 2rem;
97
+ align-items: center;
98
+ justify-content: center;
99
+ border-radius: 9999px;
100
+ color: var(--color-zinc-500);
101
+ transition:
102
+ background-color 150ms,
103
+ color 150ms,
104
+ opacity 150ms;
105
+ }
106
+
107
+ .image-mask-controls button:hover:not(:disabled),
108
+ .image-mask-controls button.active {
109
+ background: var(--sveltely-primary-color);
110
+ color: white;
111
+ }
112
+
113
+ .image-mask-controls button:disabled {
114
+ cursor: not-allowed;
115
+ opacity: 0.4;
116
+ }
117
+ </style>
@@ -0,0 +1,10 @@
1
+ export declare const demo: {
2
+ name: string;
3
+ description: string;
4
+ columnSpan: number;
5
+ rowSpan: number;
6
+ };
7
+ import ImageMask from './ImageMask.svelte';
8
+ declare const ImageMask: import("svelte").Component<Record<string, never>, {}, "">;
9
+ type ImageMask = ReturnType<typeof ImageMask>;
10
+ export default ImageMask;
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import Image from '../Image';
3
+ import type { ImageFit, ImageLoading } from '../../../style/media';
4
+ import MaskLayer from './MaskLayer.svelte';
5
+ import type { ImageMaskValue, MaskTool } from './types';
6
+
7
+ let {
8
+ src = null,
9
+ alt = '',
10
+ mask = $bindable<ImageMaskValue | null>(null),
11
+ fit = 'contain',
12
+ busy = false,
13
+ disabled = false,
14
+ placeholderKey = 'default',
15
+ loading = 'lazy',
16
+ brushSize = $bindable(24),
17
+ tool = $bindable<MaskTool>('paint'),
18
+ clearRevision = $bindable(0),
19
+ exportSize = 2048,
20
+ class: className = ''
21
+ }: {
22
+ src?: string | null;
23
+ alt?: string;
24
+ mask?: ImageMaskValue | null;
25
+ fit?: ImageFit;
26
+ busy?: boolean;
27
+ disabled?: boolean;
28
+ placeholderKey?: string;
29
+ loading?: ImageLoading;
30
+ brushSize?: number;
31
+ tool?: MaskTool;
32
+ clearRevision?: number;
33
+ exportSize?: number;
34
+ class?: string;
35
+ } = $props();
36
+
37
+ const canEdit = $derived(Boolean(src) && !busy && !disabled);
38
+ </script>
39
+
40
+ <div class={`image-mask relative ${className}`}>
41
+ <Image {src} {alt} {fit} {busy} {placeholderKey} {loading} class="size-full">
42
+ {#if canEdit}
43
+ <MaskLayer bind:mask {tool} {brushSize} {exportSize} {disabled} {clearRevision} />
44
+ {/if}
45
+ </Image>
46
+ </div>
@@ -0,0 +1,20 @@
1
+ import type { ImageFit, ImageLoading } from '../../../style/media';
2
+ import type { ImageMaskValue, MaskTool } from './types';
3
+ type $$ComponentProps = {
4
+ src?: string | null;
5
+ alt?: string;
6
+ mask?: ImageMaskValue | null;
7
+ fit?: ImageFit;
8
+ busy?: boolean;
9
+ disabled?: boolean;
10
+ placeholderKey?: string;
11
+ loading?: ImageLoading;
12
+ brushSize?: number;
13
+ tool?: MaskTool;
14
+ clearRevision?: number;
15
+ exportSize?: number;
16
+ class?: string;
17
+ };
18
+ declare const ImageMask: import("svelte").Component<$$ComponentProps, {}, "mask" | "brushSize" | "tool" | "clearRevision">;
19
+ type ImageMask = ReturnType<typeof ImageMask>;
20
+ export default ImageMask;
@@ -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-primary-color').trim() ||
149
+ 'var(--sveltely-primary-color)'
150
+ : 'var(--sveltely-primary-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[];