PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/tools/lasso-mask.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:00:41 UTC 6,215 bytes, 171 lines download raw bytes

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.