funky vibe-coded app to test out hex-grid based outlines
1

Configure Feed

Select the types of activity you want to include in your feed.

Add drag painting for hex selection

Allow users to click and drag across hexes to add or erase selections, update pointer handling for touch
input, and add JS typings so pnpm check passes.

Mihai (Jun 3, 2026, 2:02 AM +0200) bb05f069 cc8e9243

+195 -41
+1
package.json
··· 16 16 "@sveltejs/adapter-auto": "^7.0.1", 17 17 "@sveltejs/kit": "^2.57.0", 18 18 "@sveltejs/vite-plugin-svelte": "^7.0.0", 19 + "@types/node": "^25.9.1", 19 20 "svelte": "^5.55.2", 20 21 "svelte-check": "^4.4.6", 21 22 "typescript": "^6.0.2",
+31 -15
pnpm-lock.yaml
··· 10 10 devDependencies: 11 11 '@sveltejs/adapter-auto': 12 12 specifier: ^7.0.1 13 - version: 7.0.1(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16))(svelte@5.56.0)(typescript@6.0.3)(vite@8.0.16)) 13 + version: 7.0.1(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16(@types/node@25.9.1)))(svelte@5.56.0)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.1))) 14 14 '@sveltejs/kit': 15 15 specifier: ^2.57.0 16 - version: 2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16))(svelte@5.56.0)(typescript@6.0.3)(vite@8.0.16) 16 + version: 2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16(@types/node@25.9.1)))(svelte@5.56.0)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.1)) 17 17 '@sveltejs/vite-plugin-svelte': 18 18 specifier: ^7.0.0 19 - version: 7.1.2(svelte@5.56.0)(vite@8.0.16) 19 + version: 7.1.2(svelte@5.56.0)(vite@8.0.16(@types/node@25.9.1)) 20 + '@types/node': 21 + specifier: ^25.9.1 22 + version: 25.9.1 20 23 svelte: 21 24 specifier: ^5.55.2 22 25 version: 5.56.0 ··· 28 31 version: 6.0.3 29 32 vite: 30 33 specifier: ^8.0.7 31 - version: 8.0.16 34 + version: 8.0.16(@types/node@25.9.1) 32 35 33 36 packages: 34 37 ··· 212 215 '@types/estree@1.0.9': 213 216 resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} 214 217 218 + '@types/node@25.9.1': 219 + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} 220 + 215 221 '@types/trusted-types@2.0.7': 216 222 resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} 217 223 ··· 442 448 engines: {node: '>=14.17'} 443 449 hasBin: true 444 450 451 + undici-types@7.24.6: 452 + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} 453 + 445 454 vite@8.0.16: 446 455 resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} 447 456 engines: {node: ^20.19.0 || >=22.12.0} ··· 601 610 dependencies: 602 611 acorn: 8.16.0 603 612 604 - '@sveltejs/adapter-auto@7.0.1(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16))(svelte@5.56.0)(typescript@6.0.3)(vite@8.0.16))': 613 + '@sveltejs/adapter-auto@7.0.1(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16(@types/node@25.9.1)))(svelte@5.56.0)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.1)))': 605 614 dependencies: 606 - '@sveltejs/kit': 2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16))(svelte@5.56.0)(typescript@6.0.3)(vite@8.0.16) 615 + '@sveltejs/kit': 2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16(@types/node@25.9.1)))(svelte@5.56.0)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.1)) 607 616 608 - '@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16))(svelte@5.56.0)(typescript@6.0.3)(vite@8.0.16)': 617 + '@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16(@types/node@25.9.1)))(svelte@5.56.0)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.1))': 609 618 dependencies: 610 619 '@standard-schema/spec': 1.1.0 611 620 '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) 612 - '@sveltejs/vite-plugin-svelte': 7.1.2(svelte@5.56.0)(vite@8.0.16) 621 + '@sveltejs/vite-plugin-svelte': 7.1.2(svelte@5.56.0)(vite@8.0.16(@types/node@25.9.1)) 613 622 '@types/cookie': 0.6.0 614 623 acorn: 8.16.0 615 624 cookie: 0.6.0 ··· 621 630 set-cookie-parser: 3.1.0 622 631 sirv: 3.0.2 623 632 svelte: 5.56.0 624 - vite: 8.0.16 633 + vite: 8.0.16(@types/node@25.9.1) 625 634 optionalDependencies: 626 635 typescript: 6.0.3 627 636 628 - '@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16)': 637 + '@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.0)(vite@8.0.16(@types/node@25.9.1))': 629 638 dependencies: 630 639 deepmerge: 4.3.1 631 640 magic-string: 0.30.21 632 641 obug: 2.1.1 633 642 svelte: 5.56.0 634 - vite: 8.0.16 635 - vitefu: 1.1.3(vite@8.0.16) 643 + vite: 8.0.16(@types/node@25.9.1) 644 + vitefu: 1.1.3(vite@8.0.16(@types/node@25.9.1)) 636 645 637 646 '@tybys/wasm-util@0.10.2': 638 647 dependencies: ··· 642 651 '@types/cookie@0.6.0': {} 643 652 644 653 '@types/estree@1.0.9': {} 654 + 655 + '@types/node@25.9.1': 656 + dependencies: 657 + undici-types: 7.24.6 645 658 646 659 '@types/trusted-types@2.0.7': {} 647 660 ··· 839 852 840 853 typescript@6.0.3: {} 841 854 842 - vite@8.0.16: 855 + undici-types@7.24.6: {} 856 + 857 + vite@8.0.16(@types/node@25.9.1): 843 858 dependencies: 844 859 lightningcss: 1.32.0 845 860 picomatch: 4.0.4 ··· 847 862 rolldown: 1.0.3 848 863 tinyglobby: 0.2.17 849 864 optionalDependencies: 865 + '@types/node': 25.9.1 850 866 fsevents: 2.3.3 851 867 852 - vitefu@1.1.3(vite@8.0.16): 868 + vitefu@1.1.3(vite@8.0.16(@types/node@25.9.1)): 853 869 optionalDependencies: 854 - vite: 8.0.16 870 + vite: 8.0.16(@types/node@25.9.1) 855 871 856 872 zimmerframe@1.1.4: {}
+36 -5
src/lib/export.js
··· 3 3 // plus the merged green outline, rotated by the current angle and cropped to a 4 4 // tight bounding box with a little padding. 5 5 6 + /** 7 + * @typedef {[number, number]} Point 8 + * @typedef {{points: Point[]}} HexLike 9 + * @typedef {{width: number, height: number, polys: string[], paths: string[]}} Shape 10 + */ 11 + 12 + /** @param {number} n */ 6 13 const r2 = (n) => Math.round(n * 100) / 100; 7 14 15 + /** @param {Point} point @param {number} deg @returns {Point} */ 8 16 function rotate([x, y], deg) { 9 17 const a = (deg * Math.PI) / 180; 10 18 const c = Math.cos(a); ··· 16 24 * Produce renderable shape data: dimensions plus polygon/path geometry already 17 25 * offset into a (0,0)-anchored box. Returns null when there's nothing selected. 18 26 * 19 - * @param {Array<{points: Array<[number, number]>}>} hexes 20 - * @param {Array<Array<[number, number]>>} outlines 27 + * @param {HexLike[]} hexes 28 + * @param {Point[][]} outlines 21 29 * @param {number} angle 30 + * @param {number} [pad] 31 + * @returns {Shape | null} 22 32 */ 23 33 export function buildShape(hexes, outlines, angle, pad = 10) { 24 34 if (!hexes.length) return null; ··· 41 51 42 52 const ox = pad - minX; 43 53 const oy = pad - minY; 54 + /** @param {Point} point @returns {Point} */ 44 55 const off = ([x, y]) => [r2(x + ox), r2(y + oy)]; 45 56 46 57 const polys = hx.map((poly) => poly.map(off).map((p) => `${p[0]},${p[1]}`).join(' ')); ··· 57 68 }; 58 69 } 59 70 60 - /** Serialize shape data to a standalone SVG string. */ 71 + /** 72 + * Serialize shape data to a standalone SVG string. 73 + * @param {Shape} shape 74 + * @param {{background?: boolean}} [options] 75 + */ 61 76 export function shapeSvgString(shape, { background = false } = {}) { 62 77 const bg = background ? `<rect width="${shape.width}" height="${shape.height}" fill="#ffffff"/>` : ''; 63 78 const hexes = shape.polys ··· 72 87 ); 73 88 } 74 89 90 + /** @param {string} href @param {string} filename */ 75 91 function download(href, filename) { 76 92 const a = document.createElement('a'); 77 93 a.href = href; ··· 81 97 a.remove(); 82 98 } 83 99 100 + /** @param {Shape | null} shape @param {string} [filename] */ 84 101 export function exportSvg(shape, filename = 'hexshape.svg') { 85 102 if (!shape) return; 86 103 const blob = new Blob([shapeSvgString(shape)], { type: 'image/svg+xml' }); ··· 89 106 URL.revokeObjectURL(url); 90 107 } 91 108 109 + /** @param {Shape | null} shape @param {string} [filename] @param {number} [scale] */ 92 110 export function exportPng(shape, filename = 'hexshape.png', scale = 2) { 93 111 if (!shape) return Promise.resolve(); 112 + /** @type {Promise<void>} */ 94 113 return new Promise((resolve, reject) => { 95 114 const blob = new Blob([shapeSvgString(shape, { background: true })], { type: 'image/svg+xml' }); 96 115 const url = URL.createObjectURL(blob); ··· 100 119 canvas.width = Math.ceil(shape.width * scale); 101 120 canvas.height = Math.ceil(shape.height * scale); 102 121 const ctx = canvas.getContext('2d'); 122 + if (!ctx) { 123 + URL.revokeObjectURL(url); 124 + reject(new Error('Could not create canvas context')); 125 + return; 126 + } 103 127 ctx.scale(scale, scale); 104 128 ctx.drawImage(img, 0, 0); 105 129 URL.revokeObjectURL(url); 106 130 canvas.toBlob((b) => { 131 + if (!b) { 132 + reject(new Error('Could not encode PNG')); 133 + return; 134 + } 107 135 const u = URL.createObjectURL(b); 108 136 download(u, filename); 109 137 URL.revokeObjectURL(u); 110 - resolve(); 138 + resolve(undefined); 111 139 }, 'image/png'); 112 140 }; 113 - img.onerror = reject; 141 + img.onerror = () => { 142 + URL.revokeObjectURL(url); 143 + reject(new Error('Could not load generated SVG')); 144 + }; 114 145 img.src = url; 115 146 }); 116 147 }
+59 -14
src/lib/hex.js
··· 12 12 // finer than any visible difference while erasing float noise. 13 13 const Q = 1000; 14 14 15 + /** 16 + * @typedef {[number, number]} Point 17 + * @typedef {{id: string, cx?: number, cy?: number, points: Point[]}} Hex 18 + * @typedef {{count: number, ak: string, bk: string, a: Point, b: Point}} Edge 19 + */ 20 + 21 + /** @param {Point} p */ 15 22 function vkey(p) { 16 23 return `${Math.round(p[0] * Q)},${Math.round(p[1] * Q)}`; 17 24 } 18 25 19 26 /** 20 27 * The six corner points of a flat-top hex centered at (cx, cy). 21 - * @returns {Array<[number, number]>} 28 + * @param {number} cx 29 + * @param {number} cy 30 + * @param {number} size 31 + * @returns {Point[]} 22 32 */ 23 33 function hexCorners(cx, cy, size) { 34 + /** @type {Point[]} */ 24 35 const pts = []; 25 36 for (let i = 0; i < 6; i++) { 26 37 const ang = (Math.PI / 180) * (60 * i); ··· 34 45 * even after it is rotated about its center (we over-cover by the diagonal so 35 46 * the corners never reveal empty space at any angle). 36 47 * 37 - * @returns {Array<{id: string, cx: number, cy: number, points: Array<[number, number]>}>} 48 + * @param {number} width 49 + * @param {number} height 50 + * @param {number} size 51 + * @returns {Hex[]} 38 52 */ 39 53 export function generateGrid(width, height, size) { 40 54 if (!width || !height || size <= 0) return []; ··· 52 66 const cHalf = Math.ceil(colCount / 2); 53 67 const rHalf = Math.ceil(rowCount / 2); 54 68 69 + /** @type {Hex[]} */ 55 70 const hexes = []; 56 71 for (let c = -cHalf; c <= cHalf; c++) { 57 72 for (let r = -rHalf; r <= rHalf; r++) { ··· 70 85 * Centers are laid out about the origin — absolute position doesn't matter to 71 86 * callers, which crop to the shape's bounding box anyway. 72 87 * 73 - * @returns {Array<{id: string, points: Array<[number, number]>}>} 88 + * @param {string[]} ids 89 + * @param {number} size 90 + * @returns {Hex[]} 74 91 */ 75 92 export function buildHexesFromIds(ids, size) { 76 93 const colStep = 1.5 * size; ··· 83 100 }); 84 101 } 85 102 86 - /** Turn a hex's points into the string an SVG <polygon> expects. */ 103 + /** 104 + * Turn a hex's points into the string an SVG <polygon> expects. 105 + * @param {Point[]} points 106 + */ 87 107 export function pointsString(points) { 88 108 return points.map((p) => `${p[0]},${p[1]}`).join(' '); 89 109 } ··· 96 116 * We collect those boundary edges and stitch them into ordered closed loops so 97 117 * each connected cluster — and any hole — becomes its own continuous path. 98 118 * 99 - * @returns {Array<Array<[number, number]>>} an array of loops (each a list of points) 119 + * @param {Hex[]} hexes 120 + * @returns {Point[][]} an array of loops (each a list of points) 100 121 */ 101 122 export function computeOutlines(hexes) { 102 - /** @type {Map<string, {count: number, ak: string, bk: string, a: [number, number], b: [number, number]}>} */ 123 + /** @type {Map<string, Edge>} */ 103 124 const edges = new Map(); 104 125 105 126 for (const h of hexes) { ··· 119 140 // Keep only boundary edges, and build a vertex adjacency map for stitching. 120 141 /** @type {Map<string, Array<{ek: string, to: string}>>} */ 121 142 const adj = new Map(); 122 - /** @type {Map<string, [number, number]>} */ 143 + /** @type {Map<string, Point>} */ 123 144 const coord = new Map(); 145 + /** @type {Set<string>} */ 124 146 const remaining = new Set(); 125 147 126 148 for (const [ek, e] of edges) { ··· 128 150 remaining.add(ek); 129 151 coord.set(e.ak, e.a); 130 152 coord.set(e.bk, e.b); 131 - if (!adj.has(e.ak)) adj.set(e.ak, []); 132 - if (!adj.has(e.bk)) adj.set(e.bk, []); 133 - adj.get(e.ak).push({ ek, to: e.bk }); 134 - adj.get(e.bk).push({ ek, to: e.ak }); 153 + let aNeighbors = adj.get(e.ak); 154 + if (!aNeighbors) { 155 + aNeighbors = []; 156 + adj.set(e.ak, aNeighbors); 157 + } 158 + let bNeighbors = adj.get(e.bk); 159 + if (!bNeighbors) { 160 + bNeighbors = []; 161 + adj.set(e.bk, bNeighbors); 162 + } 163 + aNeighbors.push({ ek, to: e.bk }); 164 + bNeighbors.push({ ek, to: e.ak }); 135 165 } 136 166 167 + /** @type {Point[][]} */ 137 168 const loops = []; 138 169 while (remaining.size) { 139 170 const startEk = remaining.values().next().value; 171 + if (!startEk) break; 140 172 const e0 = edges.get(startEk); 173 + if (!e0) { 174 + remaining.delete(startEk); 175 + continue; 176 + } 141 177 remaining.delete(startEk); 142 178 143 179 const startK = e0.ak; 144 180 let curK = e0.bk; 145 - const loop = [coord.get(e0.ak), coord.get(e0.bk)]; 181 + const a = coord.get(e0.ak); 182 + const b = coord.get(e0.bk); 183 + if (!a || !b) continue; 184 + /** @type {Point[]} */ 185 + const loop = [a, b]; 146 186 147 187 while (curK !== startK) { 148 188 const neighbors = adj.get(curK) || []; ··· 155 195 } 156 196 if (!next) break; // safety: shouldn't happen for a closed boundary 157 197 remaining.delete(next.ek); 158 - loop.push(coord.get(next.to)); 198 + const nextPoint = coord.get(next.to); 199 + if (!nextPoint) break; 200 + loop.push(nextPoint); 159 201 curK = next.to; 160 202 } 161 203 loops.push(loop); ··· 164 206 return loops; 165 207 } 166 208 167 - /** Convert a stitched loop into an SVG path string. */ 209 + /** 210 + * Convert a stitched loop into an SVG path string. 211 + * @param {Point[]} loop 212 + */ 168 213 export function outlinePath(loop) { 169 214 if (!loop.length) return ''; 170 215 const [first, ...rest] = loop;
+68 -7
src/routes/+page.svelte
··· 9 9 pointsString, 10 10 outlinePath 11 11 } from '$lib/hex.js'; 12 - import { buildShape, shapeSvgString, exportSvg, exportPng } from '$lib/export.js'; 12 + import { buildShape, exportSvg, exportPng } from '$lib/export.js'; 13 13 14 14 const LIB_KEY = 'hexabounds:library'; 15 + 16 + /** @typedef {{ key: string, name: string, ids: string[], size: number, angle: number }} LibraryItem */ 15 17 16 18 // --- settings --- 17 19 let size = $state(44); // hex radius in px 18 20 let angle = $state(0); // rotation of the whole grid, 0 = flat top/bottom 19 21 20 22 // --- selection --- 23 + /** @type {SvelteSet<string>} */ 21 24 const selected = new SvelteSet(); 25 + let painting = $state(false); 26 + /** @type {'add' | 'erase'} */ 27 + let paintMode = $state('add'); 28 + /** @type {number | null} */ 29 + let paintPointerId = $state(null); 22 30 23 31 // --- viewport (the area the grid fills) --- 24 32 let stageW = $state(0); ··· 32 40 ); 33 41 34 42 // --- library --- 43 + /** @type {LibraryItem[]} */ 35 44 let library = $state([]); 36 45 37 46 onMount(() => { ··· 47 56 if (browser) localStorage.setItem(LIB_KEY, JSON.stringify(library)); 48 57 } 49 58 50 - function toggle(id) { 51 - if (selected.has(id)) selected.delete(id); 52 - else selected.add(id); 59 + /** @param {string} id */ 60 + function applyPaint(id) { 61 + if (paintMode === 'add') selected.add(id); 62 + else selected.delete(id); 63 + } 64 + 65 + /** @param {PointerEvent} event @param {string} id */ 66 + function startPainting(event, id) { 67 + if (event.isPrimary === false) return; 68 + if (event.pointerType === 'mouse' && event.button !== 0) return; 69 + 70 + event.preventDefault(); 71 + if (event.currentTarget instanceof Element) { 72 + try { 73 + event.currentTarget.releasePointerCapture(event.pointerId); 74 + } catch { 75 + // The pointer may not have been captured (e.g. mouse input). 76 + } 77 + } 78 + painting = true; 79 + paintPointerId = event.pointerId; 80 + paintMode = selected.has(id) ? 'erase' : 'add'; 81 + applyPaint(id); 82 + } 83 + 84 + /** @param {PointerEvent} event */ 85 + function paintFromPointer(event) { 86 + if (!painting || event.pointerId !== paintPointerId) return; 87 + 88 + const target = document.elementFromPoint(event.clientX, event.clientY); 89 + if (!(target instanceof Element)) return; 90 + 91 + const hex = target.closest('.hex'); 92 + if (hex instanceof SVGElement && hex.dataset.id) applyPaint(hex.dataset.id); 93 + } 94 + 95 + /** @param {PointerEvent | undefined} event */ 96 + function stopPainting(event = undefined) { 97 + if (event?.pointerId != null && paintPointerId != null && event.pointerId !== paintPointerId) return; 98 + painting = false; 99 + paintPointerId = null; 53 100 } 54 101 55 102 function clear() { ··· 69 116 persist(); 70 117 } 71 118 119 + /** @param {LibraryItem} item */ 72 120 function loadShape(item) { 73 121 selected.clear(); 74 122 for (const id of item.ids) selected.add(id); ··· 76 124 angle = item.angle; 77 125 } 78 126 127 + /** @param {string} key */ 79 128 function deleteShape(key) { 80 129 library = library.filter((i) => i.key !== key); 81 130 persist(); 82 131 } 83 132 133 + /** @param {LibraryItem} item @param {string} name */ 84 134 function rename(item, name) { 85 135 item.name = name; 86 136 library = library; ··· 88 138 } 89 139 90 140 // A small preview shape for a library item, rebuilt from its saved ids. 141 + /** @param {LibraryItem} item */ 91 142 function thumb(item) { 92 143 const hexes = buildHexesFromIds(item.ids, item.size); 93 144 return buildShape(hexes, computeOutlines(hexes), item.angle, 6); 94 145 } 95 146 </script> 96 147 148 + <svelte:window 149 + onpointermove={paintFromPointer} 150 + onpointerup={stopPainting} 151 + onpointercancel={stopPainting} 152 + /> 153 + 97 154 <div class="app"> 98 155 <aside class="panel"> 99 156 <header> 100 157 <h1>hexabounds</h1> 101 - <p class="hint">Click hexes to outline them. Touching hexes share one outline.</p> 158 + <p class="hint">Click or drag across hexes to paint. Start on a selected hex to erase.</p> 102 159 </header> 103 160 104 161 <label class="control"> ··· 172 229 173 230 <main class="stage" bind:clientWidth={stageW} bind:clientHeight={stageH}> 174 231 {#if stageW && stageH} 175 - <svg width={stageW} height={stageH} role="presentation"> 232 + <svg width={stageW} height={stageH} role="presentation" onpointerleave={stopPainting}> 176 233 <g transform="rotate({angle} {stageW / 2} {stageH / 2})"> 177 234 {#each grid as hex (hex.id)} 235 + <!-- svelte-ignore a11y_no_static_element_interactions --> 178 236 <polygon 179 237 class="hex" 180 238 class:selected={selected.has(hex.id)} 239 + data-id={hex.id} 181 240 points={pointsString(hex.points)} 182 - onclick={() => toggle(hex.id)} 241 + onpointerdown={(e) => startPainting(e, hex.id)} 183 242 /> 184 243 {/each} 185 244 ··· 410 469 411 470 svg { 412 471 display: block; 472 + touch-action: none; 473 + user-select: none; 413 474 } 414 475 415 476 .hex {