// src/floodfill.js // // The magic-wand fill behind the "hide ぶるべー" mask. // // Kept free of the DOM so it can be tested with a plain Uint8ClampedArray: // `pixels` is RGBA row-major, `mask` is the same layout, and the fill writes // opaque white into `mask` for every 4-connected pixel whose colour is close // enough to the seed's. /** * How tolerant a slider value (0..100) is, in RGB distance. * * The scale runs to 1.5x the diagonal of the RGB cube so that the top of the * slider still merges neighbouring tones in a photo instead of stopping at * "almost white". */ export function toleranceDistance(tolerance) { const n = Number(tolerance); const value = Number.isFinite(n) ? Math.min(100, Math.max(0, n)) : 0; return (value / 100) * 255 * 1.5; } /** * Fill from `(x, y)`. * * @param {Uint8ClampedArray|Uint8Array} pixels RGBA source * @param {Uint8ClampedArray|Uint8Array} mask RGBA destination (white = hide) * @param {number} width * @param {number} height * @param {number} x seed column * @param {number} y seed row * @param {number} tolerance 0..100 * @returns {number} how many pixels were painted */ export function floodFill({ pixels, mask, width, height, x, y, tolerance = 32 }) { if (!pixels || !mask || !(width > 0) || !(height > 0)) return 0; const sx = Math.max(0, Math.min(width - 1, Math.round(Number(x) || 0))); const sy = Math.max(0, Math.min(height - 1, Math.round(Number(y) || 0))); const count = width * height; const start = sy * width + sx; const base = start * 4; const sr = pixels[base]; const sg = pixels[base + 1]; const sb = pixels[base + 2]; const limit = toleranceDistance(tolerance) ** 2 * 3; // 0 = untouched, 1 = queued, 2 = done. Marking on push keeps the stack to // one entry per pixel, so a preallocated buffer can never overflow. const seen = new Uint8Array(count); const stack = new Int32Array(count); let top = 0; let painted = 0; stack[top++] = start; seen[start] = 1; while (top > 0) { const p = stack[--top]; seen[p] = 2; const i = p * 4; const dr = pixels[i] - sr; const dg = pixels[i + 1] - sg; const db = pixels[i + 2] - sb; if (dr * dr + dg * dg + db * db > limit) continue; mask[i] = 255; mask[i + 1] = 255; mask[i + 2] = 255; mask[i + 3] = 255; painted++; const px = p % width; const py = (p - px) / width; if (px > 0 && !seen[p - 1]) { seen[p - 1] = 1; stack[top++] = p - 1; } if (px < width - 1 && !seen[p + 1]) { seen[p + 1] = 1; stack[top++] = p + 1; } if (py > 0 && !seen[p - width]) { seen[p - width] = 1; stack[top++] = p - width; } if (py < height - 1 && !seen[p + width]) { seen[p + width] = 1; stack[top++] = p + width; } } return painted; }