mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 21:46:35 +00:00
223 lines
5.6 KiB
TypeScript
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;
|
|
}
|