diff --git a/web/src/lib/categories.ts b/web/src/lib/categories.ts index 71f20d1..821202e 100644 --- a/web/src/lib/categories.ts +++ b/web/src/lib/categories.ts @@ -1,4 +1,11 @@ -export type CategoryId = 'convert' | 'alpha' | 'color' | 'geometry' | 'analyze' | 'generate'; +export type CategoryId = + | 'convert' + | 'alpha' + | 'color' + | 'geometry' + | 'analyze' + | 'generate' + | 'filters'; export type Category = { id: CategoryId; label: string }; @@ -7,6 +14,7 @@ export const CATEGORIES: Category[] = [ { id: 'alpha', label: 'Прозрачность' }, { id: 'color', label: 'Цвет' }, { id: 'geometry', label: 'Геометрия' }, + { id: 'filters', label: 'Фильтры' }, { id: 'analyze', label: 'Анализ' }, { id: 'generate', label: 'Генерация' } ]; diff --git a/web/src/lib/registry.test.ts b/web/src/lib/registry.test.ts index 228a7da..e7a645f 100644 --- a/web/src/lib/registry.test.ts +++ b/web/src/lib/registry.test.ts @@ -58,11 +58,14 @@ const PLAN_TOOL_IDS = [ 'linear-gradient-png', 'png-is-grayscale', 'png-is-transparent', - 'png-orientation' + 'png-orientation', + 'remove-background-png', + 'blur-png', + 'sharpen-png' ]; describe('реестр инструментов', () => { - it('содержит ровно 48 инструментов из плана', () => { + it('содержит ровно 51 инструмент из плана', () => { expect(TOOLS.map((t) => t.id).sort()).toEqual([...PLAN_TOOL_IDS].sort()); }); diff --git a/web/src/lib/registry.ts b/web/src/lib/registry.ts index 5bb6d62..53e88aa 100644 --- a/web/src/lib/registry.ts +++ b/web/src/lib/registry.ts @@ -13,6 +13,8 @@ import { isGrayscale, orientationOf } from './core/analyze'; +import { backgroundMaskPreview, removeBackground } from './core/background'; +import { gaussianBlur, sharpen as sharpenImage } from './core/convolution'; import { gradientImage, noiseImage, solidImage } from './core/generate'; import { brightnessContrast, @@ -406,6 +408,28 @@ export const TOOLS: ToolEntry[] = [ params: [], run: (img) => centerByAlpha(img) }, + { + id: 'blur-png', + title: 'Размытие PNG', + description: + 'Гауссово размытие: три прохода разделяемого бокса — быстро при любом радиусе. Прозрачные края не темнеют.', + category: 'filters', + params: [ + { id: 'radius', label: 'Радиус, px', type: 'slider', min: 1, max: 32, step: 1, default: 4 } + ], + run: (img, p) => gaussianBlur(img, num(p, 'radius')) + }, + { + id: 'sharpen-png', + title: 'Резкость PNG', + description: + 'Подчёркивает края ядром резкости; сила задаёт смесь с оригиналом. 0% — без изменений.', + category: 'filters', + params: [ + { id: 'strength', label: 'Сила, %', type: 'slider', min: 0, max: 100, step: 1, default: 50 } + ], + run: (img, p) => sharpenImage(img, num(p, 'strength')) + }, { id: 'grayscale-png', title: 'Чёрно-белый PNG', @@ -601,6 +625,33 @@ export const TOOLS: ToolEntry[] = [ params: [], run: (img) => invertAlpha(img) }, + { + id: 'remove-background-png', + title: 'Удалить фон PNG (умно)', + description: + 'Убирает однотонный фон: по цвету с допуском, только внешние области от краёв или весь совпадающий цвет. Умеет сглаживать границу.', + category: 'alpha', + params: [ + { id: 'color', label: 'Цвет фона', type: 'color', default: '#ffffff' }, + { id: 'tolerance', label: 'Допуск похожести, %', type: 'slider', min: 0, max: 100, step: 1, default: 10 }, + { id: 'outerOnly', label: 'Только внешние области', type: 'checkbox', default: true }, + { id: 'smooth', label: 'Сглаживание границы, проходы', type: 'slider', min: 0, max: 8, step: 1, default: 1 } + ], + run: (img, p) => + removeBackground(img, { + color: str(p, 'color'), + tolerancePercent: num(p, 'tolerance'), + outerOnly: p['outerOnly'] === true, + smoothPasses: num(p, 'smooth') + }), + preview: (img, p) => + backgroundMaskPreview(img, { + color: str(p, 'color'), + tolerancePercent: num(p, 'tolerance'), + outerOnly: p['outerOnly'] === true, + smoothPasses: num(p, 'smooth') + }) + }, { id: 'remove-color-from-png', title: 'Удалить цвет из PNG (прозрачность)',