1/** 2 * Lasso-tool pixel & path helpers. 3 * 4 * All functions take the lasso polygon `points` as an explicit 5 * argument so they can be tested in isolation. The legacy gallery 6 * editor calls them with its module-level `_lassoPoints` array. 7 */ 8 9/** 10 * Shift each polygon vertex along the outward normal by `grow` pixels. 11 * Used by the lasso overlay (to draw the "feather" halo) and by 12 * `buildLassoMask` (to bake the grown polygon into the mask). 13 * 14 * @param {{x: number, y: number}[]} points Polygon vertices in draw order. 15 * @param {number} grow Positive = expand outward, negative = contract. 16 * @returns {{x: number, y: number}[]} New array (same length, original is not mutated). 17 */ 18export function lassoOffsetPoints(points, grow) { 19 const n = points.length; 20 if (n < 3 || !grow) return points; 21 // Polygon winding (positive = CCW) â flip the normal so it points 22 // away from the interior regardless of draw direction. 23 let area = 0; 24 for (let i = 0; i < n; i++) { 25 const p = points[i], q = points[(i + 1) % n]; 26 area += (q.x - p.x) * (q.y + p.y); 27 } 28 const sign = area > 0 ? 1 : -1; 29 const out = new Array(n); 30 for (let i = 0; i < n; i++) { 31 const a = points[(i - 1 + n) % n], b = points[i], c = points[(i + 1) % n]; 32 const e1x = b.x - a.x, e1y = b.y - a.y; 33 const e2x = c.x - b.x, e2y = c.y - b.y; 34 const l1 = Math.hypot(e1x, e1y) || 1; 35 const l2 = Math.hypot(e2x, e2y) || 1; 36 // Perpendicular (dy, -dx); flip via `sign` for outward direction. 37 const n1x = (e1y / l1) * sign, n1y = (-e1x / l1) * sign; 38 const n2x = (e2y / l2) * sign, n2y = (-e2x / l2) * sign; 39 const nx = (n1x + n2x) / 2; 40 const ny = (n1y + n2y) / 2; 41 const nl = Math.hypot(nx, ny) || 1; 42 out[i] = { x: b.x + (nx / nl) * grow, y: b.y + (ny / nl) * grow }; 43 } 44 return out; 45} 46 47 48/** 49 * Trace the lasso polygon on the given context (move-to + line-to, 50 * closed). Caller is responsible for `stroke()` / `fill()` choice. 51 */ 52export function getLassoPath(ctx, points) { 53 if (!points || points.length < 1) return; 54 ctx.beginPath(); 55 ctx.moveTo(points[0].x, points[0].y); 56 for (let i = 1; i < points.length; i++) { 57 ctx.lineTo(points[i].x, points[i].y); 58 } 59 ctx.closePath(); 60} 61 62 63/** 64 * Build a (optionally feathered, optionally grown) selection mask 65 * from a lasso polygon. 66 * 67 * @param {{x: number, y: number}[]} points Polygon vertices. 68 * @param {number} w / h Output canvas dimensions. 69 * @param {number} offX / offY Translate the polygon by (offX, offY) before rasterising. 70 * @param {number} feather Feather width in pixels. 0 = hard edge. 71 * @param {number} grow Positive = dilate the polygon, negative = erode. 72 * @returns {HTMLCanvasElement} A `w à h` canvas with alpha = selection strength. 73 */ 74export function buildLassoMask(points, w, h, offX, offY, feather, grow) { 75 // Step 1: draw hard mask 76 const hard = document.createElement('canvas'); 77 hard.width = w; hard.height = h; 78 const hCtx = hard.getContext('2d'); 79 hCtx.beginPath(); 80 hCtx.moveTo(points[0].x - offX, points[0].y - offY); 81 for (let i = 1; i < points.length; i++) { 82 hCtx.lineTo(points[i].x - offX, points[i].y - offY); 83 } 84 hCtx.closePath(); 85 hCtx.fillStyle = '#fff'; 86 hCtx.fill(); 87 88 // Step 1b: grow / shrink â blur the hard mask, threshold low for 89 // grow and high for shrink. Same technique as the bg-remove edge 90 // tuner. RGB is left alone, alpha is replaced. 91 if (grow && grow !== 0) { 92 const blurC = document.createElement('canvas'); 93 blurC.width = w; blurC.height = h; 94 const bctx = blurC.getContext('2d'); 95 bctx.filter = `blur(${Math.abs(grow)}px)`; 96 bctx.drawImage(hard, 0, 0); 97 bctx.filter = 'none'; 98 const blurred = bctx.getImageData(0, 0, w, h).data; 99 const hd = hCtx.getImageData(0, 0, w, h); 100 const out = hd.data; 101 const thr = grow > 0 ? 32 : 200; 102 for (let i = 0; i < out.length; i += 4) { 103 const a = blurred[i + 3] >= thr ? 255 : 0; 104 out[i] = a; out[i + 1] = a; out[i + 2] = a; out[i + 3] = a; 105 } 106 hCtx.putImageData(hd, 0, 0); 107 } 108 109 if (feather <= 0) return hard; 110 111 // Step 2: pixel data and distance-based feather. 112 const hardData = hCtx.getImageData(0, 0, w, h); 113 const d = hardData.data; 114 115 // Build inside/outside map. 116 const inside = new Uint8Array(w * h); 117 for (let i = 0; i < w * h; i++) { 118 inside[i] = d[i * 4] > 128 ? 1 : 0; 119 } 120 121 // Distance from edge (for pixels inside the selection, distance to nearest outside pixel). 122 const dist = new Float32Array(w * h); 123 dist.fill(feather + 1); 124 125 // Seed: edge pixels (inside pixels adjacent to outside pixels). 126 for (let y = 0; y < h; y++) { 127 for (let x = 0; x < w; x++) { 128 const i = y * w + x; 129 if (!inside[i]) { dist[i] = 0; continue; } 130 const hasOutside = (x > 0 && !inside[i-1]) || (x < w-1 && !inside[i+1]) || 131 (y > 0 && !inside[(y-1)*w+x]) || (y < h-1 && !inside[(y+1)*w+x]); 132 if (hasOutside) dist[i] = 1; 133 } 134 } 135 136 // Two-pass chamfer distance transform. 137 for (let y = 0; y < h; y++) { 138 for (let x = 0; x < w; x++) { 139 const i = y * w + x; 140 if (dist[i] === 0) continue; 141 if (x > 0) dist[i] = Math.min(dist[i], dist[i-1] + 1); 142 if (y > 0) dist[i] = Math.min(dist[i], dist[(y-1)*w+x] + 1); 143 } 144 } 145 for (let y = h-1; y >= 0; y--) { 146 for (let x = w-1; x >= 0; x--) { 147 const i = y * w + x; 148 if (dist[i] === 0) continue; 149 if (x < w-1) dist[i] = Math.min(dist[i], dist[i+1] + 1); 150 if (y < h-1) dist[i] = Math.min(dist[i], dist[(y+1)*w+x] + 1); 151 } 152 } 153 154 // Pixels near the edge get reduced alpha. 155 const result = document.createElement('canvas'); 156 result.width = w; result.height = h; 157 const rCtx = result.getContext('2d'); 158 const rData = rCtx.createImageData(w, h); 159 160 for (let i = 0; i < w * h; i++) { 161 if (!inside[i]) continue; 162 const edgeDist = dist[i];
163 const alpha = edgeDist >= feather ? 255 : Math.round((edgeDist / feather) * 255); 164 rData.data[i*4] = alpha; 165 rData.data[i*4+1] = alpha; 166 rData.data[i*4+2] = alpha; 167 rData.data[i*4+3] = 255; 168 } 169 rCtx.putImageData(rData, 0, 0); 170 return result; 171}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.