feat: add registry tools for new preview flow

This commit is contained in:
2026-09-04 19:09:47 +05:00
parent 520a1e1d37
commit 1e9b35406d
11 changed files with 450 additions and 29 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import { CATEGORIES } from "../categories";
import { CATEGORY_IDS } from "./categories";
import { TOOLS, type ToolEntry } from "../registry-new";
export type PreviewGroup = {
@@ -23,7 +23,7 @@ const GROUP_LABELS: Record<string, string> = {
* Preview-каталог: полный реестр инструментов, сгруппированный по категориям.
* Больше не урезаем до референс-набора — показываем все инструменты из TOOLS.
*/
export const PREVIEW_GROUPS: PreviewGroup[] = CATEGORIES.map((category) => ({
export const PREVIEW_GROUPS: PreviewGroup[] = CATEGORY_IDS.map((category) => ({
id: category,
label: GROUP_LABELS[category] ?? category.toUpperCase(),
tools: TOOLS.filter((tool) => tool.category === category),
+26
View File
@@ -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[];
+44 -2
View File
@@ -1,6 +1,7 @@
import type { ToolEntry } from "./types";
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 {
color: string;
@@ -22,4 +23,45 @@ const addStroke: ToolEntry<AddStrokeParams> = {
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];
+25
View File
@@ -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];
+136
View File
@@ -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: "FloydSteinberg" },
{ value: "bayer", label: "Bayer 4x4" },
],
}),
});
const ditheringTool: ToolEntry<DitheringParams> = {
id: "dithering-png",
title: "Dithering PNG",
description:
"Applies FloydSteinberg 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,
];
+44
View File
@@ -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];
+115
View File
@@ -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,
];
+8
View File
@@ -1,12 +1,20 @@
import type { ToolEntry } from "./types";
import { geometryEntries } from "./geometry";
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 const TOOLS: ToolEntry[] = [
...geometryEntries,
...alphaEntries,
...convertEntries,
...analyzeEntries,
...filtersEntries,
...colorEntries,
] as unknown as ToolEntry[];
export function getTool(id: string): ToolEntry | undefined {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { CategoryId } from "../categories";
import type { CategoryId } from "../preview/categories";
import type { ToolSchema } from "../registry-schema";
import type { PixelImage } from "../core/types";