From 2ced83398189ac47588ddde87fdbf5cae106aa4d Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:26:02 +0900 Subject: bluebey-studio: 背景写真に「隠すマスク」を追加 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/bluebey-studio/src/floodfill.js | 80 ++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 public/bluebey-studio/src/floodfill.js (limited to 'public/bluebey-studio/src/floodfill.js') diff --git a/public/bluebey-studio/src/floodfill.js b/public/bluebey-studio/src/floodfill.js new file mode 100644 index 0000000..ef50d1d --- /dev/null +++ b/public/bluebey-studio/src/floodfill.js @@ -0,0 +1,80 @@ +// 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; +} -- cgit v1.3.1