aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/floodfill.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-22 23:26:02 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-22 23:26:02 +0900
commit2ced83398189ac47588ddde87fdbf5cae106aa4d (patch)
treef8e22708d5f3261e0a890632d0e8800bd132c07f /public/bluebey-studio/src/floodfill.js
parent6258fcb99337aa596f576a9275a867e0b9b2841f (diff)
bluebey-studio: 背景写真に「隠すマスク」を追加
Diffstat (limited to 'public/bluebey-studio/src/floodfill.js')
-rw-r--r--public/bluebey-studio/src/floodfill.js80
1 files changed, 80 insertions, 0 deletions
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;
+}