Files
easy-png-tools2/web/src/lib/core/morphology.ts
T

223 lines
5.6 KiB
TypeScript

import { parseHex } from "./alpha";
import { clonePixelImage, createPixelImage, type PixelImage } from "./types";
type Mask = Uint8Array;
type Offset = { dx: number; dy: number };
function discOffsets(radius: number): Offset[] {
const offsets: Offset[] = [];
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
if (dx * dx + dy * dy <= radius * radius) {
offsets.push({ dx, dy });
}
}
}
return offsets;
}
export function buildAlphaMask(img: PixelImage): Mask {
const mask = new Uint8Array(img.width * img.height);
for (let i = 0; i < mask.length; i++) {
mask[i] = img.data[i * 4 + 3] > 0 ? 1 : 0;
}
return mask;
}
export function dilateMask(
mask: Mask,
width: number,
height: number,
radius: number,
): Mask {
const r = Math.trunc(radius);
if (r < 1) return mask.slice();
const out = new Uint8Array(mask.length);
const offsets = discOffsets(r);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let hit = 0;
for (const { dx, dy } of offsets) {
const sx = x + dx;
const sy = y + dy;
if (sx < 0 || sy < 0 || sx >= width || sy >= height) continue;
if (mask[sy * width + sx] === 1) {
hit = 1;
break;
}
}
out[y * width + x] = hit;
}
}
return out;
}
export function erodeMask(
mask: Mask,
width: number,
height: number,
radius: number,
): Mask {
const r = Math.trunc(radius);
if (r < 1) return mask.slice();
const out = new Uint8Array(mask.length);
const offsets = discOffsets(r);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let solid = 1;
for (const { dx, dy } of offsets) {
const sx = x + dx;
const sy = y + dy;
if (sx < 0 || sy < 0 || sx >= width || sy >= height) continue;
if (mask[sy * width + sx] !== 1) {
solid = 0;
break;
}
}
out[y * width + x] = solid;
}
}
return out;
}
export function applyMaskAlpha(img: PixelImage, mask: Mask): PixelImage {
const out = createPixelImage(img.width, img.height);
for (let i = 0; i < mask.length; i++) {
const di = i * 4;
if (mask[i] === 1) {
out.data[di] = img.data[di];
out.data[di + 1] = img.data[di + 1];
out.data[di + 2] = img.data[di + 2];
out.data[di + 3] = 255;
} else {
out.data[di + 3] = 0;
}
}
return out;
}
export function dilateImage(img: PixelImage, radiusPx: number): PixelImage {
if (radiusPx < 1) return clonePixelImage(img);
return applyMaskAlpha(
img,
dilateMask(buildAlphaMask(img), img.width, img.height, radiusPx),
);
}
export function erodeImage(img: PixelImage, radiusPx: number): PixelImage {
if (radiusPx < 1) return clonePixelImage(img);
return applyMaskAlpha(
img,
erodeMask(buildAlphaMask(img), img.width, img.height, radiusPx),
);
}
export function strokeImage(
img: PixelImage,
radiusPx: number,
colorHex: string,
): PixelImage {
const r = Math.trunc(radiusPx);
if (r < 1) return clonePixelImage(img);
const [cr, cg, cb] = parseHex(colorHex);
const mask = buildAlphaMask(img);
const ring = dilateMask(mask, img.width, img.height, r);
const out = createPixelImage(img.width, img.height);
for (let i = 0; i < ring.length; i++) {
const di = i * 4;
if (ring[i] === 1 && mask[i] === 0) {
out.data[di] = cr;
out.data[di + 1] = cg;
out.data[di + 2] = cb;
out.data[di + 3] = 255;
} else if (mask[i] === 1) {
out.data[di] = img.data[di];
out.data[di + 1] = img.data[di + 1];
out.data[di + 2] = img.data[di + 2];
out.data[di + 3] = img.data[di + 3];
}
}
return out;
}
export function contourImage(
img: PixelImage,
radiusPx: number,
colorHex: string,
): PixelImage {
const r = Math.max(1, Math.trunc(radiusPx));
const [cr, cg, cb] = parseHex(colorHex);
const mask = buildAlphaMask(img);
const inner = erodeMask(mask, img.width, img.height, r);
const line = new Uint8Array(mask.length);
for (let i = 0; i < mask.length; i++) {
line[i] = mask[i] === 1 && inner[i] === 0 ? 1 : 0;
}
const out = createPixelImage(img.width, img.height);
for (let i = 0; i < line.length; i++) {
const di = i * 4;
if (line[i] === 1) {
out.data[di] = cr;
out.data[di + 1] = cg;
out.data[di + 2] = cb;
out.data[di + 3] = 255;
}
}
return out;
}
export function openingMask(
mask: Mask,
w: number,
h: number,
radius: number,
): Mask {
return dilateMask(erodeMask(mask, w, h, radius), w, h, radius);
}
export function closingMask(
mask: Mask,
w: number,
h: number,
radius: number,
): Mask {
return erodeMask(dilateMask(mask, w, h, radius), w, h, radius);
}
export function openingImage(img: PixelImage, radiusPx: number): PixelImage {
if (radiusPx < 1) return clonePixelImage(img);
const mask = buildAlphaMask(img);
const opened = openingMask(mask, img.width, img.height, radiusPx);
const out = createPixelImage(img.width, img.height);
for (let i = 0; i < opened.length; i++) {
const di = i * 4;
out.data[di] = img.data[di];
out.data[di + 1] = img.data[di + 1];
out.data[di + 2] = img.data[di + 2];
out.data[di + 3] = opened[i] === 1 ? img.data[di + 3] : 0;
}
return out;
}
export function closingImage(img: PixelImage, radiusPx: number): PixelImage {
if (radiusPx < 1) return clonePixelImage(img);
const mask = buildAlphaMask(img);
const closed = closingMask(mask, img.width, img.height, radiusPx);
const out = createPixelImage(img.width, img.height);
for (let i = 0; i < closed.length; i++) {
const di = i * 4;
if (closed[i] === 1 && mask[i] === 0) {
out.data[di] = 0;
out.data[di + 1] = 0;
out.data[di + 2] = 0;
} else {
out.data[di] = img.data[di];
out.data[di + 1] = img.data[di + 1];
out.data[di + 2] = img.data[di + 2];
}
out.data[di + 3] = closed[i] === 1 ? 255 : img.data[di + 3];
}
return out;
}