1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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;
}
|