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

204 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { hexToRgb } from "./palette";
import type { PixelImage } from "./types";
import { createPixelImage } from "./types";
import { gaussianBlur } from "./convolution";
/** Детерминированный ГПСЧ (mulberry32): одинаковый seed — одинаковый результат. */
export function mulberry32(seed: number): () => number {
let a = seed >>> 0;
return () => {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function clampByte(v: number): number {
return v < 0 ? 0 : v > 255 ? 255 : Math.round(v);
}
/** Усреднение каждого блока blockSize×BlockSize в его верхний-левый пиксель цвета. */
export function pixelate(img: PixelImage, blockSize: number): PixelImage {
const bs = Math.max(1, Math.round(blockSize));
const out = createPixelImage(img.width, img.height);
for (let by = 0; by < img.height; by += bs) {
for (let bx = 0; bx < img.width; bx += bs) {
let r = 0;
let g = 0;
let b = 0;
let n = 0;
const yMax = Math.min(by + bs, img.height);
const xMax = Math.min(bx + bs, img.width);
for (let y = by; y < yMax; y++) {
for (let x = bx; x < xMax; x++) {
const i = (y * img.width + x) * 4;
r += img.data[i];
g += img.data[i + 1];
b += img.data[i + 2];
n++;
}
}
if (n === 0) continue;
r /= n;
g /= n;
b /= n;
for (let y = by; y < yMax; y++) {
for (let x = bx; x < xMax; x++) {
const i = (y * img.width + x) * 4;
out.data[i] = r;
out.data[i + 1] = g;
out.data[i + 2] = b;
out.data[i + 3] = img.data[i + 3];
}
}
}
}
return out;
}
/** Перемешивает блоки blockSize×Blocksize между собой детерминированно по seed. */
export function shuffleBlocks(
img: PixelImage,
blockSize: number,
seed: number,
): PixelImage {
const bs = Math.max(1, Math.round(blockSize));
const cols = Math.ceil(img.width / bs);
const rows = Math.ceil(img.height / bs);
const total = cols * rows;
const rng = mulberry32(seed);
const order = Array.from({ length: total }, (_, i) => i);
for (let i = total - 1; i > 0; i--) {
const j = Math.floor(rng() * (i + 1));
[order[i], order[j]] = [order[j], order[i]];
}
const out = createPixelImage(img.width, img.height);
for (let i = 0; i < total; i++) {
const src = order[i];
const sx = (i % cols) * bs;
const sy = Math.floor(i / cols) * bs;
const dx = (src % cols) * bs;
const dy = Math.floor(src / cols) * bs;
for (let yy = 0; yy < bs; yy++) {
for (let xx = 0; xx < bs; xx++) {
if (sy + yy >= img.height || sx + xx >= img.width) continue;
if (dy + yy >= img.height || dx + xx >= img.width) continue;
const si = ((sy + yy) * img.width + sx + xx) * 4;
const di = ((dy + yy) * img.width + dx + xx) * 4;
for (let ch = 0; ch < 4; ch++) out.data[di + ch] = img.data[si + ch];
}
}
}
return out;
}
export type NoiseMode = "mono" | "color";
/** Зерно: amountPercent — сила отклонения от оригинала. Детерминировано по seed. */
export function addNoise(
img: PixelImage,
amountPercent: number,
mode: NoiseMode,
seed: number,
): PixelImage {
const out = createPixelImage(img.width, img.height);
const amount = Math.min(Math.max(amountPercent, 0), 100) / 100;
const rng = mulberry32(seed);
for (let i = 0; i < img.data.length; i += 4) {
const shift = (rng() * 2 - 1) * amount * 255;
if (mode === "mono") {
out.data[i] = clampByte(img.data[i] + shift);
out.data[i + 1] = clampByte(img.data[i + 1] + shift);
out.data[i + 2] = clampByte(img.data[i + 2] + shift);
} else {
out.data[i] = clampByte(img.data[i] + (rng() * 2 - 1) * amount * 255);
out.data[i + 1] = clampByte(
img.data[i + 1] + (rng() * 2 - 1) * amount * 255,
);
out.data[i + 2] = clampByte(
img.data[i + 2] + (rng() * 2 - 1) * amount * 255,
);
}
out.data[i + 3] = img.data[i + 3];
}
return out;
}
/** Размытие только альфа-канала: мягкие края при неизменном цвете. */
export function featherAlpha(img: PixelImage, radius: number): PixelImage {
const gray = createPixelImage(img.width, img.height);
for (let i = 0; i < img.data.length; i += 4) {
gray.data[i] = img.data[i + 3];
gray.data[i + 1] = img.data[i + 3];
gray.data[i + 2] = img.data[i + 3];
gray.data[i + 3] = 255;
}
const blurred = gaussianBlur(gray, radius);
const out = createPixelImage(img.width, img.height);
for (let i = 0; i < img.data.length; i += 4) {
out.data[i] = img.data[i];
out.data[i + 1] = img.data[i + 1];
out.data[i + 2] = img.data[i + 2];
out.data[i + 3] = blurred.data[i];
}
return out;
}
/**
* Убирает цветную кайму на полупрозрачных краях: RGB полупрозрачного пикселя
* заменяется цветом ближайшего полностью непрозрачного соседа в пределах radius.
*/
export function defringe(img: PixelImage, radius: number): PixelImage {
const out = createPixelImage(img.width, img.height);
out.data.set(img.data);
const rad = Math.max(1, Math.round(radius));
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const di = (y * img.width + x) * 4;
const a = img.data[di + 3];
if (a === 0 || a === 255) continue;
let found = false;
for (let ry = 1; ry <= rad && !found; ry++) {
for (let dy = -ry; dy <= ry && !found; dy++) {
for (let dx = -ry; dx <= ry && !found; dx++) {
if (Math.max(Math.abs(dx), Math.abs(dy)) !== ry) continue;
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || ny < 0 || nx >= img.width || ny >= img.height)
continue;
const si = (ny * img.width + nx) * 4;
if (img.data[si + 3] !== 255) continue;
out.data[di] = img.data[si];
out.data[di + 1] = img.data[si + 1];
out.data[di + 2] = img.data[si + 2];
found = true;
}
}
}
}
}
return out;
}
/** Силуэт: все видимые пиксели заливаются одним цветом, альфа сохраняется. */
export function silhouette(
img: PixelImage,
colorHex: string,
alphaThreshold: number,
): PixelImage {
const out = createPixelImage(img.width, img.height);
const { r, g, b } = hexToRgb(colorHex);
for (let i = 0; i < img.data.length; i += 4) {
if (img.data[i + 3] <= alphaThreshold) continue;
out.data[i] = r;
out.data[i + 1] = g;
out.data[i + 2] = b;
out.data[i + 3] = img.data[i + 3];
}
return out;
}