mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 21:46:35 +00:00
feat: add core operations
This commit is contained in:
@@ -1,7 +1,56 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { colorMask, flattenOntoColor, invertAlpha, parseHex, removeColorToAlpha } from './alpha';
|
import {
|
||||||
|
colorMask,
|
||||||
|
extractAlphaMask,
|
||||||
|
flattenOntoColor,
|
||||||
|
invertAlpha,
|
||||||
|
parseHex,
|
||||||
|
removeColorToAlpha,
|
||||||
|
roundCorners,
|
||||||
|
setAlphaChannel
|
||||||
|
} from './alpha';
|
||||||
import { makeImage } from './test-helpers';
|
import { makeImage } from './test-helpers';
|
||||||
|
|
||||||
|
describe('setAlphaChannel', () => {
|
||||||
|
it('задаёт константную альфу', () => {
|
||||||
|
const out = setAlphaChannel(makeImage(2, 1, [
|
||||||
|
[1, 2, 3, 255],
|
||||||
|
[4, 5, 6, 0]
|
||||||
|
]), 50);
|
||||||
|
expect(out.data[3]).toBe(128);
|
||||||
|
expect(out.data[7]).toBe(128);
|
||||||
|
expect(out.data[0]).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractAlphaMask', () => {
|
||||||
|
it('переводит альфу в чёрно-белую непрозрачную маску', () => {
|
||||||
|
const out = extractAlphaMask(makeImage(2, 1, [
|
||||||
|
[10, 20, 30, 255],
|
||||||
|
[40, 50, 60, 0]
|
||||||
|
]));
|
||||||
|
expect([...out.data]).toEqual([
|
||||||
|
255, 255, 255, 255,
|
||||||
|
0, 0, 0, 255
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('roundCorners', () => {
|
||||||
|
it('срезает углы, центр и середины сторон остаются', () => {
|
||||||
|
const img = makeImage(11, 11, new Array(121).fill([100, 100, 100, 255]));
|
||||||
|
const out = roundCorners(img, 40);
|
||||||
|
expect(out.data[(0 * 11 + 0) * 4 + 3]).toBe(0);
|
||||||
|
expect(out.data[(5 * 11 + 5) * 4 + 3]).toBe(255);
|
||||||
|
expect(out.data[(5 * 11 + 0) * 4 + 3]).toBe(255);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('нулевой радиус ничего не меняет', () => {
|
||||||
|
const img = makeImage(2, 2, new Array(4).fill([1, 2, 3, 255]));
|
||||||
|
expect([...roundCorners(img, 0).data]).toEqual([...img.data]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('invertAlpha', () => {
|
describe('invertAlpha', () => {
|
||||||
it('обращает альфу, RGB не трогает', () => {
|
it('обращает альфу, RGB не трогает', () => {
|
||||||
const out = invertAlpha(makeImage(1, 1, [[10, 20, 30, 128]]));
|
const out = invertAlpha(makeImage(1, 1, [[10, 20, 30, 128]]));
|
||||||
|
|||||||
@@ -22,6 +22,46 @@ export function removeColorToAlpha(
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setAlphaChannel(img: PixelImage, percent: number): PixelImage {
|
||||||
|
const alpha = Math.round((clamp(percent, 0, 100) / 100) * 255);
|
||||||
|
const out: PixelImage = { width: img.width, height: img.height, data: img.data.slice() };
|
||||||
|
for (let i = 3; i < out.data.length; i += 4) {
|
||||||
|
out.data[i] = alpha;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractAlphaMask(img: PixelImage): PixelImage {
|
||||||
|
const out = createPixelImage(img.width, img.height);
|
||||||
|
for (let i = 0; i < out.data.length; i += 4) {
|
||||||
|
const v = img.data[i + 3];
|
||||||
|
out.data[i] = v;
|
||||||
|
out.data[i + 1] = v;
|
||||||
|
out.data[i + 2] = v;
|
||||||
|
out.data[i + 3] = 255;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function roundCorners(img: PixelImage, radiusPercent: number): PixelImage {
|
||||||
|
const radius = (clamp(radiusPercent, 0, 50) / 100) * (Math.min(img.width, img.height) / 2);
|
||||||
|
if (radius < 1) return { width: img.width, height: img.height, data: img.data.slice() };
|
||||||
|
const out: PixelImage = { width: img.width, height: img.height, data: img.data.slice() };
|
||||||
|
const r2 = radius * radius;
|
||||||
|
for (let y = 0; y < out.height; y++) {
|
||||||
|
for (let x = 0; x < out.width; x++) {
|
||||||
|
const cx = clamp(x, radius, out.width - 1 - radius);
|
||||||
|
const cy = clamp(y, radius, out.height - 1 - radius);
|
||||||
|
const dx = x - cx;
|
||||||
|
const dy = y - cy;
|
||||||
|
if (dx * dx + dy * dy > r2) {
|
||||||
|
out.data[(y * out.width + x) * 4 + 3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
export function invertAlpha(img: PixelImage): PixelImage {
|
export function invertAlpha(img: PixelImage): PixelImage {
|
||||||
const out = createPixelImage(img.width, img.height);
|
const out = createPixelImage(img.width, img.height);
|
||||||
for (let i = 0; i < out.data.length; i += 4) {
|
for (let i = 0; i < out.data.length; i += 4) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { crop, flip, resize, rotate90 } from './geometry';
|
import { centerByAlpha, crop, expandCanvas, flip, resize, rotate90, tile } from './geometry';
|
||||||
import { makeImage } from './test-helpers';
|
import { makeImage } from './test-helpers';
|
||||||
|
|
||||||
const square = () =>
|
const square = () =>
|
||||||
@@ -126,6 +126,64 @@ describe('crop', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('expandCanvas', () => {
|
||||||
|
const pixel = () => makeImage(1, 1, [[10, 20, 30, 255]]);
|
||||||
|
|
||||||
|
it('прозрачное расширение кладёт пиксель со смещением', () => {
|
||||||
|
const out = expandCanvas(pixel(), 1, 2, 3, 4);
|
||||||
|
expect(out.width).toBe(5);
|
||||||
|
expect(out.height).toBe(7);
|
||||||
|
expect([...out.data.slice(0, 4)]).toEqual([0, 0, 0, 0]);
|
||||||
|
expect([...out.data.slice((2 * 5 + 1) * 4, (2 * 5 + 1) * 4 + 4)]).toEqual([10, 20, 30, 255]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('цветной фон заливает всё вокруг', () => {
|
||||||
|
const out = expandCanvas(pixel(), 1, 0, 0, 0, '#ffffff');
|
||||||
|
expect(out.data[0]).toBe(255);
|
||||||
|
expect(out.data[3]).toBe(255);
|
||||||
|
expect(out.data[(0 * 2 + 1) * 4 + 3]).toBe(255);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tile', () => {
|
||||||
|
it('повторяет изображение по сетке', () => {
|
||||||
|
const out = tile(makeImage(1, 1, [[9, 9, 9, 255]]), 3, 2);
|
||||||
|
expect(out.width).toBe(3);
|
||||||
|
expect(out.height).toBe(2);
|
||||||
|
expect([...out.data].filter((_, i) => i % 4 === 0)).toEqual([9, 9, 9, 9, 9, 9]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('centerByAlpha', () => {
|
||||||
|
it('вырезает непрозрачный блок и центрирует на прежнем холсте', () => {
|
||||||
|
const img = makeImage(3, 3, [
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[5, 5, 5, 255],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0]
|
||||||
|
]);
|
||||||
|
const out = centerByAlpha(img);
|
||||||
|
expect(out.width).toBe(3);
|
||||||
|
expect(out.height).toBe(3);
|
||||||
|
const alphaAt = (x: number, y: number) => out.data[(y * 3 + x) * 4 + 3];
|
||||||
|
expect(alphaAt(0, 0)).toBe(0);
|
||||||
|
expect(alphaAt(1, 1)).toBe(255);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('полностью прозрачное изображение возвращается без изменений', () => {
|
||||||
|
const img = makeImage(2, 1, [
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
[0, 0, 0, 0]
|
||||||
|
]);
|
||||||
|
expect([...centerByAlpha(img).data]).toEqual([...img.data]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('resize', () => {
|
describe('resize', () => {
|
||||||
const twoByTwo = () =>
|
const twoByTwo = () =>
|
||||||
makeImage(2, 2, [
|
makeImage(2, 2, [
|
||||||
|
|||||||
@@ -1,5 +1,85 @@
|
|||||||
|
import { parseHex } from './alpha';
|
||||||
import { clonePixelImage, createPixelImage, type PixelImage } from './types';
|
import { clonePixelImage, createPixelImage, type PixelImage } from './types';
|
||||||
|
|
||||||
|
export function expandCanvas(
|
||||||
|
img: PixelImage,
|
||||||
|
left: number,
|
||||||
|
top: number,
|
||||||
|
right: number,
|
||||||
|
bottom: number,
|
||||||
|
backgroundHex?: string
|
||||||
|
): PixelImage {
|
||||||
|
const l = Math.max(0, Math.trunc(left));
|
||||||
|
const t = Math.max(0, Math.trunc(top));
|
||||||
|
const r = Math.max(0, Math.trunc(right));
|
||||||
|
const b = Math.max(0, Math.trunc(bottom));
|
||||||
|
const out = createPixelImage(img.width + l + r, img.height + t + b);
|
||||||
|
if (backgroundHex !== undefined) {
|
||||||
|
const [cr, cg, cb] = parseHex(backgroundHex);
|
||||||
|
for (let i = 0; i < out.data.length; i += 4) {
|
||||||
|
out.data[i] = cr;
|
||||||
|
out.data[i + 1] = cg;
|
||||||
|
out.data[i + 2] = cb;
|
||||||
|
out.data[i + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let y = 0; y < img.height; y++) {
|
||||||
|
const srcStart = y * img.width * 4;
|
||||||
|
out.data.set(
|
||||||
|
img.data.subarray(srcStart, srcStart + img.width * 4),
|
||||||
|
((y + t) * out.width + l) * 4
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tile(img: PixelImage, columns: number, rows: number): PixelImage {
|
||||||
|
const cols = Math.max(1, Math.trunc(columns));
|
||||||
|
const rowsCount = Math.max(1, Math.trunc(rows));
|
||||||
|
const out = createPixelImage(img.width * cols, img.height * rowsCount);
|
||||||
|
for (let ty = 0; ty < rowsCount; ty++) {
|
||||||
|
for (let tx = 0; tx < cols; tx++) {
|
||||||
|
for (let y = 0; y < img.height; y++) {
|
||||||
|
const srcStart = y * img.width * 4;
|
||||||
|
out.data.set(
|
||||||
|
img.data.subarray(srcStart, srcStart + img.width * 4),
|
||||||
|
((ty * img.height + y) * out.width + tx * img.width) * 4
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function centerByAlpha(img: PixelImage): PixelImage {
|
||||||
|
let minX = img.width;
|
||||||
|
let minY = img.height;
|
||||||
|
let maxX = -1;
|
||||||
|
let maxY = -1;
|
||||||
|
for (let y = 0; y < img.height; y++) {
|
||||||
|
for (let x = 0; x < img.width; x++) {
|
||||||
|
if (img.data[(y * img.width + x) * 4 + 3] > 0) {
|
||||||
|
if (x < minX) minX = x;
|
||||||
|
if (y < minY) minY = y;
|
||||||
|
if (x > maxX) maxX = x;
|
||||||
|
if (y > maxY) maxY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxX < 0) return clonePixelImage(img);
|
||||||
|
const content = crop(img, minX, minY, maxX - minX + 1, maxY - minY + 1);
|
||||||
|
const out = createPixelImage(img.width, img.height);
|
||||||
|
const dx = Math.floor((img.width - content.width) / 2);
|
||||||
|
const dy = Math.floor((img.height - content.height) / 2);
|
||||||
|
for (let y = 0; y < content.height; y++) {
|
||||||
|
out.data.set(
|
||||||
|
content.data.subarray(y * content.width * 4, (y + 1) * content.width * 4),
|
||||||
|
((y + dy) * out.width + dx) * 4
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
export type FlipAxis = 'horizontal' | 'vertical';
|
export type FlipAxis = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
export function flip(img: PixelImage, axis: FlipAxis): PixelImage {
|
export function flip(img: PixelImage, axis: FlipAxis): PixelImage {
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ const PLAN_TOOL_IDS = [
|
|||||||
'crop-png',
|
'crop-png',
|
||||||
'rotate-png',
|
'rotate-png',
|
||||||
'flip-png',
|
'flip-png',
|
||||||
|
'add-padding-png',
|
||||||
|
'add-border-png',
|
||||||
|
'fit-on-background-png',
|
||||||
|
'tile-png',
|
||||||
|
'center-by-alpha-png',
|
||||||
'grayscale-png',
|
'grayscale-png',
|
||||||
'invert-colors-png',
|
'invert-colors-png',
|
||||||
'adjust-brightness-contrast-png',
|
'adjust-brightness-contrast-png',
|
||||||
@@ -42,11 +47,15 @@ const PLAN_TOOL_IDS = [
|
|||||||
'swap-channels-png',
|
'swap-channels-png',
|
||||||
'black-and-white-png',
|
'black-and-white-png',
|
||||||
'posterize-png',
|
'posterize-png',
|
||||||
'two-colors-png'
|
'two-colors-png',
|
||||||
|
'remove-alpha-channel-png',
|
||||||
|
'set-alpha-channel-png',
|
||||||
|
'extract-alpha-mask-png',
|
||||||
|
'round-corners-png'
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('реестр инструментов', () => {
|
describe('реестр инструментов', () => {
|
||||||
it('содержит ровно 32 инструмента из плана', () => {
|
it('содержит ровно 41 инструмент из плана', () => {
|
||||||
expect(TOOLS.map((t) => t.id).sort()).toEqual([...PLAN_TOOL_IDS].sort());
|
expect(TOOLS.map((t) => t.id).sort()).toEqual([...PLAN_TOOL_IDS].sort());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+128
-2
@@ -1,5 +1,13 @@
|
|||||||
import type { CategoryId } from './categories';
|
import type { CategoryId } from './categories';
|
||||||
import { colorMask, flattenOntoColor, invertAlpha, removeColorToAlpha } from './core/alpha';
|
import {
|
||||||
|
colorMask,
|
||||||
|
extractAlphaMask,
|
||||||
|
flattenOntoColor,
|
||||||
|
invertAlpha,
|
||||||
|
removeColorToAlpha,
|
||||||
|
roundCorners,
|
||||||
|
setAlphaChannel
|
||||||
|
} from './core/alpha';
|
||||||
import {
|
import {
|
||||||
brightnessContrast,
|
brightnessContrast,
|
||||||
changeHue,
|
changeHue,
|
||||||
@@ -15,7 +23,7 @@ import {
|
|||||||
type ChannelSwapPair,
|
type ChannelSwapPair,
|
||||||
type RgbChannel
|
type RgbChannel
|
||||||
} from './core/color';
|
} from './core/color';
|
||||||
import { crop, flip, resize, rotate90 } from './core/geometry';
|
import { centerByAlpha, crop, expandCanvas, flip, resize, rotate90, tile } from './core/geometry';
|
||||||
import { decodeTextImage, toBase64, toDataUrl, type OutputMime } from './core/io';
|
import { decodeTextImage, toBase64, toDataUrl, type OutputMime } from './core/io';
|
||||||
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';
|
||||||
@@ -293,6 +301,87 @@ export const TOOLS: ToolEntry[] = [
|
|||||||
],
|
],
|
||||||
run: (img, p) => flip(img, str(p, 'axis') === 'vertical' ? 'vertical' : 'horizontal')
|
run: (img, p) => flip(img, str(p, 'axis') === 'vertical' ? 'vertical' : 'horizontal')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'add-padding-png',
|
||||||
|
title: 'Добавить поля PNG',
|
||||||
|
description: 'Расширяет холст во все стороны на выбранное число пикселей.',
|
||||||
|
category: 'geometry',
|
||||||
|
params: [
|
||||||
|
{ id: 'padding', label: 'Поля, px', type: 'number', min: 1, max: 2000, step: 1, default: 10 },
|
||||||
|
{ id: 'transparent', label: 'Прозрачные поля', type: 'checkbox', default: true },
|
||||||
|
{ id: 'color', label: 'Цвет полей', type: 'color', default: '#ffffff' }
|
||||||
|
],
|
||||||
|
run: (img, p) =>
|
||||||
|
expandCanvas(
|
||||||
|
img,
|
||||||
|
num(p, 'padding'),
|
||||||
|
num(p, 'padding'),
|
||||||
|
num(p, 'padding'),
|
||||||
|
num(p, 'padding'),
|
||||||
|
p['transparent'] === true ? undefined : str(p, 'color')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-border-png',
|
||||||
|
title: 'Добавить рамку PNG',
|
||||||
|
description: 'Рисует цветную рамку вокруг изображения выбранной толщины.',
|
||||||
|
category: 'geometry',
|
||||||
|
params: [
|
||||||
|
{ id: 'thickness', label: 'Толщина рамки, px', type: 'number', min: 1, max: 500, step: 1, default: 5 },
|
||||||
|
{ id: 'color', label: 'Цвет рамки', type: 'color', default: '#000000' }
|
||||||
|
],
|
||||||
|
run: (img, p) => expandCanvas(img, num(p, 'thickness'), num(p, 'thickness'), num(p, 'thickness'), num(p, 'thickness'), str(p, 'color'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fit-on-background-png',
|
||||||
|
title: 'Вписать PNG на фон',
|
||||||
|
description:
|
||||||
|
'Помещает изображение по центру полотна заданного размера с прозрачным или цветным фоном.',
|
||||||
|
category: 'geometry',
|
||||||
|
params: [
|
||||||
|
{ id: 'width', label: 'Ширина полотна', type: 'number', min: 1, max: 20000, step: 1, default: 800 },
|
||||||
|
{ id: 'height', label: 'Высота полотна', type: 'number', min: 1, max: 20000, step: 1, default: 600 },
|
||||||
|
{ id: 'transparent', label: 'Прозрачный фон', type: 'checkbox', default: false },
|
||||||
|
{ id: 'color', label: 'Цвет фона', type: 'color', default: '#ffffff' }
|
||||||
|
],
|
||||||
|
run: (img, p) => {
|
||||||
|
const width = Math.trunc(num(p, 'width'));
|
||||||
|
const height = Math.trunc(num(p, 'height'));
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
throw new Error('Укажите положительные размеры полотна');
|
||||||
|
}
|
||||||
|
const left = Math.max(0, Math.floor((width - img.width) / 2));
|
||||||
|
const top = Math.max(0, Math.floor((height - img.height) / 2));
|
||||||
|
return expandCanvas(
|
||||||
|
img,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
Math.max(0, width - img.width - left),
|
||||||
|
Math.max(0, height - img.height - top),
|
||||||
|
p['transparent'] === true ? undefined : str(p, 'color')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tile-png',
|
||||||
|
title: 'Замостить PNG',
|
||||||
|
description: 'Повторяет изображение сеткой из выбранного числа столбцов и строк.',
|
||||||
|
category: 'geometry',
|
||||||
|
params: [
|
||||||
|
{ id: 'columns', label: 'Столбцов', type: 'number', min: 1, max: 50, step: 1, default: 2 },
|
||||||
|
{ id: 'rows', label: 'Строк', type: 'number', min: 1, max: 50, step: 1, default: 2 }
|
||||||
|
],
|
||||||
|
run: (img, p) => tile(img, num(p, 'columns'), num(p, 'rows'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'center-by-alpha-png',
|
||||||
|
title: 'Центрировать PNG по содержимому',
|
||||||
|
description:
|
||||||
|
'Находит непрозрачную часть изображения и размещает её по центру прежнего холста.',
|
||||||
|
category: 'geometry',
|
||||||
|
params: [],
|
||||||
|
run: (img) => centerByAlpha(img)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'grayscale-png',
|
id: 'grayscale-png',
|
||||||
title: 'Чёрно-белый PNG',
|
title: 'Чёрно-белый PNG',
|
||||||
@@ -443,6 +532,43 @@ export const TOOLS: ToolEntry[] = [
|
|||||||
output: { mime: 'image/webp', ext: 'webp', qualityParamId: 'quality' },
|
output: { mime: 'image/webp', ext: 'webp', qualityParamId: 'quality' },
|
||||||
run: (img) => clonePixelImage(img)
|
run: (img) => clonePixelImage(img)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'remove-alpha-channel-png',
|
||||||
|
title: 'Убрать альфа-канал PNG',
|
||||||
|
description: 'Накладывает изображение на белый фон и сохраняет без прозрачности.',
|
||||||
|
category: 'alpha',
|
||||||
|
params: [],
|
||||||
|
run: (img) => flattenOntoColor(img, '#ffffff')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'set-alpha-channel-png',
|
||||||
|
title: 'Задать альфа-канал PNG',
|
||||||
|
description: 'Присваивает всем пикселям одинаковую прозрачность, цвета не меняются.',
|
||||||
|
category: 'alpha',
|
||||||
|
params: [
|
||||||
|
{ id: 'percent', label: 'Прозрачность, %', type: 'slider', min: 0, max: 100, step: 1, default: 100 }
|
||||||
|
],
|
||||||
|
run: (img, p) => setAlphaChannel(img, num(p, 'percent'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'extract-alpha-mask-png',
|
||||||
|
title: 'Извлечь маску альфы PNG',
|
||||||
|
description: 'Превращает прозрачность в чёрно-белую непрозрачную маску.',
|
||||||
|
category: 'alpha',
|
||||||
|
params: [],
|
||||||
|
run: (img) => extractAlphaMask(img)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'round-corners-png',
|
||||||
|
title: 'Скруглить углы PNG',
|
||||||
|
description:
|
||||||
|
'Обрезает углы по радиусу, заданному в процентах от половины меньшей стороны.',
|
||||||
|
category: 'alpha',
|
||||||
|
params: [
|
||||||
|
{ id: 'radius', label: 'Радиус скругления, %', type: 'slider', min: 0, max: 50, step: 1, default: 10 }
|
||||||
|
],
|
||||||
|
run: (img, p) => roundCorners(img, num(p, 'radius'))
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'invert-alpha-png',
|
id: 'invert-alpha-png',
|
||||||
title: 'Инвертировать альфа-канал PNG',
|
title: 'Инвертировать альфа-канал PNG',
|
||||||
|
|||||||
Reference in New Issue
Block a user