mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 21:46:35 +00:00
feat: add registry tools for new preview flow
This commit is contained in:
@@ -20,3 +20,11 @@
|
|||||||
|
|
||||||
6. **Избранные инструменты (fav tools)** — оценка S/M.
|
6. **Избранные инструменты (fav tools)** — оценка S/M.
|
||||||
Тоггл-звёздочка на карточках каталога, результатах поиска и в шапке страницы инструмента; список id в localStorage. Избранное показывается отдельной секцией сверху каталога и поднимается в выдаче поиска (бонус к popularity при скоринге).
|
Тоггл-звёздочка на карточках каталога, результатах поиска и в шапке страницы инструмента; список id в localStorage. Избранное показывается отдельной секцией сверху каталога и поднимается в выдаче поиска (бонус к popularity при скоринге).
|
||||||
|
|
||||||
|
7. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download** — оценка M/L.
|
||||||
|
Инструменты `convert-png-to-jpg` / `convert-png-to-webp` (и др. форматы) не должны жить как самостоятельные инструменты цепочки. Вместо этого — бесшовное внедрение выбора формата/качества прямо в кнопку/диалог **Download**: юзер скачивает результат в нужном формате (mime/ext/quality), а pipeline при этом не усложняется лишним звеном.
|
||||||
|
Связано с планом типизации: в новом `ToolEntry<P>` под `run` в `registry-new` нет поля `output` (mime/ext/qualityParamId), которое есть у старых конвертеров — см. `plan-composite-params.md`, замечание по Фазе 2. Формат-метаданные должны переехать в понятие «формат результата» на уровне скачивания, а не отдельного инструмента.
|
||||||
|
**Решение «не сейчас» (исследование, 2026-09):**
|
||||||
|
- В старом UI этот паттерн уже работает: `convert-*`-инструменты не конвертируют в `run`, а только настраивают `output` (mime/ext/qualityParamId), который читает `DownloadButton` → `encode(img, mime, quality)`. То есть формат уже фактически «поле скачивания» в старом pipeline.
|
||||||
|
- В новом preview `registry-new` поля `output` нет, `executor.worker` возвращает только пиксели, а `SchemaToolView.download()` захардкожен на `image/png` + имя `with-border.png`. Делать ретработу сейчас = параллельный спец-проект.
|
||||||
|
- **Когда делать:** вместе с достройкой нового download/сохранения в новом UI (в рамках `plan-composite-params`, Фаза 4/UI). Тогда выбор формата встраивается в download-flow естественно, без выдёргивания отдельным проектом. Не начинать, пока не закрыта типизация pipeline.
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
# План: составные типы параметров и полная типизация pipeline
|
# План: составные типы параметров и полная типизация pipeline
|
||||||
|
|
||||||
> Статус: **в реализации.** Фаза 0 (фундамент) ✔, Фаза 1 (рабочий инструмент
|
> Статус: **в реализации.** Фаза 0 (фундамент) ✔, Фаза 1 (рабочий инструмент
|
||||||
> в preview) ✔, Фаза 2 — начата: построен отдельный **новый registry**
|
> в preview) ✔, Фаза 2 (простые инструменты без составных типов) — переведены
|
||||||
> (`web/src/lib/registry-new/`) с первыми инструментами (`add-border-png`,
|
> все одиночные инструменты (18 шт): `add-border-png`, `add-stroke-png`,
|
||||||
> `add-stroke-png`), preview переключён на него. Следующее: продолжить Фазу 2 —
|
> `find-contour-png` и весь блок 8-22 (convert/noise/pixelate/color/filters...).
|
||||||
> следующий инструмент `find-contour-png` в новый registry.
|
> Новый registry `web/src/lib/registry-new/` разбит по категориям
|
||||||
|
> (`geometry`, `alpha`, `convert`, `analyze`, `filters`, `color`), preview
|
||||||
|
> переключён на него. Следующее: Фаза 3 — инструменты с составными типами.
|
||||||
>
|
>
|
||||||
> Ключевые файлы нового registry: `web/src/lib/registry-new/{types,geometry,alpha,index}.ts`,
|
> Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts`
|
||||||
|
> (по файлу на категорию) + `web/src/lib/preview/categories.ts`,
|
||||||
> `web/src/lib/registry-schema.ts`. Старый `web/src/lib/registry.ts` разбит по
|
> `web/src/lib/registry-schema.ts`. Старый `web/src/lib/registry.ts` разбит по
|
||||||
> категориям в `web/src/lib/registry/` (см. `registry.ts` — тонкий баррель).
|
> категориям в `web/src/lib/registry/` (см. `registry.ts` — тонкий баррель).
|
||||||
|
|
||||||
@@ -21,6 +24,16 @@
|
|||||||
сборка, изоляция от старого, затем новый становится основным и старый
|
сборка, изоляция от старого, затем новый становится основным и старый
|
||||||
удаляется вместе со старым дизайном.
|
удаляется вместе со старым дизайном.
|
||||||
|
|
||||||
|
**Правило копий (важно!):** если для нового UI/registry нужно внести изменения
|
||||||
|
в файл, который **прямо или косвенно** уже используется `(old)` веткой, — этот
|
||||||
|
файл **НЕ трогаем**. Вместо этого делаем **копию** в новом месте (например,
|
||||||
|
в `lib/preview/`) и правим копию. Это приводит к дублированию, но
|
||||||
|
**гарантированно не задевает старую ветку сайта**. Пример: категории — новый
|
||||||
|
`lib/preview/categories.ts` (object as const) копирует и заменяет собой
|
||||||
|
`../categories` для нового кода, старый `categories.ts` обслуживает `(old)` и
|
||||||
|
остаётся без изменений. После перехода (Фаза 5) копия становится основной,
|
||||||
|
а исходник удаляется вместе со старым UI.
|
||||||
|
|
||||||
**Разделение схем: две независимые схемы.**
|
**Разделение схем: две независимые схемы.**
|
||||||
|
|
||||||
| Схема | Источник | Использование |
|
| Схема | Источник | Использование |
|
||||||
@@ -286,28 +299,32 @@ Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` —
|
|||||||
составных типов. Каждый — отдельный маленький diff (~15-30 строк), тем самым
|
составных типов. Каждый — отдельный маленький diff (~15-30 строк), тем самым
|
||||||
проверенным в Фазе 1 паттерном. **Переезжают в `registry-new`.**
|
проверенным в Фазе 1 паттерном. **Переезжают в `registry-new`.**
|
||||||
|
|
||||||
Переведено: `add-border-png` (Фаза 1), `add-stroke-png`. Следующий —
|
Переведено: `add-border-png` (Фаза 1), `add-stroke-png`, `find-contour-png`, плюс
|
||||||
`find-contour-png`.
|
весь блок простых инструментов ниже (пункты 8-22). Следующее — Фаза 3.
|
||||||
|
|
||||||
6. ~~add-stroke-png (color + slider)~~ → переведён в `registry-new` ✔
|
6. ~~add-stroke-png (color + slider)~~ → переведён в `registry-new` ✔
|
||||||
7. find-contour-png (color + slider) — **следующий**
|
7. ~~find-contour-png (color + slider)~~ → переведён в `registry-new` ✔
|
||||||
8. convert-png-to-jpg (color + slider)
|
8. ~~convert-png-to-jpg (color + slider)~~ → переведён в `registry-new` ✔
|
||||||
9. convert-png-to-webp (slider)
|
9. ~~convert-png-to-webp (slider)~~ → переведён в `registry-new` ✔
|
||||||
10. remove-color-from-png (color + slider)
|
10. ~~remove-color-from-png (color + slider)~~ → переведён в `registry-new` ✔
|
||||||
11. extract-color-from-png (color + slider)
|
11. ~~extract-color-from-png (color + slider)~~ → переведён в `registry-new` ✔
|
||||||
12. add-noise-png (slider + select + number)
|
12. ~~add-noise-png (slider + select + number)~~ → переведён в `registry-new` ✔
|
||||||
13. randomize-pixels-png (slider + number)
|
13. ~~randomize-pixels-png (slider + number)~~ → переведён в `registry-new` ✔
|
||||||
14. pixelate-png (slider)
|
14. ~~pixelate-png (slider)~~ → переведён в `registry-new` ✔
|
||||||
15. vignette-png (slider)
|
15. ~~vignette-png (slider)~~ → переведён в `registry-new` ✔
|
||||||
16. gamma-png (slider)
|
16. ~~gamma-png (slider)~~ → переведён в `registry-new` ✔
|
||||||
17. temperature-png (slider)
|
17. ~~temperature-png (slider)~~ → переведён в `registry-new` ✔
|
||||||
18. tint-png (color + slider)
|
18. ~~tint-png (color + slider)~~ → переведён в `registry-new` ✔
|
||||||
19. quantize-png (slider)
|
19. ~~quantize-png (slider)~~ → переведён в `registry-new` ✔
|
||||||
20. custom-palette-png (text)
|
20. ~~custom-palette-png (text)~~ → переведён в `registry-new` ✔
|
||||||
21. dithering-png (slider + select)
|
21. ~~dithering-png (slider + select)~~ → переведён в `registry-new` ✔
|
||||||
22. jpeg-artifacts-png (slider)
|
22. ~~jpeg-artifacts-png (slider)~~ → переведён в `registry-new` ✔
|
||||||
|
|
||||||
(и т.п. — все одиночные инструменты в этом же ключе)
|
> **Замечание (конвертеры):** старые `convert-png-to-jpg`/`convert-png-to-webp`
|
||||||
|
> несли `output`-метаданные (`mime`/`ext`/`qualityParamId`) для выбора формата
|
||||||
|
> вывода. В новом `ToolEntry<P>` поля `output` пока нет, поэтому при переводе
|
||||||
|
> эти метаданные не перенесены — preview пока отдаёт результат как PNG.
|
||||||
|
> Механика выбора формата/качества в новом UI — отдельный шаг (не блокирует Фазу 2).
|
||||||
|
|
||||||
### Фаза 3 — инструменты с составными типами (по типу, затем по инструментам)
|
### Фаза 3 — инструменты с составными типами (по типу, затем по инструментам)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CATEGORIES } from "../categories";
|
import { CATEGORY_IDS } from "./categories";
|
||||||
import { TOOLS, type ToolEntry } from "../registry-new";
|
import { TOOLS, type ToolEntry } from "../registry-new";
|
||||||
|
|
||||||
export type PreviewGroup = {
|
export type PreviewGroup = {
|
||||||
@@ -23,7 +23,7 @@ const GROUP_LABELS: Record<string, string> = {
|
|||||||
* Preview-каталог: полный реестр инструментов, сгруппированный по категориям.
|
* Preview-каталог: полный реестр инструментов, сгруппированный по категориям.
|
||||||
* Больше не урезаем до референс-набора — показываем все инструменты из TOOLS.
|
* Больше не урезаем до референс-набора — показываем все инструменты из TOOLS.
|
||||||
*/
|
*/
|
||||||
export const PREVIEW_GROUPS: PreviewGroup[] = CATEGORIES.map((category) => ({
|
export const PREVIEW_GROUPS: PreviewGroup[] = CATEGORY_IDS.map((category) => ({
|
||||||
id: category,
|
id: category,
|
||||||
label: GROUP_LABELS[category] ?? category.toUpperCase(),
|
label: GROUP_LABELS[category] ?? category.toUpperCase(),
|
||||||
tools: TOOLS.filter((tool) => tool.category === category),
|
tools: TOOLS.filter((tool) => tool.category === category),
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Категории инструментов для нового registry/preview.
|
||||||
|
*
|
||||||
|
* Единая точка правды: тип `CategoryId` и порядок отображения в каталоге
|
||||||
|
* выводятся из одного `as const`-объекта, чтобы не рассинхронизироваться.
|
||||||
|
* Этот файл — локальная копия подхода для нового UI; старый `../categories`
|
||||||
|
* (массив + union вручную) не трогаем — он обслуживает старый UI в `(old)/`.
|
||||||
|
*
|
||||||
|
* Человекочитаемые названия живут в словарях i18n: секция categories,
|
||||||
|
* ключ = CategoryId.
|
||||||
|
*/
|
||||||
|
export const CATEGORIES = {
|
||||||
|
convert: "convert",
|
||||||
|
alpha: "alpha",
|
||||||
|
color: "color",
|
||||||
|
geometry: "geometry",
|
||||||
|
filters: "filters",
|
||||||
|
text: "text",
|
||||||
|
analyze: "analyze",
|
||||||
|
generate: "generate",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type CategoryId = (typeof CATEGORIES)[keyof typeof CATEGORIES];
|
||||||
|
|
||||||
|
/** Порядок категорий в каталоге (как задано в `CATEGORIES`). */
|
||||||
|
export const CATEGORY_IDS = Object.keys(CATEGORIES) as CategoryId[];
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ToolEntry } from "./types";
|
import type { ToolEntry } from "./types";
|
||||||
import { field, toolSchema } from "../registry-schema";
|
import { field, toolSchema } from "../registry-schema";
|
||||||
import { strokeImage } from "../core/morphology";
|
import { colorMask, removeColorToAlpha } from "../core/alpha";
|
||||||
|
import { contourImage, strokeImage } from "../core/morphology";
|
||||||
|
|
||||||
interface AddStrokeParams {
|
interface AddStrokeParams {
|
||||||
color: string;
|
color: string;
|
||||||
@@ -22,4 +23,45 @@ const addStroke: ToolEntry<AddStrokeParams> = {
|
|||||||
run: (img, p) => strokeImage(img, p.thickness, p.color),
|
run: (img, p) => strokeImage(img, p.thickness, p.color),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const alphaEntries = [addStroke];
|
interface FindContourParams {
|
||||||
|
color: string;
|
||||||
|
thickness: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findContourSchema = toolSchema<FindContourParams>({
|
||||||
|
color: field.color({ default: "#000000" }),
|
||||||
|
thickness: field.slider({ min: 1, max: 5, step: 1, default: 1 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const findContour: ToolEntry<FindContourParams> = {
|
||||||
|
id: "find-contour-png",
|
||||||
|
title: "Find contour PNG",
|
||||||
|
description:
|
||||||
|
"Leaves only a line along the boundary of opaque regions in the chosen color and thickness.",
|
||||||
|
category: "alpha",
|
||||||
|
schema: findContourSchema,
|
||||||
|
run: (img, p) => contourImage(img, p.thickness, p.color),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RemoveColorParams {
|
||||||
|
targetColor: string;
|
||||||
|
tolerance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeColorSchema = toolSchema<RemoveColorParams>({
|
||||||
|
targetColor: field.color({ default: "#00ff00" }),
|
||||||
|
tolerance: field.slider({ min: 0, max: 100, step: 1, default: 10 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeColor: ToolEntry<RemoveColorParams> = {
|
||||||
|
id: "remove-color-from-png",
|
||||||
|
title: "Remove color from PNG (make transparent)",
|
||||||
|
description:
|
||||||
|
"Makes all pixels close to the chosen color transparent. The tolerance sets the allowed deviation as a percentage of the maximum color distance.",
|
||||||
|
category: "alpha",
|
||||||
|
schema: removeColorSchema,
|
||||||
|
run: (img, p) => removeColorToAlpha(img, p.targetColor, p.tolerance),
|
||||||
|
preview: (img, p) => colorMask(img, p.targetColor, p.tolerance),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const alphaEntries = [addStroke, findContour, removeColor];
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { ToolEntry } from "./types";
|
||||||
|
import { field, toolSchema } from "../registry-schema";
|
||||||
|
import { extractByColor } from "../core/masks";
|
||||||
|
|
||||||
|
interface ExtractColorParams {
|
||||||
|
color: string;
|
||||||
|
tolerance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractColorSchema = toolSchema<ExtractColorParams>({
|
||||||
|
color: field.color({ default: "#00ff88" }),
|
||||||
|
tolerance: field.slider({ min: 0, max: 50, step: 1, default: 10 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const extractColor: ToolEntry<ExtractColorParams> = {
|
||||||
|
id: "extract-color-from-png",
|
||||||
|
title: "Extract Color from PNG",
|
||||||
|
description:
|
||||||
|
"Keeps only pixels close to the chosen color and makes everything else transparent — the inverse of Remove Color.",
|
||||||
|
category: "analyze",
|
||||||
|
schema: extractColorSchema,
|
||||||
|
run: (img, p) => extractByColor(img, p.color, p.tolerance),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const analyzeEntries = [extractColor];
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import type { ToolEntry } from "./types";
|
||||||
|
import { field, toolSchema } from "../registry-schema";
|
||||||
|
import {
|
||||||
|
ditherImage,
|
||||||
|
mapToNearest,
|
||||||
|
quantizeImage,
|
||||||
|
} from "../core/quantize";
|
||||||
|
import { gammaCorrection, temperature, tint } from "../core/color";
|
||||||
|
import { parseHexList } from "../core/palette";
|
||||||
|
|
||||||
|
interface GammaParams {
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gammaSchema = toolSchema<GammaParams>({
|
||||||
|
value: field.slider({ min: 0.1, max: 3, step: 0.05, default: 1 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const gammaTool: ToolEntry<GammaParams> = {
|
||||||
|
id: "gamma-png",
|
||||||
|
title: "Gamma correction PNG",
|
||||||
|
description:
|
||||||
|
"Corrects midtone brightness. <1 darker, >1 lighter, 1 — unchanged.",
|
||||||
|
category: "color",
|
||||||
|
schema: gammaSchema,
|
||||||
|
run: (img, p) => gammaCorrection(img, p.value),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TemperatureParams {
|
||||||
|
percent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const temperatureSchema = toolSchema<TemperatureParams>({
|
||||||
|
percent: field.slider({ min: -100, max: 100, step: 1, default: 0 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const temperatureTool: ToolEntry<TemperatureParams> = {
|
||||||
|
id: "temperature-png",
|
||||||
|
title: "Temperature PNG",
|
||||||
|
description:
|
||||||
|
"Positive values make the image warmer (more orange), negative ones cooler (more blue).",
|
||||||
|
category: "color",
|
||||||
|
schema: temperatureSchema,
|
||||||
|
run: (img, p) => temperature(img, p.percent),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TintParams {
|
||||||
|
color: string;
|
||||||
|
strength: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tintSchema = toolSchema<TintParams>({
|
||||||
|
color: field.color({ default: "#ffb060" }),
|
||||||
|
strength: field.slider({ min: 0, max: 100, step: 1, default: 30 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tintTool: ToolEntry<TintParams> = {
|
||||||
|
id: "tint-png",
|
||||||
|
title: "Tint PNG",
|
||||||
|
description:
|
||||||
|
"Multiplies color channels by the chosen tint with the given strength.",
|
||||||
|
category: "color",
|
||||||
|
schema: tintSchema,
|
||||||
|
run: (img, p) => tint(img, p.color, p.strength),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface QuantizeParams {
|
||||||
|
colors: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const quantizeSchema = toolSchema<QuantizeParams>({
|
||||||
|
colors: field.slider({ min: 2, max: 64, step: 1, default: 16 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const quantizeTool: ToolEntry<QuantizeParams> = {
|
||||||
|
id: "quantize-png",
|
||||||
|
title: "Quantize PNG",
|
||||||
|
description:
|
||||||
|
"Reduces the image to k colors via median-cut palette. Transparent pixels are preserved.",
|
||||||
|
category: "color",
|
||||||
|
schema: quantizeSchema,
|
||||||
|
run: (img, p) => quantizeImage(img, p.colors).image,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CustomPaletteParams {
|
||||||
|
colors: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customPaletteSchema = toolSchema<CustomPaletteParams>({
|
||||||
|
colors: field.text({ default: "#000000,#ffffff" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const customPalette: ToolEntry<CustomPaletteParams> = {
|
||||||
|
id: "custom-palette-png",
|
||||||
|
title: "Custom Palette PNG",
|
||||||
|
description:
|
||||||
|
"Maps every pixel to the nearest color from your comma-separated hex list.",
|
||||||
|
category: "color",
|
||||||
|
schema: customPaletteSchema,
|
||||||
|
run: (img, p) => mapToNearest(img, parseHexList(p.colors)),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DitheringParams {
|
||||||
|
colors: number;
|
||||||
|
pattern: "floyd-steinberg" | "bayer";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ditheringSchema = toolSchema<DitheringParams>({
|
||||||
|
colors: field.slider({ min: 2, max: 16, step: 1, default: 4 }),
|
||||||
|
pattern: field.select({
|
||||||
|
default: "floyd-steinberg",
|
||||||
|
options: [
|
||||||
|
{ value: "floyd-steinberg", label: "Floyd–Steinberg" },
|
||||||
|
{ value: "bayer", label: "Bayer 4x4" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ditheringTool: ToolEntry<DitheringParams> = {
|
||||||
|
id: "dithering-png",
|
||||||
|
title: "Dithering PNG",
|
||||||
|
description:
|
||||||
|
"Applies Floyd–Steinberg error diffusion or ordered Bayer dithering while reducing to k colors.",
|
||||||
|
category: "color",
|
||||||
|
schema: ditheringSchema,
|
||||||
|
run: (img, p) => ditherImage(img, p.colors, p.pattern),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const colorEntries = [
|
||||||
|
gammaTool,
|
||||||
|
temperatureTool,
|
||||||
|
tintTool,
|
||||||
|
quantizeTool,
|
||||||
|
customPalette,
|
||||||
|
ditheringTool,
|
||||||
|
];
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { ToolEntry } from "./types";
|
||||||
|
import { field, toolSchema } from "../registry-schema";
|
||||||
|
import { flattenOntoColor } from "../core/alpha";
|
||||||
|
import { clonePixelImage } from "../core/types";
|
||||||
|
|
||||||
|
interface ConvertToJpgParams {
|
||||||
|
background: string;
|
||||||
|
quality: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertToJpgSchema = toolSchema<ConvertToJpgParams>({
|
||||||
|
background: field.color({ default: "#ffffff" }),
|
||||||
|
quality: field.slider({ min: 1, max: 100, step: 1, default: 90 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const convertToJpg: ToolEntry<ConvertToJpgParams> = {
|
||||||
|
id: "convert-png-to-jpg",
|
||||||
|
title: "Convert PNG to JPG",
|
||||||
|
description:
|
||||||
|
"Transparency is composited over the chosen backdrop color (white by default) and saved as JPEG.",
|
||||||
|
category: "convert",
|
||||||
|
schema: convertToJpgSchema,
|
||||||
|
run: (img, p) => flattenOntoColor(img, p.background),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ConvertToWebpParams {
|
||||||
|
quality: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertToWebpSchema = toolSchema<ConvertToWebpParams>({
|
||||||
|
quality: field.slider({ min: 1, max: 100, step: 1, default: 90 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const convertToWebp: ToolEntry<ConvertToWebpParams> = {
|
||||||
|
id: "convert-png-to-webp",
|
||||||
|
title: "Convert PNG to WebP",
|
||||||
|
description:
|
||||||
|
"Re-encodes the image into WebP with adjustable quality. Transparency is preserved.",
|
||||||
|
category: "convert",
|
||||||
|
schema: convertToWebpSchema,
|
||||||
|
run: (img) => clonePixelImage(img),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertEntries = [convertToJpg, convertToWebp];
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import type { ToolEntry } from "./types";
|
||||||
|
import { field, toolSchema } from "../registry-schema";
|
||||||
|
import { addNoise, pixelate, shuffleBlocks } from "../core/pixel-fx";
|
||||||
|
import { vignette } from "../core/effects";
|
||||||
|
import { jpegRoundtrip } from "../core/io";
|
||||||
|
|
||||||
|
interface VignetteParams {
|
||||||
|
strength: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const vignetteSchema = toolSchema<VignetteParams>({
|
||||||
|
strength: field.slider({ min: 0, max: 100, step: 5, default: 50 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const vignetteTool: ToolEntry<VignetteParams> = {
|
||||||
|
id: "vignette-png",
|
||||||
|
title: "Vignette PNG",
|
||||||
|
description:
|
||||||
|
"Smoothly darkens the edges of the image, leaving the center untouched.",
|
||||||
|
category: "filters",
|
||||||
|
schema: vignetteSchema,
|
||||||
|
run: (img, p) => vignette(img, p.strength),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PixelateParams {
|
||||||
|
blockSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pixelateSchema = toolSchema<PixelateParams>({
|
||||||
|
blockSize: field.slider({ min: 2, max: 64, step: 1, default: 8 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pixelateTool: ToolEntry<PixelateParams> = {
|
||||||
|
id: "pixelate-png",
|
||||||
|
title: "Pixelate PNG",
|
||||||
|
description:
|
||||||
|
"Averages every blockSize×blockSize area into one color — classic mosaic.",
|
||||||
|
category: "filters",
|
||||||
|
schema: pixelateSchema,
|
||||||
|
run: (img, p) => pixelate(img, p.blockSize),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RandomizePixelsParams {
|
||||||
|
blockSize: number;
|
||||||
|
seed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const randomizePixelsSchema = toolSchema<RandomizePixelsParams>({
|
||||||
|
blockSize: field.slider({ min: 1, max: 64, step: 1, default: 8 }),
|
||||||
|
seed: field.number({ min: 0, max: 999999, step: 1, default: 42 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const randomizePixels: ToolEntry<RandomizePixelsParams> = {
|
||||||
|
id: "randomize-pixels-png",
|
||||||
|
title: "Randomize Pixels PNG",
|
||||||
|
description:
|
||||||
|
"Shuffles blocks of the image between positions. Same seed gives the same arrangement.",
|
||||||
|
category: "filters",
|
||||||
|
schema: randomizePixelsSchema,
|
||||||
|
run: (img, p) => shuffleBlocks(img, p.blockSize, p.seed),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AddNoiseParams {
|
||||||
|
amount: number;
|
||||||
|
mode: "mono" | "color";
|
||||||
|
seed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addNoiseSchema = toolSchema<AddNoiseParams>({
|
||||||
|
amount: field.slider({ min: 0, max: 100, step: 1, default: 25 }),
|
||||||
|
mode: field.select({
|
||||||
|
default: "mono",
|
||||||
|
options: [
|
||||||
|
{ value: "mono", label: "Monochrome grain" },
|
||||||
|
{ value: "color", label: "Color noise" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
seed: field.number({ min: 0, max: 999999, step: 1, default: 1234 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const addNoiseTool: ToolEntry<AddNoiseParams> = {
|
||||||
|
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",
|
||||||
|
schema: addNoiseSchema,
|
||||||
|
run: (img, p) => addNoise(img, p.amount, p.mode, p.seed),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface JpegArtifactsParams {
|
||||||
|
quality: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const jpegArtifactsSchema = toolSchema<JpegArtifactsParams>({
|
||||||
|
quality: field.slider({ min: 1, max: 50, step: 1, default: 10 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const jpegArtifacts: ToolEntry<JpegArtifactsParams> = {
|
||||||
|
id: "jpeg-artifacts-png",
|
||||||
|
title: "JPEG artifacts",
|
||||||
|
description:
|
||||||
|
"Simulates low-quality JPEG re-compression — visible blocks and smeared colors.",
|
||||||
|
category: "filters",
|
||||||
|
schema: jpegArtifactsSchema,
|
||||||
|
run: (img, p) => jpegRoundtrip(img, p.quality),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filtersEntries = [
|
||||||
|
vignetteTool,
|
||||||
|
pixelateTool,
|
||||||
|
randomizePixels,
|
||||||
|
addNoiseTool,
|
||||||
|
jpegArtifacts,
|
||||||
|
];
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
import type { ToolEntry } from "./types";
|
import type { ToolEntry } from "./types";
|
||||||
import { geometryEntries } from "./geometry";
|
import { geometryEntries } from "./geometry";
|
||||||
import { alphaEntries } from "./alpha";
|
import { alphaEntries } from "./alpha";
|
||||||
|
import { convertEntries } from "./convert";
|
||||||
|
import { analyzeEntries } from "./analyze";
|
||||||
|
import { filtersEntries } from "./filters";
|
||||||
|
import { colorEntries } from "./color";
|
||||||
|
|
||||||
export type { ToolEntry } from "./types";
|
export type { ToolEntry } from "./types";
|
||||||
|
|
||||||
export const TOOLS: ToolEntry[] = [
|
export const TOOLS: ToolEntry[] = [
|
||||||
...geometryEntries,
|
...geometryEntries,
|
||||||
...alphaEntries,
|
...alphaEntries,
|
||||||
|
...convertEntries,
|
||||||
|
...analyzeEntries,
|
||||||
|
...filtersEntries,
|
||||||
|
...colorEntries,
|
||||||
] as unknown as ToolEntry[];
|
] as unknown as ToolEntry[];
|
||||||
|
|
||||||
export function getTool(id: string): ToolEntry | undefined {
|
export function getTool(id: string): ToolEntry | undefined {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CategoryId } from "../categories";
|
import type { CategoryId } from "../preview/categories";
|
||||||
import type { ToolSchema } from "../registry-schema";
|
import type { ToolSchema } from "../registry-schema";
|
||||||
import type { PixelImage } from "../core/types";
|
import type { PixelImage } from "../core/types";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user