mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 13:36:36 +00:00
feat: add pixel effects core tools
This commit is contained in:
@@ -37,7 +37,7 @@
|
|||||||
Ядро: bbox по альфе (trim) переиспользуется тремя инструментами источника.
|
Ядро: bbox по альфе (trim) переиспользуется тремя инструментами источника.
|
||||||
Состав: trim-empty-space (закрывает remove border/space), change-canvas-size, change-aspect-ratio, landscape↔portrait, symmetric-copy.
|
Состав: trim-empty-space (закрывает remove border/space), change-canvas-size, change-aspect-ratio, landscape↔portrait, symmetric-copy.
|
||||||
|
|
||||||
### W6. Эффекты лёгкие — 7 инструментов, M
|
### W6. Эффекты лёгкие — ВЫПОЛНЕНА (6 инструментов; color-blocks покрыт pixelate)
|
||||||
|
|
||||||
Состав: pixelate, color-blocks, randomize-pixels (seed), add-noise, feather-edges, clean-edges, silhouette.
|
Состав: pixelate, color-blocks, randomize-pixels (seed), add-noise, feather-edges, clean-edges, silhouette.
|
||||||
Shadow/glow — сюда же, если потянет этап: оба = размытая альфа + смещение + цвет (ядро blur уже есть).
|
Shadow/glow — сюда же, если потянет этап: оба = размытая альфа + смещение + цвет (ядро blur уже есть).
|
||||||
|
|||||||
+8
-9
@@ -35,6 +35,8 @@
|
|||||||
- find-contour-png — color, thickness
|
- find-contour-png — color, thickness
|
||||||
- make-thicker-png / make-thinner-png — radius
|
- make-thicker-png / make-thinner-png — radius
|
||||||
- harden-alpha-png — threshold
|
- harden-alpha-png — threshold
|
||||||
|
- feather-edges-png — radius (размытие только альфы)
|
||||||
|
- clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного)
|
||||||
- despeckle-alpha-png / close-holes-png — radius
|
- despeckle-alpha-png / close-holes-png — radius
|
||||||
- center-by-alpha-png — без параметров
|
- center-by-alpha-png — без параметров
|
||||||
- round-corners-png — radius
|
- round-corners-png — radius
|
||||||
@@ -90,6 +92,10 @@
|
|||||||
- sharpen-png — strength
|
- sharpen-png — strength
|
||||||
- vignette-png — strength
|
- vignette-png — strength
|
||||||
- jpeg-artifacts-png — quality (имитация пережатия jpg/webp)
|
- jpeg-artifacts-png — quality (имитация пережатия jpg/webp)
|
||||||
|
- pixelate-png — blockSize (закрывает и их Color Blocks)
|
||||||
|
- randomize-pixels-png — blockSize, seed
|
||||||
|
- add-noise-png — amount, mode (mono/color), seed
|
||||||
|
- silhouette-png — color, threshold
|
||||||
|
|
||||||
### Анализ
|
### Анализ
|
||||||
|
|
||||||
@@ -141,26 +147,19 @@
|
|||||||
|
|
||||||
- separate-colors — minShare слоя (MEDIUM, мультифайловый вывод → пока идея)
|
- separate-colors — minShare слоя (MEDIUM, мультифайловый вывод → пока идея)
|
||||||
|
|
||||||
### Края и силуэт
|
### Края и силуэт — остаток
|
||||||
|
|
||||||
- feather-edges — radius (размытие только альфы)
|
|
||||||
- clean-edges-defringe — tolerance, радиус подбора цвета края
|
|
||||||
- silhouette — color силуэта
|
|
||||||
- glow — radius, color, intensity
|
- glow — radius, color, intensity
|
||||||
- shadow — offsetX, offsetY, blur, color, alpha
|
- shadow — offsetX, offsetY, blur, color, alpha
|
||||||
|
|
||||||
### Эффекты
|
### Эффекты — остаток
|
||||||
|
|
||||||
- pixelate — blockSize (! обещан в роадмапе)
|
|
||||||
- randomize-pixels — blockSize, seed
|
|
||||||
- add-noise — amount, моно/цветной (их Add Noise; наш noise только генератор)
|
|
||||||
- censor-region / erase-region — область (MEDIUM: нужен UI выделения → см. идеи)
|
- censor-region / erase-region — область (MEDIUM: нужен UI выделения → см. идеи)
|
||||||
- whirl — угол, центр, радиус (MEDIUM)
|
- whirl — угол, центр, радиус (MEDIUM)
|
||||||
|
|
||||||
### Сортировка/блоки пикселей
|
### Сортировка/блоки пикселей
|
||||||
|
|
||||||
- sort-pixels — blockSize, ключ (яркость/канал), направление (MEDIUM)
|
- sort-pixels — blockSize, ключ (яркость/канал), направление (MEDIUM)
|
||||||
- color-blocks — blockSize (усреднение блоков) — EASY, родственник pixelate
|
|
||||||
- slow-reveal / fade-in / fade-out / disappear — анимационные (→ идеи)
|
- slow-reveal / fade-in / fade-out / disappear — анимационные (→ идеи)
|
||||||
|
|
||||||
### Сжатие и качество (5, все MEDIUM)
|
### Сжатие и качество (5, все MEDIUM)
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { addNoise, defringe, featherAlpha, mulberry32, pixelate, shuffleBlocks, silhouette } from './pixel-fx';
|
||||||
|
import { makeImage } from './test-helpers';
|
||||||
|
|
||||||
|
describe('pixelate', () => {
|
||||||
|
it('блок усредняется: шахматка 2×2 с блоком 2 → один цвет', () => {
|
||||||
|
const img = makeImage(2, 2, [
|
||||||
|
[0, 0, 0, 255],
|
||||||
|
[200, 200, 200, 255],
|
||||||
|
[100, 100, 100, 255],
|
||||||
|
[140, 140, 140, 255]
|
||||||
|
]);
|
||||||
|
const out = pixelate(img, 2);
|
||||||
|
expect(out.data[0]).toBeCloseTo(110, 0);
|
||||||
|
expect(out.data[4]).toBeCloseTo(110, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('однотонное изображение не меняется', () => {
|
||||||
|
const img = makeImage(3, 3, new Array(9).fill([50, 60, 70, 255]));
|
||||||
|
expect([...pixelate(img, 2).data]).toEqual([...img.data]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shuffleBlocks / addNoise — детерминизм по seed', () => {
|
||||||
|
it('тот же seed даёт то же перемешивание', () => {
|
||||||
|
const img = makeImage(4, 1, [
|
||||||
|
[10, 0, 0, 255],
|
||||||
|
[20, 0, 0, 255],
|
||||||
|
[30, 0, 0, 255],
|
||||||
|
[40, 0, 0, 255]
|
||||||
|
]);
|
||||||
|
const a = shuffleBlocks(img, 1, 7);
|
||||||
|
const b = shuffleBlocks(img, 1, 7);
|
||||||
|
expect([...a.data]).toEqual([...b.data]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('мультимножество пикселей сохраняется (перестановка)', () => {
|
||||||
|
const img = makeImage(4, 1, [
|
||||||
|
[10, 0, 0, 255],
|
||||||
|
[20, 0, 0, 255],
|
||||||
|
[30, 0, 0, 255],
|
||||||
|
[40, 0, 0, 255]
|
||||||
|
]);
|
||||||
|
const out = [...shuffleBlocks(img, 1, 99).data.filter((_, i) => i % 4 === 0)].sort(
|
||||||
|
(x, y) => x - y
|
||||||
|
);
|
||||||
|
expect(out).toEqual([10, 20, 30, 40]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('addNoise при том же seed воспроизводим, amount=0 — идентичность', () => {
|
||||||
|
const img = makeImage(2, 2, new Array(4).fill([128, 64, 32, 255]));
|
||||||
|
const a = addNoise(img, 20, 'mono', 5);
|
||||||
|
const b = addNoise(img, 20, 'mono', 5);
|
||||||
|
expect([...a.data]).toEqual([...b.data]);
|
||||||
|
expect([...addNoise(img, 0, 'mono', 999).data]).toEqual([...img.data]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('featherAlpha', () => {
|
||||||
|
it('жёсткий край получает промежуточные альфы', () => {
|
||||||
|
// левая половина непрозрачная, правая прозрачная
|
||||||
|
const img = makeImage(6, 1, [
|
||||||
|
[255, 0, 0, 255],
|
||||||
|
[255, 0, 0, 255],
|
||||||
|
[255, 0, 0, 255],
|
||||||
|
[255, 0, 0, 0],
|
||||||
|
[255, 0, 0, 0],
|
||||||
|
[255, 0, 0, 0]
|
||||||
|
]);
|
||||||
|
const out = featherAlpha(img, 1);
|
||||||
|
const alphas = [0, 1, 2].map((x) => out.data[x * 4 + 3]);
|
||||||
|
expect(alphas[0]).toBeGreaterThan(240);
|
||||||
|
expect(alphas.some((v) => v > 0 && v < 240)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('defringe', () => {
|
||||||
|
it('полупрозрачному пикселю берётся RGB от соседнего непрозрачного', () => {
|
||||||
|
const img = makeImage(2, 1, [
|
||||||
|
[250, 250, 250, 255],
|
||||||
|
[255, 0, 0, 128]
|
||||||
|
]);
|
||||||
|
const out = defringe(img, 2);
|
||||||
|
expect(out.data[4]).toBe(250);
|
||||||
|
expect(out.data[6]).toBe(250);
|
||||||
|
expect(out.data[7]).toBe(128); // альфа сохранена
|
||||||
|
});
|
||||||
|
|
||||||
|
it('полностью прозрачные области не трогаются', () => {
|
||||||
|
const img = makeImage(2, 1, [
|
||||||
|
[250, 250, 250, 255],
|
||||||
|
[0, 0, 0, 0]
|
||||||
|
]);
|
||||||
|
const out = defringe(img, 2);
|
||||||
|
expect(out.data[4 + 3]).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('silhouette', () => {
|
||||||
|
it('видимые пиксели заливаются цветом, ниже порога — прозрачность', () => {
|
||||||
|
const img = makeImage(2, 1, [
|
||||||
|
[123, 45, 67, 255],
|
||||||
|
[123, 45, 67, 10]
|
||||||
|
]);
|
||||||
|
const out = silhouette(img, '#00ff00', 50);
|
||||||
|
expect([...out.data.slice(0, 4)]).toEqual([0, 255, 0, 255]);
|
||||||
|
expect(out.data[4 + 3]).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mulberry32', () => {
|
||||||
|
it('последовательность детерминирована', () => {
|
||||||
|
const a = mulberry32(42);
|
||||||
|
const b = mulberry32(42);
|
||||||
|
expect([a(), a(), a()]).toEqual([b(), b(), b()]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -723,6 +723,42 @@ export const ru: Dict = {
|
|||||||
keepSide: { left: 'Левая', right: 'Правая', top: 'Верхняя', bottom: 'Нижняя' }
|
keepSide: { left: 'Левая', right: 'Правая', top: 'Верхняя', bottom: 'Нижняя' }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'feather-edges-png': {
|
||||||
|
title: 'Растушевать края PNG',
|
||||||
|
description:
|
||||||
|
'Размывает только альфа-канал: жёсткие края выреза становятся мягкими, цвета не трогаются.',
|
||||||
|
params: { radius: 'Радиус растушёвки, px' }
|
||||||
|
},
|
||||||
|
'clean-edges-png': {
|
||||||
|
title: 'Почистить края PNG (defringe)',
|
||||||
|
description:
|
||||||
|
'Заменяет цветную кайму полупрозрачных пикселей цветом ближайшего полностью непрозрачного соседа. Альфа остаётся как была.',
|
||||||
|
params: { radius: 'Радиус поиска, px' }
|
||||||
|
},
|
||||||
|
'pixelate-png': {
|
||||||
|
title: 'Пикселизация PNG',
|
||||||
|
description: 'Усредняет каждый блок blockSize×blockSize в один цвет — классическая мозаика.',
|
||||||
|
params: { blockSize: 'Размер блока, px' }
|
||||||
|
},
|
||||||
|
'randomize-pixels-png': {
|
||||||
|
title: 'Перемешать пиксели PNG',
|
||||||
|
description:
|
||||||
|
'Переставляет блоки изображения между собой. Одинаковый seed даёт одинаковую раскладку.',
|
||||||
|
params: { blockSize: 'Размер блока, px', seed: 'Seed' }
|
||||||
|
},
|
||||||
|
'add-noise-png': {
|
||||||
|
title: 'Добавить шум PNG',
|
||||||
|
description:
|
||||||
|
'Добавляет зерно в стиле плёнки. Детерминировано по seed; монохромный сохраняет баланс оттенков.',
|
||||||
|
params: { amount: 'Сила, %', mode: 'Тип шума', seed: 'Seed' },
|
||||||
|
options: { mode: { mono: 'Монохромное зерно', color: 'Цветной шум' } }
|
||||||
|
},
|
||||||
|
'silhouette-png': {
|
||||||
|
title: 'Силуэт PNG',
|
||||||
|
description:
|
||||||
|
'Заливает все видимые пиксели одним цветом, сохраняя их прозрачность — мгновенный силуэт.',
|
||||||
|
params: { color: 'Цвет силуэта', threshold: 'Порог видимости, %' }
|
||||||
|
},
|
||||||
'show-transparent-png': {
|
'show-transparent-png': {
|
||||||
title: 'Показать прозрачные области PNG',
|
title: 'Показать прозрачные области PNG',
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -101,6 +101,14 @@ import {
|
|||||||
trimToContent,
|
trimToContent,
|
||||||
type Anchor9
|
type Anchor9
|
||||||
} from './core/geometry';
|
} from './core/geometry';
|
||||||
|
import {
|
||||||
|
addNoise,
|
||||||
|
defringe,
|
||||||
|
featherAlpha,
|
||||||
|
pixelate,
|
||||||
|
shuffleBlocks,
|
||||||
|
silhouette
|
||||||
|
} from './core/pixel-fx';
|
||||||
import { hexToPixels, pixelsToHex } from './core/text';
|
import { hexToPixels, pixelsToHex } from './core/text';
|
||||||
import { clonePixelImage, type PixelImage } from './core/types';
|
import { clonePixelImage, type PixelImage } from './core/types';
|
||||||
|
|
||||||
@@ -1144,6 +1152,28 @@ export const TOOLS: ToolEntry[] = [
|
|||||||
],
|
],
|
||||||
run: (img, p) => erodeImage(img, num(p, 'radius'))
|
run: (img, p) => erodeImage(img, num(p, 'radius'))
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'feather-edges-png',
|
||||||
|
title: 'Feather Edges PNG',
|
||||||
|
description:
|
||||||
|
'Blurs only the alpha channel: hard cutout edges become soft and gradual, colors stay untouched.',
|
||||||
|
category: 'alpha',
|
||||||
|
params: [
|
||||||
|
{ id: 'radius', label: 'Feather radius, px', type: 'slider', min: 1, max: 20, step: 1, default: 3 }
|
||||||
|
],
|
||||||
|
run: (img, p) => featherAlpha(img, num(p, 'radius'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'clean-edges-png',
|
||||||
|
title: 'Clean Edges PNG (defringe)',
|
||||||
|
description:
|
||||||
|
'Replaces edge-halo colors of semi-transparent pixels with the nearest fully opaque color. Alpha stays as is.',
|
||||||
|
category: 'alpha',
|
||||||
|
params: [
|
||||||
|
{ id: 'radius', label: 'Search radius, px', type: 'slider', min: 1, max: 10, step: 1, default: 3 }
|
||||||
|
],
|
||||||
|
run: (img, p) => defringe(img, num(p, 'radius'))
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'harden-alpha-png',
|
id: 'harden-alpha-png',
|
||||||
title: 'Harden edges PNG',
|
title: 'Harden edges PNG',
|
||||||
@@ -1907,6 +1937,68 @@ export const TOOLS: ToolEntry[] = [
|
|||||||
],
|
],
|
||||||
run: (img, p) => vignette(img, num(p, 'strength'))
|
run: (img, p) => vignette(img, num(p, 'strength'))
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'pixelate-png',
|
||||||
|
title: 'Pixelate PNG',
|
||||||
|
description: 'Averages every blockSize×blockSize area into one color — classic mosaic.',
|
||||||
|
category: 'filters',
|
||||||
|
params: [
|
||||||
|
{ id: 'blockSize', label: 'Block size, px', type: 'slider', min: 2, max: 64, step: 1, default: 8 }
|
||||||
|
],
|
||||||
|
run: (img, p) => pixelate(img, num(p, 'blockSize'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'randomize-pixels-png',
|
||||||
|
title: 'Randomize Pixels PNG',
|
||||||
|
description:
|
||||||
|
'Shuffles blocks of the image between positions. Same seed gives the same arrangement.',
|
||||||
|
category: 'filters',
|
||||||
|
params: [
|
||||||
|
{ id: 'blockSize', label: 'Block size, px', type: 'slider', min: 1, max: 64, step: 1, default: 8 },
|
||||||
|
{ id: 'seed', label: 'Seed', type: 'number', min: 0, max: 999999, step: 1, default: 42 }
|
||||||
|
],
|
||||||
|
run: (img, p) => shuffleBlocks(img, num(p, 'blockSize'), num(p, 'seed'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-noise-png',
|
||||||
|
title: 'Add Noise to PNG',
|
||||||
|
description:
|
||||||
|
'Adds film-grain style noise. Deterministic by seed; monochrome keeps original hue balance.',
|
||||||
|
category: 'filters',
|
||||||
|
params: [
|
||||||
|
{ id: 'amount', label: 'Amount, %', type: 'slider', min: 0, max: 100, step: 1, default: 25 },
|
||||||
|
{
|
||||||
|
id: 'mode',
|
||||||
|
label: 'Noise type',
|
||||||
|
type: 'select',
|
||||||
|
default: 'mono',
|
||||||
|
options: [
|
||||||
|
{ value: 'mono', label: 'Monochrome grain' },
|
||||||
|
{ value: 'color', label: 'Color noise' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ id: 'seed', label: 'Seed', type: 'number', min: 0, max: 999999, step: 1, default: 1234 }
|
||||||
|
],
|
||||||
|
run: (img, p) =>
|
||||||
|
addNoise(
|
||||||
|
img,
|
||||||
|
num(p, 'amount'),
|
||||||
|
str(p, 'mode') === 'color' ? 'color' : 'mono',
|
||||||
|
num(p, 'seed')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'silhouette-png',
|
||||||
|
title: 'Silhouette PNG',
|
||||||
|
description:
|
||||||
|
'Turns all visible pixels into a single solid color while keeping their transparency — instant silhouette.',
|
||||||
|
category: 'filters',
|
||||||
|
params: [
|
||||||
|
{ id: 'color', label: 'Silhouette color', type: 'color', default: '#111318' },
|
||||||
|
{ id: 'threshold', label: 'Visibility threshold, %', type: 'slider', min: 0, max: 100, step: 1, default: 10 }
|
||||||
|
],
|
||||||
|
run: (img, p) => silhouette(img, str(p, 'color'), num(p, 'threshold') * 2.55)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'jpeg-artifacts-png',
|
id: 'jpeg-artifacts-png',
|
||||||
title: 'JPEG artifacts',
|
title: 'JPEG artifacts',
|
||||||
|
|||||||
@@ -116,6 +116,12 @@ export const TOOL_ICONS: Record<string, typeof AppWindow> = {
|
|||||||
'dark-pixel-mask-png': Moon,
|
'dark-pixel-mask-png': Moon,
|
||||||
'unique-color-mask-png': Dices,
|
'unique-color-mask-png': Dices,
|
||||||
'extract-color-from-png': Focus,
|
'extract-color-from-png': Focus,
|
||||||
|
'feather-edges-png': Droplets,
|
||||||
|
'clean-edges-png': Scissors,
|
||||||
|
'pixelate-png': Grid3x3,
|
||||||
|
'randomize-pixels-png': Dices,
|
||||||
|
'add-noise-png': Hash,
|
||||||
|
'silhouette-png': Contrast,
|
||||||
'trim-empty-space-png': Crop,
|
'trim-empty-space-png': Crop,
|
||||||
'change-canvas-size-png': Frame,
|
'change-canvas-size-png': Frame,
|
||||||
'change-aspect-ratio-png': Scaling,
|
'change-aspect-ratio-png': Scaling,
|
||||||
|
|||||||
Reference in New Issue
Block a user