function jumpFloodBuffer(pixels, w, h, radius = 0, feather = 0) {
const t0 = performance.now();
const n = w * h;
const pixelsIn = pixels.slice();
const EMPTY = new Uint32Array([-1])[0];
const closest = new Uint32Array(w * h);
for (let i = 0; i < n; i++) {
closest[i] = pixels[i] > 128 ? i : EMPTY;
}
const maxRadius = radius + feather * 0.5;
const maxStep =
2 ** Math.ceil(Math.log2(Math.min(maxRadius / 2, Math.max(w, h))));
for (let step = maxStep; step >= 1; step >>= 1) {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const k = x + w * y;
let bestSeed = EMPTY;
for (let i = -step, bestDist = Infinity; i <= step; i += step) {
const y0 = y + i;
if (y0 < 0 || y0 >= h) continue;
for (let j = -step; j <= step; j += step) {
const x0 = x + j;
if (x0 < 0 || x0 >= w) continue;
const m = x0 + w * y0;
if (pixels[m] === 0) continue;
const seed = closest[m];
const dx = x - (seed % w);
const dy = y - ((seed / w) | 0);
const d = dx * dx + dy * dy;
if (d < bestDist) {
bestDist = d;
bestSeed = seed;
}
}
}
if (bestSeed === EMPTY) continue;
closest[k] = bestSeed;
pixels[k] = pixels[bestSeed];
}
}
}
const f = Math.max(feather, 1e-8);
const a = radius - f * 0.5;
const b = radius + f * 0.5;
for (let y0 = 0; y0 < h; y0++) {
for (let x0 = 0; x0 < w; x0++) {
const idx = x0 + w * y0;
const closestIdx = closest[idx];
if (closestIdx === EMPTY) {
pixels[idx] = 0;
continue;
}
const dx = x0 - (closestIdx % w);
const dy = y0 - ((closestIdx / w) | 0);
const dist = Math.sqrt(dx * dx + dy * dy);
pixels[idx] = 255 * linearstep(b, a, dist);
}
}
const t1 = performance.now();
mutable jumpFloodMs = Math.round(10 * (t1 - t0)) * 0.1;
return pixels;
}