Files
2026-05-31 03:04:30 +07:00

112 lines
3.0 KiB
TypeScript

type RGBA = [number, number, number, number];
function colorDistance(a: RGBA, b: RGBA): number {
// Weight alpha at 25% so transparent regions fill correctly
return (
Math.abs(a[0] - b[0]) +
Math.abs(a[1] - b[1]) +
Math.abs(a[2] - b[2]) +
Math.abs(a[3] - b[3]) * 0.25
);
}
function matchesTarget(pixel: RGBA, target: RGBA, tolerance: number): boolean {
// Fully transparent pixels all match each other regardless of RGB
if (target[3] === 0) return pixel[3] <= tolerance;
return colorDistance(pixel, target) <= tolerance;
}
function hexToRgba(hex: string): RGBA {
const clean = hex.replace("#", "");
const r = parseInt(clean.slice(0, 2), 16);
const g = parseInt(clean.slice(2, 4), 16);
const b = parseInt(clean.slice(4, 6), 16);
const a = clean.length === 8 ? parseInt(clean.slice(6, 8), 16) : 255;
return [r, g, b, a];
}
export function floodFillDataUrl(
sourceUri: string,
px: number,
py: number,
fillColor: string,
tolerance: number = 32,
): Promise<string> {
return new Promise((resolve, reject) => {
const image = new Image();
image.crossOrigin = "anonymous";
image.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(new Error("Cannot get canvas context"));
return;
}
ctx.drawImage(image, 0, 0);
const { width, height } = canvas;
const data = ctx.getImageData(0, 0, width, height);
const pixels = data.data;
const x = Math.round(px);
const y = Math.round(py);
if (x < 0 || x >= width || y < 0 || y >= height) {
resolve(sourceUri);
return;
}
const idx = (y * width + x) * 4;
const target: RGBA = [
pixels[idx],
pixels[idx + 1],
pixels[idx + 2],
pixels[idx + 3],
];
const fill = hexToRgba(fillColor);
if (matchesTarget(target, fill, 0)) {
resolve(sourceUri);
return;
}
const visited = new Uint8Array(width * height);
const stack: number[] = [x + y * width];
while (stack.length > 0) {
const pos = stack.pop()!;
if (visited[pos]) continue;
visited[pos] = 1;
const cx = pos % width;
const cy = Math.floor(pos / width);
const ci = pos * 4;
const current: RGBA = [
pixels[ci],
pixels[ci + 1],
pixels[ci + 2],
pixels[ci + 3],
];
if (!matchesTarget(current, target, tolerance)) continue;
pixels[ci] = fill[0];
pixels[ci + 1] = fill[1];
pixels[ci + 2] = fill[2];
pixels[ci + 3] = fill[3];
if (cx > 0) stack.push(pos - 1);
if (cx < width - 1) stack.push(pos + 1);
if (cy > 0) stack.push(pos - width);
if (cy < height - 1) stack.push(pos + width);
}
ctx.putImageData(data, 0, 0);
resolve(canvas.toDataURL("image/png"));
};
image.onerror = reject;
image.src = sourceUri;
});
}