mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
feat: separate slug pages from tools
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import { CATEGORY_IDS } from "./categories";
|
||||
import { TOOLS, type ToolEntry } from "./registry";
|
||||
import { PAGES, type Page } from "./registry";
|
||||
|
||||
export type PreviewGroup = {
|
||||
id: string;
|
||||
tools: ToolEntry[];
|
||||
pages: Page[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Preview-каталог: полный реестр инструментов, сгруппированный по категориям.
|
||||
* Preview-каталог: все страницы, сгруппированные по категориям.
|
||||
*/
|
||||
export const PREVIEW_GROUPS: PreviewGroup[] = CATEGORY_IDS.map((category) => ({
|
||||
id: category,
|
||||
tools: TOOLS.filter((tool) => tool.category === category),
|
||||
})).filter((group) => group.tools.length > 0);
|
||||
pages: PAGES.filter((page) => page.category === category),
|
||||
})).filter((group) => group.pages.length > 0);
|
||||
|
||||
export const PREVIEW_TOTAL = TOOLS.length;
|
||||
export const PREVIEW_TOTAL = PAGES.length;
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import { execute } from "$lib/executor";
|
||||
import {
|
||||
toolDescription,
|
||||
toolTitle,
|
||||
pageDescription,
|
||||
pageTitle,
|
||||
verdictText,
|
||||
} from "$lib/i18n/schema-tool-strings";
|
||||
import { t } from "$lib/i18n/t";
|
||||
import type { FileResult, ToolEntry } from "$lib/registry";
|
||||
import type { FileResult, Page, Tool } from "$lib/registry";
|
||||
import {
|
||||
applySourceDefaults,
|
||||
defaultSchemaParams,
|
||||
@@ -22,12 +22,13 @@
|
||||
import { downloadZip } from "$lib/zip";
|
||||
|
||||
interface Props {
|
||||
tool: ToolEntry;
|
||||
page: Page;
|
||||
tool: Tool;
|
||||
}
|
||||
type DisplayError =
|
||||
| { kind: "i18n"; key: string; vars?: ErrorVars }
|
||||
| { kind: "plain"; text: string };
|
||||
let { tool }: Props = $props();
|
||||
let { page, tool }: Props = $props();
|
||||
|
||||
const schema = $derived(
|
||||
tool.schema as ToolSchema<Record<string, unknown>> | undefined,
|
||||
@@ -141,7 +142,7 @@
|
||||
try {
|
||||
if (resultKind === "files") {
|
||||
if (!fileResult) return;
|
||||
await downloadZip(fileResult.files, `${tool.id}.zip`);
|
||||
await downloadZip(fileResult.files, `${page.slug}.zip`);
|
||||
return;
|
||||
}
|
||||
if (!result) return;
|
||||
@@ -150,7 +151,7 @@
|
||||
? Number(values[out.qualityParamId]) / 100
|
||||
: undefined;
|
||||
const blob = await encode(result, out?.mime ?? "image/png", quality);
|
||||
downloadBlob(blob, `${tool.id}.${out?.ext ?? "png"}`);
|
||||
downloadBlob(blob, `${page.slug}.${out?.ext ?? "png"}`);
|
||||
} catch (e) {
|
||||
displayError = toDisplayError(e);
|
||||
} finally {
|
||||
@@ -175,7 +176,7 @@
|
||||
if (!textResult) return;
|
||||
try {
|
||||
const blob = new Blob([textResult], { type: "text/plain" });
|
||||
downloadBlob(blob, `${tool.id}.txt`);
|
||||
downloadBlob(blob, `${page.slug}.txt`);
|
||||
} catch (e) {
|
||||
displayError = toDisplayError(e);
|
||||
}
|
||||
@@ -208,8 +209,8 @@
|
||||
<div class="schema-tool">
|
||||
<header class="header">
|
||||
<div class="title-block">
|
||||
<h1>{toolTitle(tool)}</h1>
|
||||
<p class="lede">{toolDescription(tool)}</p>
|
||||
<h1>{pageTitle(page)}</h1>
|
||||
<p class="lede">{pageDescription(page)}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
id: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
index?: number;
|
||||
icon?: Component<{ size?: number; class?: string }>;
|
||||
}
|
||||
let { title, id, description, index, icon }: Props = $props();
|
||||
let { title, slug, description, index, icon }: Props = $props();
|
||||
</script>
|
||||
|
||||
<a class="tool-card" href={resolve("/tools/[id]", { id })}>
|
||||
<a class="tool-card" href={resolve("/tools/[id]", { id: slug })}>
|
||||
{#if icon}<span class="tool-icon"><Icon {icon} size={19} /></span>{/if}
|
||||
<span class="tool-copy">
|
||||
<strong>{title}</strong>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "./protocol";
|
||||
import { handleWorkerRequest } from "./worker-handler";
|
||||
import { field, toolSchema, type Dimension } from "../registry-schema";
|
||||
import type { ToolEntry } from "../registry";
|
||||
import type { Tool } from "../registry";
|
||||
|
||||
class FakeWorker implements WorkerLike {
|
||||
onmessage: ((event: MessageEvent<WorkerResponse>) => void) | null = null;
|
||||
@@ -61,15 +61,11 @@ function createFactory(throwOnPost = false): {
|
||||
const emptySchema = toolSchema<Record<string, never>>({});
|
||||
|
||||
function makeTool(
|
||||
run: ToolEntry["run"] = () => "direct",
|
||||
overrides: Partial<ToolEntry> = {},
|
||||
): ToolEntry {
|
||||
run: Tool["run"] = () => "direct",
|
||||
overrides: Partial<Tool> = {},
|
||||
): Tool {
|
||||
return {
|
||||
id: "test-tool",
|
||||
slug: "test-tool",
|
||||
title: "Test tool",
|
||||
description: "Test tool",
|
||||
category: "analyze",
|
||||
schema: emptySchema,
|
||||
input: "none",
|
||||
run,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ToolError } from "../core/errors";
|
||||
import type { PixelImage } from "../core/types";
|
||||
import type { ToolContext, ToolEntry, ToolResult } from "../registry";
|
||||
import type { ToolContext, Tool, ToolResult } from "../registry";
|
||||
import { sanitizeSchemaParams } from "../registry-schema";
|
||||
import {
|
||||
decodeWorkerResponse,
|
||||
@@ -37,7 +37,7 @@ function defaultWorkerFactory(): WorkerLike {
|
||||
|
||||
export function createExecutor(
|
||||
options: { workerFactory?: WorkerFactory } = {},
|
||||
): (tool: ToolEntry, ctx: ExecuteContext) => Promise<ToolResult> {
|
||||
): (tool: Tool, ctx: ExecuteContext) => Promise<ToolResult> {
|
||||
const workerFactory = options.workerFactory ?? defaultWorkerFactory;
|
||||
let worker: WorkerLike | null = null;
|
||||
let workerTried = false;
|
||||
@@ -102,7 +102,7 @@ export function createExecutor(
|
||||
}
|
||||
|
||||
async function runDirect(
|
||||
tool: ToolEntry,
|
||||
tool: Tool,
|
||||
ctx: ExecuteContext,
|
||||
): Promise<ToolResult> {
|
||||
return await tool.run(ctx as ToolContext<Record<string, unknown>>);
|
||||
@@ -110,7 +110,7 @@ export function createExecutor(
|
||||
|
||||
function runInWorker(
|
||||
candidate: WorkerLike,
|
||||
tool: ToolEntry,
|
||||
tool: Tool,
|
||||
ctx: ExecuteContext,
|
||||
): Promise<ToolResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -136,10 +136,7 @@ export function createExecutor(
|
||||
});
|
||||
}
|
||||
|
||||
async function route(
|
||||
tool: ToolEntry,
|
||||
ctx: ExecuteContext,
|
||||
): Promise<ToolResult> {
|
||||
async function route(tool: Tool, ctx: ExecuteContext): Promise<ToolResult> {
|
||||
if (tool.domOnly) return await runDirect(tool, ctx);
|
||||
const candidate = ensureWorker();
|
||||
if (candidate === null) return await runDirect(tool, ctx);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { TOOLS } from "../registry";
|
||||
import { PAGES } from "../registry";
|
||||
import { setLocale } from "./locale.svelte";
|
||||
import {
|
||||
searchTools,
|
||||
toolDescription,
|
||||
toolTitle,
|
||||
pageDescription,
|
||||
pageTitle,
|
||||
searchPages,
|
||||
verdictText,
|
||||
} from "./schema-tool-strings";
|
||||
|
||||
@@ -12,53 +12,41 @@ afterEach(() => {
|
||||
setLocale("ru");
|
||||
});
|
||||
|
||||
const ids = (tools: { id: string }[]) => tools.map((t) => t.id);
|
||||
const slugs = (pages: { slug: string }[]) => pages.map((page) => page.slug);
|
||||
|
||||
describe("searchTools (кросс-языковой поиск каталога)", () => {
|
||||
describe("searchPages (кросс-языковой поиск каталога)", () => {
|
||||
it("пустой запрос возвращает весь каталог", () => {
|
||||
expect(searchTools(TOOLS, "")).toHaveLength(TOOLS.length);
|
||||
expect(searchTools(TOOLS, " ")).toHaveLength(TOOLS.length);
|
||||
expect(searchPages(PAGES, "")).toHaveLength(PAGES.length);
|
||||
expect(searchPages(PAGES, " ")).toHaveLength(PAGES.length);
|
||||
});
|
||||
|
||||
it("находит ru-название, даже когда локаль en", () => {
|
||||
setLocale("en");
|
||||
expect(ids(searchTools(TOOLS, "обрез"))).toContain("crop-png");
|
||||
expect(ids(searchTools(TOOLS, "пустые поля"))).toContain(
|
||||
expect(slugs(searchPages(PAGES, "обрез"))).toContain("crop-png");
|
||||
expect(slugs(searchPages(PAGES, "пустые поля"))).toContain(
|
||||
"trim-empty-space-png",
|
||||
);
|
||||
});
|
||||
|
||||
it("находит en-название, даже когда локаль ru", () => {
|
||||
expect(ids(searchTools(TOOLS, "resize"))).toContain("resize-png");
|
||||
expect(ids(searchTools(TOOLS, "round corners"))).toContain(
|
||||
expect(slugs(searchPages(PAGES, "resize"))).toContain("resize-png");
|
||||
expect(slugs(searchPages(PAGES, "round corners"))).toContain(
|
||||
"round-corners-png",
|
||||
);
|
||||
});
|
||||
|
||||
it("находит по id", () => {
|
||||
expect(ids(searchTools(TOOLS, "crop png"))).toContain("crop-png");
|
||||
it("находит по slug (тексту адреса)", () => {
|
||||
expect(slugs(searchPages(PAGES, "crop png"))).toContain("crop-png");
|
||||
});
|
||||
|
||||
it("ё и е — один запрос", () => {
|
||||
const byYo = searchTools(TOOLS, "чёрно");
|
||||
const byYe = searchTools(TOOLS, "черно");
|
||||
expect(ids(byYo)).toEqual(ids(byYe));
|
||||
const byYo = searchPages(PAGES, "чёрно");
|
||||
const byYe = searchPages(PAGES, "черно");
|
||||
expect(slugs(byYo)).toEqual(slugs(byYe));
|
||||
});
|
||||
|
||||
it("нет совпадения — пустой список", () => {
|
||||
expect(searchTools(TOOLS, "квантовый тостер")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("metadata fallback", () => {
|
||||
it("uses registry title and description in the base locale", () => {
|
||||
setLocale("en");
|
||||
for (const tool of TOOLS) {
|
||||
expect(toolTitle(tool), `${tool.id}: title`).toBe(tool.title);
|
||||
expect(toolDescription(tool), `${tool.id}: description`).toBe(
|
||||
tool.description,
|
||||
);
|
||||
}
|
||||
expect(searchPages(PAGES, "квантовый тостер")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ToolEntry } from "$lib/registry";
|
||||
import type { Page } from "$lib/registry";
|
||||
import type { Field } from "$lib/registry-schema";
|
||||
import { getMergedDict } from "./locale.svelte";
|
||||
import { normalizeForSearch, scoreDoc, type SearchDoc } from "./matching";
|
||||
@@ -12,12 +12,12 @@ function labelOf(id: string): string {
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
export function toolTitle(tool: ToolEntry): string {
|
||||
return getMergedDict().tools[tool.id]?.title ?? tool.title;
|
||||
export function pageTitle(page: Page): string {
|
||||
return getMergedDict().tools[page.slug]?.title ?? page.title;
|
||||
}
|
||||
|
||||
export function toolDescription(tool: ToolEntry): string {
|
||||
return getMergedDict().tools[tool.id]?.description ?? tool.description;
|
||||
export function pageDescription(page: Page): string {
|
||||
return getMergedDict().tools[page.slug]?.description ?? page.description;
|
||||
}
|
||||
|
||||
export function fieldLabel(field: Field<unknown>, id: string): string {
|
||||
@@ -64,33 +64,30 @@ function dedupe(values: string[]): string[] {
|
||||
return [...new Set(values.filter((v) => v.length > 0))];
|
||||
}
|
||||
|
||||
export function toolSearchDoc(tool: ToolEntry): SearchDoc {
|
||||
const active = getMergedDict().tools[tool.id];
|
||||
export function pageSearchDoc(page: Page): SearchDoc {
|
||||
const active = getMergedDict().tools[page.slug];
|
||||
return {
|
||||
id: tool.id,
|
||||
id: page.slug,
|
||||
titles: dedupe([
|
||||
active?.title ?? "",
|
||||
ru.tools[tool.id]?.title ?? "",
|
||||
tool.title,
|
||||
ru.tools[page.slug]?.title ?? "",
|
||||
page.title,
|
||||
]),
|
||||
descriptions: dedupe([
|
||||
active?.description ?? "",
|
||||
ru.tools[tool.id]?.description ?? "",
|
||||
tool.description,
|
||||
ru.tools[page.slug]?.description ?? "",
|
||||
page.description,
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Кросс-языковой поиск по каталогу: скор через `scoreDoc` на
|
||||
* `toolSearchDoc` (заголовки/описания всех локалей). Порядок каталога
|
||||
* `pageSearchDoc` (заголовки/описания всех локалей). Порядок каталога
|
||||
* сохраняется, элементы без совпадения отбрасываются.
|
||||
*/
|
||||
export function searchTools(
|
||||
tools: readonly ToolEntry[],
|
||||
query: string,
|
||||
): ToolEntry[] {
|
||||
export function searchPages(pages: readonly Page[], query: string): Page[] {
|
||||
const q = normalizeForSearch(query.trim());
|
||||
if (q.length === 0) return [...tools];
|
||||
return tools.filter((tool) => scoreDoc(toolSearchDoc(tool), q) !== null);
|
||||
if (q.length === 0) return [...pages];
|
||||
return pages.filter((page) => scoreDoc(pageSearchDoc(page), q) !== null);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { getLocale, setLocale } from "./locale.svelte";
|
||||
import { LOCALE_TAGS } from "./dict";
|
||||
import { normalizeForSearch } from "./matching";
|
||||
import { t } from "./t";
|
||||
import { getTool, TOOLS } from "../registry";
|
||||
import { toolTitle } from "./schema-tool-strings";
|
||||
import { getPageBySlug, PAGES } from "../registry";
|
||||
import { pageTitle } from "./schema-tool-strings";
|
||||
|
||||
afterEach(() => {
|
||||
setLocale("ru");
|
||||
@@ -52,11 +52,11 @@ describe("Смоук §6 i18n (новый UI)", () => {
|
||||
});
|
||||
|
||||
it("4. Заголовок инструмента переключается локалями (schema-tool-strings)", () => {
|
||||
const tool = getTool("crop-png") ?? TOOLS[0];
|
||||
const page = getPageBySlug("crop-png") ?? PAGES[0];
|
||||
setLocale("ru");
|
||||
expect(toolTitle(tool)).toContain("Обрезать");
|
||||
expect(pageTitle(page)).toContain("Обрезать");
|
||||
setLocale("en");
|
||||
expect(toolTitle(tool)).toBe(tool.title);
|
||||
expect(pageTitle(page)).toBe(page.title);
|
||||
});
|
||||
|
||||
it("5. Ошибки с vars локализуются на оба языка", () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TOOLS, type ToolEntry } from "../registry";
|
||||
import { PAGES, TOOLS, type Tool } from "../registry";
|
||||
import { en } from "./en";
|
||||
import { ru } from "./ru";
|
||||
|
||||
function toolKeys(tool: ToolEntry): { fields: string[]; groups: string[] } {
|
||||
function toolKeys(tool: Tool): { fields: string[]; groups: string[] } {
|
||||
const fields: string[] = [];
|
||||
for (const field of Object.values(tool.schema?.fields ?? {})) {
|
||||
const label = (field as { spec?: { label?: string } }).spec?.label;
|
||||
@@ -18,7 +18,7 @@ function toolKeys(tool: ToolEntry): { fields: string[]; groups: string[] } {
|
||||
type SelectOption = { value: string; label: string };
|
||||
|
||||
function selectOptions(
|
||||
tool: ToolEntry,
|
||||
tool: Tool,
|
||||
): { fieldId: string; option: SelectOption }[] {
|
||||
const result: { fieldId: string; option: SelectOption }[] = [];
|
||||
for (const [fieldId, field] of Object.entries(tool.schema?.fields ?? {})) {
|
||||
@@ -32,12 +32,12 @@ function selectOptions(
|
||||
}
|
||||
|
||||
describe("полнота словарей для нового registry", () => {
|
||||
it("у каждого инструмента есть перевод ru с непустыми title/description", () => {
|
||||
for (const tool of TOOLS) {
|
||||
const strings = ru.tools[tool.id];
|
||||
expect(strings, `нет перевода ru для ${tool.id}`).toBeDefined();
|
||||
expect(strings?.title, `${tool.id}: title`).toBeTruthy();
|
||||
expect(strings?.description, `${tool.id}: description`).toBeTruthy();
|
||||
it("у каждой страницы есть перевод ru с непустыми title/description", () => {
|
||||
for (const page of PAGES) {
|
||||
const strings = ru.tools[page.slug];
|
||||
expect(strings, `нет перевода ru для ${page.slug}`).toBeDefined();
|
||||
expect(strings?.title, `${page.slug}: title`).toBeTruthy();
|
||||
expect(strings?.description, `${page.slug}: description`).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,11 +75,14 @@ describe("полнота словарей для нового registry", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("в словарях нет лишних инструментов", () => {
|
||||
const ids = new Set(TOOLS.map((tool) => tool.id));
|
||||
it("в словарях нет ключей без записи в реестре", () => {
|
||||
const known = new Set([
|
||||
...TOOLS.map((tool) => tool.id),
|
||||
...PAGES.map((page) => page.slug),
|
||||
]);
|
||||
for (const dict of [ru.tools, en.tools]) {
|
||||
for (const id of Object.keys(dict)) {
|
||||
expect(ids.has(id), `лишний инструмент в словаре: ${id}`).toBe(true);
|
||||
for (const key of Object.keys(dict)) {
|
||||
expect(known.has(key), `лишний ключ в словаре: ${key}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { alphaEntries } from "./alpha";
|
||||
import { analyzeEntries } from "./analyze";
|
||||
import { colorEntries } from "./color";
|
||||
import { convertEntries } from "./convert";
|
||||
import { filtersEntries } from "./filters";
|
||||
import { generateEntries } from "./generate";
|
||||
import { geometryEntries } from "./geometry";
|
||||
import { textEntries } from "./text";
|
||||
import type { ToolEntry } from "./types";
|
||||
import { alphaTools } from "./tools/alpha";
|
||||
import { analyzeTools } from "./tools/analyze";
|
||||
import { colorTools } from "./tools/color";
|
||||
import { convertTools } from "./tools/convert";
|
||||
import { filtersTools } from "./tools/filters";
|
||||
import { generateTools } from "./tools/generate";
|
||||
import { geometryTools } from "./tools/geometry";
|
||||
import { textTools } from "./tools/text";
|
||||
import { alphaPages } from "./pages/alpha";
|
||||
import { analyzePages } from "./pages/analyze";
|
||||
import { colorPages } from "./pages/color";
|
||||
import { convertPages } from "./pages/convert";
|
||||
import { filtersPages } from "./pages/filters";
|
||||
import { generatePages } from "./pages/generate";
|
||||
import { geometryPages } from "./pages/geometry";
|
||||
import { textPages } from "./pages/text";
|
||||
import type { Page, Tool } from "./types";
|
||||
|
||||
export {
|
||||
genTool,
|
||||
@@ -20,30 +28,43 @@ export {
|
||||
export type {
|
||||
FileResult,
|
||||
InputMode,
|
||||
Page,
|
||||
PageStep,
|
||||
ResultKind,
|
||||
Tool,
|
||||
ToolContext,
|
||||
ToolEntry,
|
||||
ToolImageFile,
|
||||
ToolResult,
|
||||
VerdictResult,
|
||||
} from "./types";
|
||||
|
||||
export const TOOLS: ToolEntry[] = [
|
||||
...geometryEntries,
|
||||
...alphaEntries,
|
||||
...convertEntries,
|
||||
...analyzeEntries,
|
||||
...filtersEntries,
|
||||
...colorEntries,
|
||||
...generateEntries,
|
||||
...textEntries,
|
||||
] as unknown as ToolEntry[];
|
||||
export const TOOLS: Tool[] = [
|
||||
...geometryTools,
|
||||
...alphaTools,
|
||||
...convertTools,
|
||||
...analyzeTools,
|
||||
...filtersTools,
|
||||
...colorTools,
|
||||
...generateTools,
|
||||
...textTools,
|
||||
] as unknown as Tool[];
|
||||
|
||||
export function getTool(id: string): ToolEntry | undefined {
|
||||
export const PAGES: Page[] = [
|
||||
...geometryPages,
|
||||
...alphaPages,
|
||||
...convertPages,
|
||||
...analyzePages,
|
||||
...filtersPages,
|
||||
...colorPages,
|
||||
...generatePages,
|
||||
...textPages,
|
||||
] as unknown as Page[];
|
||||
|
||||
export function getTool(id: string): Tool | undefined {
|
||||
return TOOLS.find((tool) => tool.id === id);
|
||||
}
|
||||
|
||||
/** Поиск по адресу страницы; `getTool` остаётся поиском по внутреннему имени. */
|
||||
export function getToolBySlug(slug: string): ToolEntry | undefined {
|
||||
return TOOLS.find((tool) => tool.slug === slug);
|
||||
/** Единственный способ достать страницу: маршрут, крошка и имя файла идут от slug. */
|
||||
export function getPageBySlug(slug: string): Page | undefined {
|
||||
return PAGES.find((page) => page.slug === slug);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { Page } from "../types";
|
||||
|
||||
export const alphaPages: Page[] = [
|
||||
{
|
||||
slug: "add-stroke-png",
|
||||
title: "Outline PNG",
|
||||
description:
|
||||
"Adds a colored ring outline around the opaque content with the chosen thickness.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "add-stroke-png" }],
|
||||
},
|
||||
{
|
||||
slug: "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",
|
||||
steps: [{ id: "find-contour-png" }],
|
||||
},
|
||||
{
|
||||
slug: "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",
|
||||
steps: [{ id: "remove-color-from-png" }],
|
||||
},
|
||||
{
|
||||
slug: "circle-mask-png",
|
||||
title: "Circle Mask PNG",
|
||||
description:
|
||||
"Cuts the image into a circle. Diameter is set as a share of the smaller side.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "circle-mask-png" }],
|
||||
},
|
||||
{
|
||||
slug: "square-mask-png",
|
||||
title: "Square Mask PNG",
|
||||
description:
|
||||
"Cuts the image into a rectangle with sides as a share of the smaller side.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "square-mask-png" }],
|
||||
},
|
||||
{
|
||||
slug: "star-mask-png",
|
||||
title: "Star Mask PNG",
|
||||
description:
|
||||
"Cuts the image into an n-pointed star with adjustable inner radius and rotation.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "star-mask-png" }],
|
||||
},
|
||||
{
|
||||
slug: "wavy-mask-png",
|
||||
title: "Wavy Mask PNG",
|
||||
description:
|
||||
"Cuts the image into a wavy-edged circle: radius is modulated by a sine with chosen amplitude and frequency.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "wavy-mask-png" }],
|
||||
},
|
||||
{
|
||||
slug: "remove-alpha-channel-png",
|
||||
title: "Remove alpha channel PNG",
|
||||
description:
|
||||
"Composites the image over a white background and saves without transparency.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "remove-alpha-channel-png" }],
|
||||
},
|
||||
{
|
||||
slug: "set-alpha-channel-png",
|
||||
title: "Set alpha channel PNG",
|
||||
description:
|
||||
"Assigns the same opacity to all pixels; colors stay unchanged.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "set-alpha-channel-png" }],
|
||||
},
|
||||
{
|
||||
slug: "extract-alpha-mask-png",
|
||||
title: "Extract alpha mask PNG",
|
||||
description: "Turns transparency into a black-and-white opaque mask.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "extract-alpha-mask-png" }],
|
||||
},
|
||||
{
|
||||
slug: "round-corners-png",
|
||||
title: "Round corners PNG",
|
||||
description:
|
||||
"Clips corners by a radius set as a percentage of half the smaller side.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "round-corners-png" }],
|
||||
},
|
||||
{
|
||||
slug: "invert-alpha-png",
|
||||
title: "Invert alpha PNG",
|
||||
description: "Opaque areas become transparent and vice versa.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "invert-alpha-png" }],
|
||||
},
|
||||
{
|
||||
slug: "remove-background-png",
|
||||
title: "Remove background PNG (smart)",
|
||||
description:
|
||||
"Removes a solid background: by color with tolerance, outer regions from the edges only, or every matching pixel. Can smooth the boundary.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "remove-background-png" }],
|
||||
},
|
||||
{
|
||||
slug: "make-thicker-png",
|
||||
title: "Thicken PNG",
|
||||
description: "Expands opaque areas by the given number of pixels.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "make-thicker-png" }],
|
||||
},
|
||||
{
|
||||
slug: "make-thinner-png",
|
||||
title: "Thin PNG",
|
||||
description:
|
||||
"Shrinks opaque areas — thins the strokes of text and details.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "make-thinner-png" }],
|
||||
},
|
||||
{
|
||||
slug: "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",
|
||||
steps: [{ id: "feather-edges-png" }],
|
||||
},
|
||||
{
|
||||
slug: "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",
|
||||
steps: [{ id: "clean-edges-png" }],
|
||||
},
|
||||
{
|
||||
slug: "harden-alpha-png",
|
||||
title: "Harden edges PNG",
|
||||
description:
|
||||
"Binarizes the alpha channel by threshold: semi-transparent pixels become either fully transparent or fully opaque.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "harden-alpha-png" }],
|
||||
},
|
||||
{
|
||||
slug: "despeckle-alpha-png",
|
||||
title: "Despeckle PNG",
|
||||
description:
|
||||
"Opening: removes lone semi-transparent pixels and small specks.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "despeckle-alpha-png" }],
|
||||
},
|
||||
{
|
||||
slug: "close-holes-png",
|
||||
title: "Close holes PNG",
|
||||
description: "Closing: fills lone transparent dots inside the object.",
|
||||
category: "alpha",
|
||||
steps: [{ id: "close-holes-png" }],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { Page } from "../types";
|
||||
|
||||
export const analyzePages: Page[] = [
|
||||
{
|
||||
slug: "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",
|
||||
steps: [{ id: "extract-color-from-png" }],
|
||||
},
|
||||
{
|
||||
slug: "show-transparent-png",
|
||||
title: "Show Transparent Areas PNG",
|
||||
description:
|
||||
"Highlights every transparent or semi-transparent pixel with the chosen color so gaps become obvious.",
|
||||
category: "analyze",
|
||||
steps: [{ id: "show-transparent-png" }],
|
||||
},
|
||||
{
|
||||
slug: "show-grayscale-pixels-png",
|
||||
title: "Show Grayscale Pixels PNG",
|
||||
description:
|
||||
"Finds pixels whose channels are nearly equal and renders them as a mask. Tolerance is in channel units.",
|
||||
category: "analyze",
|
||||
steps: [{ id: "show-grayscale-pixels-png" }],
|
||||
},
|
||||
{
|
||||
slug: "show-color-pixels-png",
|
||||
title: "Show Color Pixels PNG",
|
||||
description:
|
||||
"Finds colored (non-gray) pixels beyond the channel tolerance and renders them as a mask.",
|
||||
category: "analyze",
|
||||
steps: [{ id: "show-color-pixels-png" }],
|
||||
},
|
||||
{
|
||||
slug: "light-pixel-mask-png",
|
||||
title: "Light Pixel Mask PNG",
|
||||
description: "Selects pixels brighter than the luminance threshold.",
|
||||
category: "analyze",
|
||||
steps: [{ id: "light-pixel-mask-png" }],
|
||||
},
|
||||
{
|
||||
slug: "dark-pixel-mask-png",
|
||||
title: "Dark Pixel Mask PNG",
|
||||
description: "Selects pixels darker than the luminance threshold.",
|
||||
category: "analyze",
|
||||
steps: [{ id: "dark-pixel-mask-png" }],
|
||||
},
|
||||
{
|
||||
slug: "unique-color-mask-png",
|
||||
title: "Unique Color Mask PNG",
|
||||
description:
|
||||
"Selects colors that occur no more than the given number of times — rare and one-off pixels.",
|
||||
category: "analyze",
|
||||
steps: [{ id: "unique-color-mask-png" }],
|
||||
},
|
||||
{
|
||||
slug: "verify-is-png",
|
||||
title: "Verify If Image Is a PNG",
|
||||
description:
|
||||
"Checks the signature of pasted base64 / data-uri content and reports whether it is a real PNG.",
|
||||
category: "analyze",
|
||||
steps: [{ id: "verify-is-png" }],
|
||||
},
|
||||
{
|
||||
slug: "png-is-grayscale",
|
||||
title: "Check: is PNG grayscale?",
|
||||
description: "Reports whether the image consists only of shades of gray.",
|
||||
category: "analyze",
|
||||
steps: [{ id: "png-is-grayscale" }],
|
||||
},
|
||||
{
|
||||
slug: "png-file-size",
|
||||
title: "PNG File Size",
|
||||
description:
|
||||
"Encodes the image as PNG and reports the resulting file size.",
|
||||
category: "analyze",
|
||||
steps: [{ id: "png-file-size" }],
|
||||
},
|
||||
{
|
||||
slug: "png-is-transparent",
|
||||
title: "Check: is PNG transparent?",
|
||||
description:
|
||||
"Reports whether the image contains transparent or semi-transparent pixels.",
|
||||
category: "analyze",
|
||||
steps: [{ id: "png-is-transparent" }],
|
||||
},
|
||||
{
|
||||
slug: "png-orientation",
|
||||
title: "PNG orientation",
|
||||
description: "Reports whether it is portrait, landscape or square.",
|
||||
category: "analyze",
|
||||
steps: [{ id: "png-orientation" }],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,202 @@
|
||||
import type { Page } from "../types";
|
||||
|
||||
export const colorPages: Page[] = [
|
||||
{
|
||||
slug: "two-colors-png",
|
||||
title: "Two colors PNG",
|
||||
description:
|
||||
"Recolors the image into two chosen colors by luminance threshold.",
|
||||
category: "color",
|
||||
steps: [{ id: "two-colors-png" }],
|
||||
},
|
||||
{
|
||||
slug: "gamma-png",
|
||||
title: "Gamma correction PNG",
|
||||
description:
|
||||
"Corrects midtone brightness. <1 darker, >1 lighter, 1 — unchanged.",
|
||||
category: "color",
|
||||
steps: [{ id: "gamma-png" }],
|
||||
},
|
||||
{
|
||||
slug: "temperature-png",
|
||||
title: "Temperature PNG",
|
||||
description:
|
||||
"Positive values make the image warmer (more orange), negative ones cooler (more blue).",
|
||||
category: "color",
|
||||
steps: [{ id: "temperature-png" }],
|
||||
},
|
||||
{
|
||||
slug: "tint-png",
|
||||
title: "Tint PNG",
|
||||
description:
|
||||
"Multiplies color channels by the chosen tint with the given strength.",
|
||||
category: "color",
|
||||
steps: [{ id: "tint-png" }],
|
||||
},
|
||||
{
|
||||
slug: "quantize-png",
|
||||
title: "Quantize PNG",
|
||||
description:
|
||||
"Reduces the image to k colors via median-cut palette. Transparent pixels are preserved.",
|
||||
category: "color",
|
||||
steps: [{ id: "quantize-png" }],
|
||||
},
|
||||
{
|
||||
slug: "custom-palette-png",
|
||||
title: "Custom Palette PNG",
|
||||
description:
|
||||
"Maps every pixel to the nearest color from your comma-separated hex list.",
|
||||
category: "color",
|
||||
steps: [{ id: "custom-palette-png" }],
|
||||
},
|
||||
{
|
||||
slug: "dithering-png",
|
||||
title: "Dithering PNG",
|
||||
description:
|
||||
"Applies Floyd–Steinberg error diffusion or ordered Bayer dithering while reducing to k colors.",
|
||||
category: "color",
|
||||
steps: [{ id: "dithering-png" }],
|
||||
},
|
||||
{
|
||||
slug: "grayscale-png",
|
||||
title: "Grayscale PNG",
|
||||
description:
|
||||
"Converts the image to shades of gray using the BT.601 luminance formula. Alpha is preserved.",
|
||||
category: "color",
|
||||
steps: [{ id: "grayscale-png" }],
|
||||
},
|
||||
{
|
||||
slug: "invert-colors-png",
|
||||
title: "Invert colors PNG",
|
||||
description:
|
||||
"Inverts each color channel (255 − value). Alpha is unchanged.",
|
||||
category: "color",
|
||||
steps: [{ id: "invert-colors-png" }],
|
||||
},
|
||||
{
|
||||
slug: "adjust-brightness-contrast-png",
|
||||
title: "Brightness & contrast PNG",
|
||||
description:
|
||||
"Adjusts brightness and contrast in the range from −100 to +100. Zero means no change.",
|
||||
category: "color",
|
||||
steps: [{ id: "adjust-brightness-contrast-png" }],
|
||||
},
|
||||
{
|
||||
slug: "change-png-opacity",
|
||||
title: "Change PNG opacity",
|
||||
description:
|
||||
"Multiplies the alpha channel by a percentage: 0% — fully transparent, 100% — unchanged.",
|
||||
category: "color",
|
||||
steps: [{ id: "change-png-opacity" }],
|
||||
},
|
||||
{
|
||||
slug: "sepia-png",
|
||||
title: "Sepia effect",
|
||||
description: "Tints the image into the warm brown tones of classic sepia.",
|
||||
category: "color",
|
||||
steps: [{ id: "sepia-png" }],
|
||||
},
|
||||
{
|
||||
slug: "change-png-hue",
|
||||
title: "Change hue PNG",
|
||||
description:
|
||||
"Shifts the hue around the circle. Saturation and lightness are preserved.",
|
||||
category: "color",
|
||||
steps: [{ id: "change-png-hue" }],
|
||||
},
|
||||
{
|
||||
slug: "extract-channel-png",
|
||||
title: "Extract channel PNG",
|
||||
description:
|
||||
"Keeps only the chosen channel — red, green or blue — as shades of gray.",
|
||||
category: "color",
|
||||
steps: [{ id: "extract-channel-png" }],
|
||||
},
|
||||
{
|
||||
slug: "swap-channels-png",
|
||||
title: "Swap channels PNG",
|
||||
description:
|
||||
"Swaps two color channels — a quick way to get unusual coloring.",
|
||||
category: "color",
|
||||
steps: [{ id: "swap-channels-png" }],
|
||||
},
|
||||
{
|
||||
slug: "black-and-white-png",
|
||||
title: "Black & white threshold PNG",
|
||||
description:
|
||||
"Hard binarization by luminance: every pixel becomes black or white.",
|
||||
category: "color",
|
||||
steps: [{ id: "black-and-white-png" }],
|
||||
},
|
||||
{
|
||||
slug: "posterize-png",
|
||||
title: "Posterize PNG",
|
||||
description: "Reduces the number of levels per channel — a poster effect.",
|
||||
category: "color",
|
||||
steps: [{ id: "posterize-png" }],
|
||||
},
|
||||
{
|
||||
slug: "auto-contrast-png",
|
||||
title: "Auto contrast PNG",
|
||||
description:
|
||||
"Stretches each channel's range across the full available brightness range.",
|
||||
category: "color",
|
||||
steps: [{ id: "auto-contrast-png" }],
|
||||
},
|
||||
{
|
||||
slug: "decrease-color-count-png",
|
||||
title: "Decrease Color Count PNG",
|
||||
description:
|
||||
"Median-cut engine as a quick way to drop to 2–256 colors. Presets marked (extreme/strong/balanced/light) match the classic compression levels.",
|
||||
category: "color",
|
||||
steps: [{ id: "decrease-color-count-png" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-hsl",
|
||||
title: "Split PNG into HSL",
|
||||
description:
|
||||
"Decomposes the image into Hue, Saturation and Lightness components.",
|
||||
category: "color",
|
||||
steps: [{ id: "png-to-hsl" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-hsv",
|
||||
title: "Split PNG into HSV",
|
||||
description:
|
||||
"Decomposes the image into Hue, Saturation and Value (brightness) components.",
|
||||
category: "color",
|
||||
steps: [{ id: "png-to-hsv" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-hsi",
|
||||
title: "Split PNG into HSI",
|
||||
description:
|
||||
"Decomposes the image into Hue, Saturation and Intensity components.",
|
||||
category: "color",
|
||||
steps: [{ id: "png-to-hsi" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-cmyk",
|
||||
title: "Convert PNG to CMYK Colors",
|
||||
description:
|
||||
"Decomposes the image into print-style Cyan, Magenta, Yellow and Key (black) components.",
|
||||
category: "color",
|
||||
steps: [{ id: "png-to-cmyk" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-ycbcr",
|
||||
title: "Convert PNG to YCbCr Colors",
|
||||
description:
|
||||
"Decomposes the image into Luma (Y) and Blue-difference / Red-difference chroma components.",
|
||||
category: "color",
|
||||
steps: [{ id: "png-to-ycbcr" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-lab",
|
||||
title: "Convert PNG to LAB Colors",
|
||||
description:
|
||||
"Decomposes the image into perceptual Lightness and green–magenta / blue–yellow opponents.",
|
||||
category: "color",
|
||||
steps: [{ id: "png-to-lab" }],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { Page } from "../types";
|
||||
|
||||
export const convertPages: Page[] = [
|
||||
{
|
||||
slug: "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",
|
||||
steps: [{ id: "convert-png-to-jpg" }],
|
||||
},
|
||||
{
|
||||
slug: "convert-png-to-webp",
|
||||
title: "Convert PNG to WebP",
|
||||
description:
|
||||
"Re-encodes the image into WebP with adjustable quality. Transparency is preserved.",
|
||||
category: "convert",
|
||||
steps: [{ id: "convert-png-to-webp" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-bmp",
|
||||
title: "Convert PNG to BMP",
|
||||
description:
|
||||
"Saves the image as 24-bit BMP without an alpha channel: transparency is replaced with a black background.",
|
||||
category: "convert",
|
||||
steps: [{ id: "png-to-bmp" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-base64",
|
||||
title: "PNG to Base64",
|
||||
description:
|
||||
"Encodes the image into a base64 string for embedding in code or styles.",
|
||||
category: "convert",
|
||||
steps: [{ id: "png-to-base64" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-data-uri",
|
||||
title: "PNG to Data URI",
|
||||
description:
|
||||
"Builds a full data-uri (data:image/png;base64,…) for embedding in HTML/CSS.",
|
||||
category: "convert",
|
||||
steps: [{ id: "png-to-data-uri" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-hex",
|
||||
title: "PNG to HEX pixels",
|
||||
description:
|
||||
"Shows all pixels as rrggbbaa hex values — row by row, space separated.",
|
||||
category: "convert",
|
||||
steps: [{ id: "png-to-hex" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-bytes",
|
||||
title: "PNG to Bytes",
|
||||
description:
|
||||
"Lists every pixel as four decimal bytes (R G B A), one image row per line.",
|
||||
category: "convert",
|
||||
steps: [{ id: "png-to-bytes" }],
|
||||
},
|
||||
{
|
||||
slug: "png-to-rgb-values",
|
||||
title: "PNG to RGB Values",
|
||||
description:
|
||||
"Lists every pixel as rgba(r, g, b, a), one image row per line.",
|
||||
category: "convert",
|
||||
steps: [{ id: "png-to-rgb-values" }],
|
||||
},
|
||||
{
|
||||
slug: "base64-to-png",
|
||||
title: "Base64 to PNG",
|
||||
description:
|
||||
"Decodes a base64 string or data-uri back into an image. Paste the string on the left.",
|
||||
category: "convert",
|
||||
steps: [{ id: "base64-to-png" }],
|
||||
},
|
||||
{
|
||||
slug: "data-uri-to-png",
|
||||
title: "Data URI to PNG",
|
||||
description: "Decodes data:image/…;base64,… back into an image file.",
|
||||
category: "convert",
|
||||
steps: [{ id: "data-uri-to-png" }],
|
||||
},
|
||||
{
|
||||
slug: "hex-to-png",
|
||||
title: "HEX pixels to PNG",
|
||||
description:
|
||||
"Assembles an image from rrggbbaa hex values (space separated). Set the width — the height is computed automatically.",
|
||||
category: "convert",
|
||||
steps: [{ id: "hex-to-png" }],
|
||||
},
|
||||
{
|
||||
slug: "bytes-to-png",
|
||||
title: "Bytes to PNG",
|
||||
description:
|
||||
"Assembles an image from decimal RGBA byte numbers (any separators). Set the width — height is computed automatically.",
|
||||
category: "convert",
|
||||
steps: [{ id: "bytes-to-png" }],
|
||||
},
|
||||
{
|
||||
slug: "rgb-values-to-png",
|
||||
title: "RGB Values to PNG",
|
||||
description:
|
||||
"Assembles an image from rgba(r, g, b, a) numbers. Set the width — height is computed automatically.",
|
||||
category: "convert",
|
||||
steps: [{ id: "rgb-values-to-png" }],
|
||||
},
|
||||
{
|
||||
slug: "svg-to-png",
|
||||
title: "SVG to PNG",
|
||||
description:
|
||||
"Decodes SVG markup into a raster image. Paste the SVG code on the left.",
|
||||
category: "convert",
|
||||
steps: [{ id: "svg-to-png" }],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { Page } from "../types";
|
||||
|
||||
export const filtersPages: Page[] = [
|
||||
{
|
||||
slug: "blur-png",
|
||||
title: "Blur PNG",
|
||||
description:
|
||||
"Gaussian blur: three passes of separable box blur — fast at any radius. Transparent edges do not darken.",
|
||||
category: "filters",
|
||||
steps: [{ id: "blur-png" }],
|
||||
},
|
||||
{
|
||||
slug: "sharpen-png",
|
||||
title: "Sharpen PNG",
|
||||
description:
|
||||
"Emphasizes edges with a sharpening kernel; strength sets the blend with the original. 0% means no change.",
|
||||
category: "filters",
|
||||
steps: [{ id: "sharpen-png" }],
|
||||
},
|
||||
{
|
||||
slug: "silhouette-png",
|
||||
title: "Silhouette PNG",
|
||||
description:
|
||||
"Turns all visible pixels into a single solid color while keeping their transparency — instant silhouette.",
|
||||
category: "filters",
|
||||
steps: [{ id: "silhouette-png" }],
|
||||
},
|
||||
{
|
||||
slug: "vignette-png",
|
||||
title: "Vignette PNG",
|
||||
description:
|
||||
"Smoothly darkens the edges of the image, leaving the center untouched.",
|
||||
category: "filters",
|
||||
steps: [{ id: "vignette-png" }],
|
||||
},
|
||||
{
|
||||
slug: "pixelate-png",
|
||||
title: "Pixelate PNG",
|
||||
description:
|
||||
"Averages every blockSize×blockSize area into one color — classic mosaic.",
|
||||
category: "filters",
|
||||
steps: [{ id: "pixelate-png" }],
|
||||
},
|
||||
{
|
||||
slug: "randomize-pixels-png",
|
||||
title: "Randomize Pixels PNG",
|
||||
description:
|
||||
"Shuffles blocks of the image between positions. Same seed gives the same arrangement.",
|
||||
category: "filters",
|
||||
steps: [{ id: "randomize-pixels-png" }],
|
||||
},
|
||||
{
|
||||
slug: "add-noise-png",
|
||||
title: "Add Noise to PNG",
|
||||
description:
|
||||
"Adds film-grain style noise. Deterministic by seed; monochrome keeps original hue balance.",
|
||||
category: "filters",
|
||||
steps: [{ id: "add-noise-png" }],
|
||||
},
|
||||
{
|
||||
slug: "jpeg-artifacts-png",
|
||||
title: "JPEG artifacts",
|
||||
description:
|
||||
"Simulates low-quality JPEG re-compression — visible blocks and smeared colors.",
|
||||
category: "filters",
|
||||
steps: [{ id: "jpeg-artifacts-png" }],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,167 @@
|
||||
import type { Page } from "../types";
|
||||
|
||||
export const generatePages: Page[] = [
|
||||
{
|
||||
slug: "create-empty-png",
|
||||
title: "Create empty PNG",
|
||||
description:
|
||||
"Creates a blank canvas of the chosen dimensions, either transparent or filled with a solid color.",
|
||||
category: "generate",
|
||||
steps: [{ id: "create-empty-png" }],
|
||||
},
|
||||
{
|
||||
slug: "single-color-png",
|
||||
title: "Create solid color PNG",
|
||||
description: "Generates a rectangle of the given size and color.",
|
||||
category: "generate",
|
||||
steps: [{ id: "single-color-png" }],
|
||||
},
|
||||
{
|
||||
slug: "random-noise-png",
|
||||
title: "Create random noise PNG",
|
||||
description:
|
||||
"Generates an image with random pixels. The seed fixes the result: one seed — one image.",
|
||||
category: "generate",
|
||||
steps: [{ id: "random-noise-png" }],
|
||||
},
|
||||
{
|
||||
slug: "linear-gradient-png",
|
||||
title: "Create gradient PNG",
|
||||
description:
|
||||
"Generates a smooth transition between two colors along a chosen angle.",
|
||||
category: "generate",
|
||||
steps: [{ id: "linear-gradient-png" }],
|
||||
},
|
||||
{
|
||||
slug: "color-spectrum-png",
|
||||
title: "Color Spectrum PNG",
|
||||
description:
|
||||
"Full hue rainbow 0–360° along the chosen axis with adjustable saturation and lightness.",
|
||||
category: "generate",
|
||||
steps: [{ id: "color-spectrum-png" }],
|
||||
},
|
||||
{
|
||||
slug: "random-colors-png",
|
||||
title: "Random Color Blocks PNG",
|
||||
description:
|
||||
"Fills the canvas with random vivid color blocks. Deterministic by seed.",
|
||||
category: "generate",
|
||||
steps: [{ id: "random-colors-png" }],
|
||||
},
|
||||
{
|
||||
slug: "draw-grid-png",
|
||||
title: "Draw Grid PNG",
|
||||
description:
|
||||
"Draws a grid with custom columns, rows and line width on a transparent or white background.",
|
||||
category: "generate",
|
||||
steps: [{ id: "draw-grid-png" }],
|
||||
},
|
||||
{
|
||||
slug: "placeholder-png",
|
||||
title: "Create Placeholder PNG",
|
||||
description:
|
||||
"Generates a placeholder rectangle with its dimensions printed in the center.",
|
||||
category: "generate",
|
||||
steps: [{ id: "placeholder-png" }],
|
||||
},
|
||||
{
|
||||
slug: "blend-two-png",
|
||||
title: "Blend Two Colors PNG",
|
||||
description: "A continuous horizontal gradient between two colors.",
|
||||
category: "generate",
|
||||
steps: [{ id: "blend-two-png" }],
|
||||
},
|
||||
{
|
||||
slug: "step-colors-png",
|
||||
title: "Color Steps PNG",
|
||||
description: "A discrete set of evenly spaced steps between two colors.",
|
||||
category: "generate",
|
||||
steps: [{ id: "step-colors-png" }],
|
||||
},
|
||||
{
|
||||
slug: "emoji-to-png",
|
||||
title: "Emoji to PNG",
|
||||
description:
|
||||
"Renders an emoji or any Unicode symbol as a transparent PNG of the chosen size.",
|
||||
category: "generate",
|
||||
steps: [{ id: "emoji-to-png" }],
|
||||
},
|
||||
{
|
||||
slug: "color-wheel-png",
|
||||
title: "Color Wheel PNG",
|
||||
description:
|
||||
"Generates an HSL color wheel: hue around the circle, saturation from center to edge, chosen lightness.",
|
||||
category: "generate",
|
||||
steps: [{ id: "color-wheel-png" }],
|
||||
},
|
||||
{
|
||||
slug: "complementary-png",
|
||||
title: "Complementary Palette PNG",
|
||||
description:
|
||||
"Two opposite colors on the color wheel — the base and its complement.",
|
||||
category: "generate",
|
||||
steps: [{ id: "complementary-png" }],
|
||||
},
|
||||
{
|
||||
slug: "triadic-png",
|
||||
title: "Triadic Palette PNG",
|
||||
description: "Three colors evenly spaced 120° apart on the color wheel.",
|
||||
category: "generate",
|
||||
steps: [{ id: "triadic-png" }],
|
||||
},
|
||||
{
|
||||
slug: "tetradic-png",
|
||||
title: "Tetradic Palette PNG",
|
||||
description:
|
||||
"Four colors in two complementary pairs, 90° apart on the wheel.",
|
||||
category: "generate",
|
||||
steps: [{ id: "tetradic-png" }],
|
||||
},
|
||||
{
|
||||
slug: "analogous-png",
|
||||
title: "Analogous Palette PNG",
|
||||
description:
|
||||
"Neighboring hues around the base color — calm, related color scheme.",
|
||||
category: "generate",
|
||||
steps: [{ id: "analogous-png" }],
|
||||
},
|
||||
{
|
||||
slug: "monochromatic-png",
|
||||
title: "Monochromatic Palette PNG",
|
||||
description:
|
||||
"Tones of a single hue: lightness varies within the chosen range, hue and saturation stay fixed.",
|
||||
category: "generate",
|
||||
steps: [{ id: "monochromatic-png" }],
|
||||
},
|
||||
{
|
||||
slug: "shades-png",
|
||||
title: "Shade Ramp PNG",
|
||||
description: "A ramp of the base color getting darker step by step.",
|
||||
category: "generate",
|
||||
steps: [{ id: "shades-png" }],
|
||||
},
|
||||
{
|
||||
slug: "mix-colors-png",
|
||||
title: "Mix Colors PNG",
|
||||
description:
|
||||
"Averages the selected colors into one swatch. Colors become one uniform fill.",
|
||||
category: "generate",
|
||||
steps: [{ id: "mix-colors-png" }],
|
||||
},
|
||||
{
|
||||
slug: "sort-colors-png",
|
||||
title: "Sort Colors PNG",
|
||||
description:
|
||||
"Renders the chosen colors as swatches sorted by hue, brightness or saturation.",
|
||||
category: "generate",
|
||||
steps: [{ id: "sort-colors-png" }],
|
||||
},
|
||||
{
|
||||
slug: "text-to-png",
|
||||
title: "Text to PNG",
|
||||
description:
|
||||
"Creates a PNG image from text: the canvas is sized to fit the label plus padding.",
|
||||
category: "generate",
|
||||
steps: [{ id: "text-to-png" }],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,152 @@
|
||||
import type { Page } from "../types";
|
||||
|
||||
export const geometryPages: Page[] = [
|
||||
{
|
||||
slug: "add-border-png",
|
||||
title: "Add border to PNG",
|
||||
description:
|
||||
"Draws a colored frame of the chosen thickness around the image.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "add-border-png" }],
|
||||
},
|
||||
{
|
||||
slug: "fit-on-background-png",
|
||||
title: "Fit PNG onto background",
|
||||
description:
|
||||
"Places the image centered on a canvas of the given size with a transparent or colored background.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "fit-on-background-png" }],
|
||||
},
|
||||
{
|
||||
slug: "change-canvas-size-png",
|
||||
title: "Change Canvas Size PNG",
|
||||
description:
|
||||
"Sets the exact canvas size: overflow is cropped, missing space is filled with transparency. Anchor picks which part of the image stays.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "change-canvas-size-png" }],
|
||||
},
|
||||
{
|
||||
slug: "resize-png",
|
||||
title: "Resize PNG",
|
||||
description:
|
||||
"Scales the image with bilinear interpolation. With aspect kept, one side defines the scale; if both are set, the image fits inside them.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "resize-png" }],
|
||||
},
|
||||
{
|
||||
slug: "crop-png",
|
||||
title: "Crop PNG",
|
||||
description:
|
||||
"Cuts out a rectangular area. Coordinates and sizes may go beyond the image — the area is clipped to the intersection.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "crop-png" }],
|
||||
},
|
||||
{
|
||||
slug: "rotate-png",
|
||||
title: "Rotate PNG",
|
||||
description: "Rotates by 90°, 180° or 270° clockwise without quality loss.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "rotate-png" }],
|
||||
},
|
||||
{
|
||||
slug: "flip-png",
|
||||
title: "Flip PNG",
|
||||
description: "Mirrors horizontally or vertically without quality loss.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "flip-png" }],
|
||||
},
|
||||
{
|
||||
slug: "add-padding-png",
|
||||
title: "Add padding to PNG",
|
||||
description:
|
||||
"Expands the canvas on all sides by the chosen number of pixels.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "add-padding-png" }],
|
||||
},
|
||||
{
|
||||
slug: "tile-png",
|
||||
title: "Tile PNG",
|
||||
description: "Repeats the image in a grid of the chosen columns and rows.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "tile-png" }],
|
||||
},
|
||||
{
|
||||
slug: "split-into-parts-png",
|
||||
title: "Split PNG into parts",
|
||||
description:
|
||||
"Divides the image into a grid of equal-sized parts. The canvas is padded with transparency to keep every part the same size.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "split-into-parts-png" }],
|
||||
},
|
||||
{
|
||||
slug: "center-by-alpha-png",
|
||||
title: "Center PNG by content",
|
||||
description:
|
||||
"Finds the opaque part of the image and centers it on the original canvas.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "center-by-alpha-png" }],
|
||||
},
|
||||
{
|
||||
slug: "skew-png",
|
||||
title: "Skew PNG",
|
||||
description:
|
||||
"Shifts content horizontally and vertically — a perspective effect.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "skew-png" }],
|
||||
},
|
||||
{
|
||||
slug: "rotate-free-png",
|
||||
title: "Rotate by custom angle",
|
||||
description:
|
||||
"Rotation by any angle. The canvas grows to fit the new bounds; corners stay transparent.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "rotate-free-png" }],
|
||||
},
|
||||
{
|
||||
slug: "zoom-png",
|
||||
title: "Zoom PNG",
|
||||
description:
|
||||
"Magnifies content toward the center. The canvas keeps its size — edges are cropped.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "zoom-png" }],
|
||||
},
|
||||
{
|
||||
slug: "trim-empty-space-png",
|
||||
title: "Trim Empty Space PNG",
|
||||
description:
|
||||
"Crops transparent borders around the content. Pixels with alpha above the threshold count as content.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "trim-empty-space-png" }],
|
||||
},
|
||||
{
|
||||
slug: "change-aspect-ratio-png",
|
||||
title: "Change Aspect Ratio PNG",
|
||||
description:
|
||||
"Fits the image into a target aspect ratio: crop the center to fill, or pad with transparency.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "change-aspect-ratio-png" }],
|
||||
},
|
||||
{
|
||||
slug: "swap-orientation-png",
|
||||
title: "Swap Orientation PNG",
|
||||
description:
|
||||
"Rotates the image by 90° when its orientation differs from the target — landscape becomes portrait and back. Square images are untouched.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "swap-orientation-png" }],
|
||||
},
|
||||
{
|
||||
slug: "symmetric-copy-png",
|
||||
title: "Symmetric Copy PNG",
|
||||
description:
|
||||
"Doubles the canvas by mirroring the kept side onto the empty half — instant symmetric pattern.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "symmetric-copy-png" }],
|
||||
},
|
||||
{
|
||||
slug: "shift-png",
|
||||
title: "Shift PNG",
|
||||
description: "Moves content by the given X and Y offset.",
|
||||
category: "geometry",
|
||||
steps: [{ id: "shift-png" }],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Page } from "../types";
|
||||
|
||||
export const textPages: Page[] = [
|
||||
{
|
||||
slug: "add-text-png",
|
||||
title: "Add text to PNG",
|
||||
description:
|
||||
"Draws a text label on the image: font, size, color, bold, position on a 3×3 grid and an optional backing plate.",
|
||||
category: "text",
|
||||
steps: [{ id: "add-text-png" }],
|
||||
},
|
||||
{
|
||||
slug: "date-stamp-png",
|
||||
title: "Date stamp PNG",
|
||||
description:
|
||||
"Stamps the current date and time using a format string (YYYY MM DD hh mm ss tokens). Same styling options as Add text.",
|
||||
category: "text",
|
||||
steps: [{ id: "date-stamp-png" }],
|
||||
},
|
||||
{
|
||||
slug: "watermark-tile-png",
|
||||
title: "Watermark Tile PNG",
|
||||
description:
|
||||
"Covers the image with a repeating diagonal semi-transparent text tile — a protection watermark.",
|
||||
category: "text",
|
||||
steps: [{ id: "watermark-tile-png" }],
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TOOLS } from ".";
|
||||
import { PAGES, TOOLS } from ".";
|
||||
import { PREVIEW_GROUPS } from "../catalog";
|
||||
import type { PixelImage } from "../core/types";
|
||||
import type { OutputMime } from "../core/io";
|
||||
@@ -14,13 +14,13 @@ function asImage(result: ToolResult): PixelImage {
|
||||
}
|
||||
|
||||
describe("registry-new (переведённые инструменты)", () => {
|
||||
it("id уникальны", () => {
|
||||
it("id инструментов уникальны", () => {
|
||||
const ids = TOOLS.map((t) => t.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
});
|
||||
|
||||
it("slug уникальны", () => {
|
||||
const slugs = TOOLS.map((t) => t.slug);
|
||||
it("slug страниц уникальны", () => {
|
||||
const slugs = PAGES.map((page) => page.slug);
|
||||
expect(new Set(slugs).size).toBe(slugs.length);
|
||||
});
|
||||
|
||||
@@ -30,14 +30,43 @@ describe("registry-new (переведённые инструменты)", () =>
|
||||
const shape = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
||||
for (const tool of TOOLS) {
|
||||
expect(shape.test(tool.id), `id: ${tool.id}`).toBe(true);
|
||||
expect(shape.test(tool.slug), `slug: ${tool.slug}`).toBe(true);
|
||||
}
|
||||
for (const page of PAGES) {
|
||||
expect(shape.test(page.slug), `slug: ${page.slug}`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("каждый шаг страницы ссылается на существующий инструмент", () => {
|
||||
const ids = new Set(TOOLS.map((tool) => tool.id));
|
||||
for (const page of PAGES) {
|
||||
expect(page.steps.length, `${page.slug}: шагов`).toBeGreaterThan(0);
|
||||
for (const step of page.steps) {
|
||||
expect(ids.has(step.id), `${page.slug} → ${step.id}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("каждый инструмент используется хотя бы одной страницей", () => {
|
||||
const used = new Set(PAGES.flatMap((page) => page.steps.map((s) => s.id)));
|
||||
const orphans = TOOLS.map((tool) => tool.id).filter((id) => !used.has(id));
|
||||
expect(orphans, `инструменты без страниц:\n${orphans.join("\n")}`).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it("guard: у страницы ровно один шаг", () => {
|
||||
// Модель допускает цепочку, но `SchemaToolView` исполняет первый шаг и
|
||||
// форма параметров одна на страницу. Исполнение цепочек и общая форма
|
||||
// из полей двух инструментов — фаза 11 `docs/roadmap.md`.
|
||||
for (const page of PAGES) {
|
||||
expect(page.steps.length, `${page.slug}: шагов`).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
it("PREVIEW_GROUPS строится без ошибок", () => {
|
||||
expect(PREVIEW_GROUPS.length).toBeGreaterThan(0);
|
||||
expect(PREVIEW_GROUPS.reduce((n, g) => n + g.tools.length, 0)).toBe(
|
||||
TOOLS.length,
|
||||
expect(PREVIEW_GROUPS.reduce((n, g) => n + g.pages.length, 0)).toBe(
|
||||
PAGES.length,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TOOLS } from ".";
|
||||
import { PAGES } from ".";
|
||||
|
||||
const MAP_PATH = fileURLToPath(
|
||||
new URL("../../../../docs/tools-map.md", import.meta.url),
|
||||
@@ -10,7 +10,7 @@ const MAP_PATH = fileURLToPath(
|
||||
const SECTION_START = /^## 1\.\s/;
|
||||
const SECTION_END = /^## 2\.\s/;
|
||||
|
||||
// Все id в реестре содержат дефис, поэтому формат строгий: это отсекает слова
|
||||
// Все slug страниц содержат дефис, поэтому формат строгий: это отсекает слова
|
||||
// описания вроде `median-cut` из «colors (k, median-cut)».
|
||||
const ID_SHAPE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
|
||||
|
||||
@@ -18,9 +18,9 @@ type Mention = { id: string; line: number };
|
||||
|
||||
/**
|
||||
* Буллиты раздела «1. Реализовано» вместе с номером первой строки.
|
||||
* Перенесённые строки приклеиваются к буллету: иначе id, оказавшийся на
|
||||
* следующей строке, молча выпадал бы из разбора и инструмент считался бы
|
||||
* неописанным.
|
||||
* Перенесённые строки приклеиваются к буллету: иначе slug, оказавшийся на
|
||||
* следующей строке, молча выпадал бы из разбора и страница считалась бы
|
||||
* неописанной.
|
||||
*/
|
||||
function implementedBullets(
|
||||
markdown: string,
|
||||
@@ -56,7 +56,7 @@ function implementedBullets(
|
||||
}
|
||||
|
||||
/**
|
||||
* Всё до первого тире `—` — id через `/` или `,`; после тире свободное
|
||||
* Всё до первого тире `—` — slug через `/` или `,`; после тире свободное
|
||||
* описание параметров, оно не разбирается. Формат закреплён в самой карте.
|
||||
*/
|
||||
function mentionedIds(markdown: string): Mention[] {
|
||||
@@ -71,39 +71,39 @@ function mentionedIds(markdown: string): Mention[] {
|
||||
return found;
|
||||
}
|
||||
|
||||
describe("docs/tools-map.md ↔ реестр", () => {
|
||||
describe("docs/tools-map.md ↔ страницы реестра", () => {
|
||||
const markdown = readFileSync(MAP_PATH, "utf8");
|
||||
const mentioned = mentionedIds(markdown);
|
||||
const registryIds = new Set(TOOLS.map((t) => t.id));
|
||||
const registrySlugs = new Set(PAGES.map((page) => page.slug));
|
||||
|
||||
it("разбирает непустой список id (парсер не сломался молча)", () => {
|
||||
it("разбирает непустой список slug (парсер не сломался молча)", () => {
|
||||
expect(implementedBullets(markdown).length).toBeGreaterThan(0);
|
||||
expect(new Set(mentioned.map((m) => m.id)).size).toBeGreaterThan(50);
|
||||
});
|
||||
|
||||
it("в карте нет id, которых нет в реестре", () => {
|
||||
const bogus = mentioned.filter((m) => !registryIds.has(m.id));
|
||||
it("в карте нет slug, которых нет в реестре", () => {
|
||||
const bogus = mentioned.filter((m) => !registrySlugs.has(m.id));
|
||||
expect(
|
||||
bogus,
|
||||
bogus
|
||||
.map(
|
||||
(m) =>
|
||||
`${m.id} (docs/tools-map.md:${m.line}) — такого инструмента нет в web/src/lib/registry/`,
|
||||
`${m.id} (docs/tools-map.md:${m.line}) — такой страницы нет в web/src/lib/registry/pages/`,
|
||||
)
|
||||
.join("\n"),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("каждый инструмент реестра описан в карте", () => {
|
||||
it("каждая страница реестра описана в карте", () => {
|
||||
const mentionedSet = new Set(mentioned.map((m) => m.id));
|
||||
const missing = [...registryIds].filter((id) => !mentionedSet.has(id));
|
||||
const missing = [...registrySlugs].filter((id) => !mentionedSet.has(id));
|
||||
expect(
|
||||
missing,
|
||||
`не описаны в разделе «Реализовано»:\n${missing.join("\n")}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("id не продублированы между буллетами", () => {
|
||||
it("slug не продублированы между буллетами", () => {
|
||||
const seen = new Map<string, number>();
|
||||
const dupes: string[] = [];
|
||||
for (const { id, line } of mentioned) {
|
||||
@@ -114,6 +114,6 @@ describe("docs/tools-map.md ↔ реестр", () => {
|
||||
seen.set(id, line);
|
||||
}
|
||||
}
|
||||
expect(dupes, `повторяющиеся id:\n${dupes.join("\n")}`).toEqual([]);
|
||||
expect(dupes, `повторяющиеся slug:\n${dupes.join("\n")}`).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
removeColorToAlpha,
|
||||
roundCorners,
|
||||
setAlphaChannel,
|
||||
} from "../core/alpha";
|
||||
import { removeBackground } from "../core/background";
|
||||
} from "../../core/alpha";
|
||||
import { removeBackground } from "../../core/background";
|
||||
import {
|
||||
closingImage,
|
||||
contourImage,
|
||||
@@ -15,18 +15,18 @@ import {
|
||||
erodeImage,
|
||||
openingImage,
|
||||
strokeImage,
|
||||
} from "../core/morphology";
|
||||
import { defringe, featherAlpha } from "../core/pixel-fx";
|
||||
} from "../../core/morphology";
|
||||
import { defringe, featherAlpha } from "../../core/pixel-fx";
|
||||
import {
|
||||
boxTest,
|
||||
circleTest,
|
||||
renderShape,
|
||||
starTest,
|
||||
wavyTest,
|
||||
} from "../core/shapes";
|
||||
import type { Offset } from "../registry-schema";
|
||||
import { field, toolSchema } from "../registry-schema";
|
||||
import { imgTool, type ToolEntry } from "./types";
|
||||
} from "../../core/shapes";
|
||||
import type { Offset } from "../../registry-schema";
|
||||
import { field, toolSchema } from "../../registry-schema";
|
||||
import { imgTool, type Tool } from "../types";
|
||||
|
||||
interface AddStrokeParams {
|
||||
color: string;
|
||||
@@ -44,13 +44,8 @@ export const addStrokeSchema = toolSchema<AddStrokeParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const addStroke: ToolEntry<AddStrokeParams> = {
|
||||
const addStroke: Tool<AddStrokeParams> = {
|
||||
id: "add-stroke-png",
|
||||
slug: "add-stroke-png",
|
||||
title: "Outline PNG",
|
||||
description:
|
||||
"Adds a colored ring outline around the opaque content with the chosen thickness.",
|
||||
category: "alpha",
|
||||
schema: addStrokeSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => strokeImage(img, p.thickness, p.color)),
|
||||
@@ -72,13 +67,8 @@ export const findContourSchema = toolSchema<FindContourParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const findContour: ToolEntry<FindContourParams> = {
|
||||
const findContour: Tool<FindContourParams> = {
|
||||
id: "find-contour-png",
|
||||
slug: "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,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => contourImage(img, p.thickness, p.color)),
|
||||
@@ -100,13 +90,8 @@ export const removeColorSchema = toolSchema<RemoveColorParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const removeColor: ToolEntry<RemoveColorParams> = {
|
||||
const removeColor: Tool<RemoveColorParams> = {
|
||||
id: "remove-color-from-png",
|
||||
slug: "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,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => removeColorToAlpha(img, p.targetColor, p.tolerance)),
|
||||
@@ -144,13 +129,8 @@ const circleMaskSchema = toolSchema<CircleMaskParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const circleMask: ToolEntry<CircleMaskParams> = {
|
||||
const circleMask: Tool<CircleMaskParams> = {
|
||||
id: "circle-mask-png",
|
||||
slug: "circle-mask-png",
|
||||
title: "Circle Mask PNG",
|
||||
description:
|
||||
"Cuts the image into a circle. Diameter is set as a share of the smaller side.",
|
||||
category: "alpha",
|
||||
schema: circleMaskSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -207,13 +187,8 @@ const squareMaskSchema = toolSchema<SquareMaskParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const squareMask: ToolEntry<SquareMaskParams> = {
|
||||
const squareMask: Tool<SquareMaskParams> = {
|
||||
id: "square-mask-png",
|
||||
slug: "square-mask-png",
|
||||
title: "Square Mask PNG",
|
||||
description:
|
||||
"Cuts the image into a rectangle with sides as a share of the smaller side.",
|
||||
category: "alpha",
|
||||
schema: squareMaskSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -286,13 +261,8 @@ const starMaskSchema = toolSchema<StarMaskParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const starMask: ToolEntry<StarMaskParams> = {
|
||||
const starMask: Tool<StarMaskParams> = {
|
||||
id: "star-mask-png",
|
||||
slug: "star-mask-png",
|
||||
title: "Star Mask PNG",
|
||||
description:
|
||||
"Cuts the image into an n-pointed star with adjustable inner radius and rotation.",
|
||||
category: "alpha",
|
||||
schema: starMaskSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -365,13 +335,8 @@ const wavyMaskSchema = toolSchema<WavyMaskParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const wavyMask: ToolEntry<WavyMaskParams> = {
|
||||
const wavyMask: Tool<WavyMaskParams> = {
|
||||
id: "wavy-mask-png",
|
||||
slug: "wavy-mask-png",
|
||||
title: "Wavy Mask PNG",
|
||||
description:
|
||||
"Cuts the image into a wavy-edged circle: radius is modulated by a sine with chosen amplitude and frequency.",
|
||||
category: "alpha",
|
||||
schema: wavyMaskSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -388,13 +353,8 @@ interface EmptyParams {}
|
||||
|
||||
export const removeAlphaChannelSchema = toolSchema<EmptyParams>({});
|
||||
|
||||
const removeAlphaChannel: ToolEntry<EmptyParams> = {
|
||||
const removeAlphaChannel: Tool<EmptyParams> = {
|
||||
id: "remove-alpha-channel-png",
|
||||
slug: "remove-alpha-channel-png",
|
||||
title: "Remove alpha channel PNG",
|
||||
description:
|
||||
"Composites the image over a white background and saves without transparency.",
|
||||
category: "alpha",
|
||||
schema: removeAlphaChannelSchema,
|
||||
input: "image",
|
||||
run: imgTool((img) => flattenOntoColor(img, "#ffffff")),
|
||||
@@ -414,12 +374,8 @@ export const setAlphaChannelSchema = toolSchema<SetAlphaChannelParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const setAlphaChannelTool: ToolEntry<SetAlphaChannelParams> = {
|
||||
const setAlphaChannelTool: Tool<SetAlphaChannelParams> = {
|
||||
id: "set-alpha-channel-png",
|
||||
slug: "set-alpha-channel-png",
|
||||
title: "Set alpha channel PNG",
|
||||
description: "Assigns the same opacity to all pixels; colors stay unchanged.",
|
||||
category: "alpha",
|
||||
schema: setAlphaChannelSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => setAlphaChannel(img, p.percent)),
|
||||
@@ -427,12 +383,8 @@ const setAlphaChannelTool: ToolEntry<SetAlphaChannelParams> = {
|
||||
|
||||
export const extractAlphaMaskSchema = toolSchema<EmptyParams>({});
|
||||
|
||||
const extractAlphaMaskTool: ToolEntry<EmptyParams> = {
|
||||
const extractAlphaMaskTool: Tool<EmptyParams> = {
|
||||
id: "extract-alpha-mask-png",
|
||||
slug: "extract-alpha-mask-png",
|
||||
title: "Extract alpha mask PNG",
|
||||
description: "Turns transparency into a black-and-white opaque mask.",
|
||||
category: "alpha",
|
||||
schema: extractAlphaMaskSchema,
|
||||
input: "image",
|
||||
run: imgTool((img) => extractAlphaMask(img)),
|
||||
@@ -452,13 +404,8 @@ export const roundCornersSchema = toolSchema<RoundCornersParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const roundCornersTool: ToolEntry<RoundCornersParams> = {
|
||||
const roundCornersTool: Tool<RoundCornersParams> = {
|
||||
id: "round-corners-png",
|
||||
slug: "round-corners-png",
|
||||
title: "Round corners PNG",
|
||||
description:
|
||||
"Clips corners by a radius set as a percentage of half the smaller side.",
|
||||
category: "alpha",
|
||||
schema: roundCornersSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => roundCorners(img, p.radius)),
|
||||
@@ -466,12 +413,8 @@ const roundCornersTool: ToolEntry<RoundCornersParams> = {
|
||||
|
||||
export const invertAlphaSchema = toolSchema<EmptyParams>({});
|
||||
|
||||
const invertAlphaTool: ToolEntry<EmptyParams> = {
|
||||
const invertAlphaTool: Tool<EmptyParams> = {
|
||||
id: "invert-alpha-png",
|
||||
slug: "invert-alpha-png",
|
||||
title: "Invert alpha PNG",
|
||||
description: "Opaque areas become transparent and vice versa.",
|
||||
category: "alpha",
|
||||
schema: invertAlphaSchema,
|
||||
input: "image",
|
||||
run: imgTool((img) => invertAlpha(img)),
|
||||
@@ -517,13 +460,8 @@ export const removeBackgroundSchema = toolSchema<RemoveBackgroundParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const removeBackgroundTool: ToolEntry<RemoveBackgroundParams> = {
|
||||
const removeBackgroundTool: Tool<RemoveBackgroundParams> = {
|
||||
id: "remove-background-png",
|
||||
slug: "remove-background-png",
|
||||
title: "Remove background PNG (smart)",
|
||||
description:
|
||||
"Removes a solid background: by color with tolerance, outer regions from the edges only, or every matching pixel. Can smooth the boundary.",
|
||||
category: "alpha",
|
||||
schema: removeBackgroundSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -550,12 +488,8 @@ export const makeThickerSchema = toolSchema<MakeThickerParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const makeThickerTool: ToolEntry<MakeThickerParams> = {
|
||||
const makeThickerTool: Tool<MakeThickerParams> = {
|
||||
id: "make-thicker-png",
|
||||
slug: "make-thicker-png",
|
||||
title: "Thicken PNG",
|
||||
description: "Expands opaque areas by the given number of pixels.",
|
||||
category: "alpha",
|
||||
schema: makeThickerSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => dilateImage(img, p.radius)),
|
||||
@@ -575,12 +509,8 @@ export const makeThinnerSchema = toolSchema<MakeThinnerParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const makeThinnerTool: ToolEntry<MakeThinnerParams> = {
|
||||
const makeThinnerTool: Tool<MakeThinnerParams> = {
|
||||
id: "make-thinner-png",
|
||||
slug: "make-thinner-png",
|
||||
title: "Thin PNG",
|
||||
description: "Shrinks opaque areas — thins the strokes of text and details.",
|
||||
category: "alpha",
|
||||
schema: makeThinnerSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => erodeImage(img, p.radius)),
|
||||
@@ -600,13 +530,8 @@ export const featherEdgesSchema = toolSchema<FeatherEdgesParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const featherEdgesTool: ToolEntry<FeatherEdgesParams> = {
|
||||
const featherEdgesTool: Tool<FeatherEdgesParams> = {
|
||||
id: "feather-edges-png",
|
||||
slug: "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",
|
||||
schema: featherEdgesSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => featherAlpha(img, p.radius)),
|
||||
@@ -626,13 +551,8 @@ export const cleanEdgesSchema = toolSchema<CleanEdgesParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const cleanEdgesTool: ToolEntry<CleanEdgesParams> = {
|
||||
const cleanEdgesTool: Tool<CleanEdgesParams> = {
|
||||
id: "clean-edges-png",
|
||||
slug: "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",
|
||||
schema: cleanEdgesSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => defringe(img, p.radius)),
|
||||
@@ -652,13 +572,8 @@ export const hardenAlphaSchema = toolSchema<HardenAlphaParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const hardenAlphaTool: ToolEntry<HardenAlphaParams> = {
|
||||
const hardenAlphaTool: Tool<HardenAlphaParams> = {
|
||||
id: "harden-alpha-png",
|
||||
slug: "harden-alpha-png",
|
||||
title: "Harden edges PNG",
|
||||
description:
|
||||
"Binarizes the alpha channel by threshold: semi-transparent pixels become either fully transparent or fully opaque.",
|
||||
category: "alpha",
|
||||
schema: hardenAlphaSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => hardenAlpha(img, p.threshold)),
|
||||
@@ -678,13 +593,8 @@ export const despeckleAlphaSchema = toolSchema<DespeckleAlphaParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const despeckleAlphaTool: ToolEntry<DespeckleAlphaParams> = {
|
||||
const despeckleAlphaTool: Tool<DespeckleAlphaParams> = {
|
||||
id: "despeckle-alpha-png",
|
||||
slug: "despeckle-alpha-png",
|
||||
title: "Despeckle PNG",
|
||||
description:
|
||||
"Opening: removes lone semi-transparent pixels and small specks.",
|
||||
category: "alpha",
|
||||
schema: despeckleAlphaSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => openingImage(img, p.radius)),
|
||||
@@ -704,18 +614,14 @@ export const closeHolesSchema = toolSchema<CloseHolesParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const closeHolesTool: ToolEntry<CloseHolesParams> = {
|
||||
const closeHolesTool: Tool<CloseHolesParams> = {
|
||||
id: "close-holes-png",
|
||||
slug: "close-holes-png",
|
||||
title: "Close holes PNG",
|
||||
description: "Closing: fills lone transparent dots inside the object.",
|
||||
category: "alpha",
|
||||
schema: closeHolesSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => closingImage(img, p.radius)),
|
||||
};
|
||||
|
||||
export const alphaEntries = [
|
||||
export const alphaTools = [
|
||||
addStroke,
|
||||
findContour,
|
||||
removeColor,
|
||||
@@ -1,15 +1,19 @@
|
||||
import { hasTransparency, isGrayscale, orientationOf } from "../core/analyze";
|
||||
import { encode } from "../core/io";
|
||||
import {
|
||||
hasTransparency,
|
||||
isGrayscale,
|
||||
orientationOf,
|
||||
} from "../../core/analyze";
|
||||
import { encode } from "../../core/io";
|
||||
import {
|
||||
extractByColor,
|
||||
isGrayscaleish,
|
||||
luma01,
|
||||
rarityPredicate,
|
||||
renderPredicateMask,
|
||||
} from "../core/masks";
|
||||
import { base64ToBytes, looksLikePng, stripDataUri } from "../core/textio";
|
||||
import { field, toolSchema } from "../registry-schema";
|
||||
import { imgTool, textGen, type ToolEntry } from "./types";
|
||||
} from "../../core/masks";
|
||||
import { base64ToBytes, looksLikePng, stripDataUri } from "../../core/textio";
|
||||
import { field, toolSchema } from "../../registry-schema";
|
||||
import { imgTool, textGen, type Tool } from "../types";
|
||||
|
||||
interface ExtractColorParams {
|
||||
color: string;
|
||||
@@ -27,13 +31,8 @@ export const extractColorSchema = toolSchema<ExtractColorParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const extractColor: ToolEntry<ExtractColorParams> = {
|
||||
const extractColor: Tool<ExtractColorParams> = {
|
||||
id: "extract-color-from-png",
|
||||
slug: "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,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => extractByColor(img, p.color, p.tolerance)),
|
||||
@@ -88,13 +87,8 @@ export const showTransparentSchema = toolSchema<MaskParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const showTransparent: ToolEntry<MaskParams> = {
|
||||
const showTransparent: Tool<MaskParams> = {
|
||||
id: "show-transparent-png",
|
||||
slug: "show-transparent-png",
|
||||
title: "Show Transparent Areas PNG",
|
||||
description:
|
||||
"Highlights every transparent or semi-transparent pixel with the chosen color so gaps become obvious.",
|
||||
category: "analyze",
|
||||
schema: showTransparentSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => renderMask(img, p, (_r, _g, _b, a) => a < 255)),
|
||||
@@ -115,13 +109,8 @@ export const showGrayscalePixelsSchema = toolSchema<GrayscalePixelsParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const showGrayscalePixels: ToolEntry<GrayscalePixelsParams> = {
|
||||
const showGrayscalePixels: Tool<GrayscalePixelsParams> = {
|
||||
id: "show-grayscale-pixels-png",
|
||||
slug: "show-grayscale-pixels-png",
|
||||
title: "Show Grayscale Pixels PNG",
|
||||
description:
|
||||
"Finds pixels whose channels are nearly equal and renders them as a mask. Tolerance is in channel units.",
|
||||
category: "analyze",
|
||||
schema: showGrayscalePixelsSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -144,13 +133,8 @@ export const showColorPixelsSchema = toolSchema<ColorPixelsParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const showColorPixels: ToolEntry<ColorPixelsParams> = {
|
||||
const showColorPixels: Tool<ColorPixelsParams> = {
|
||||
id: "show-color-pixels-png",
|
||||
slug: "show-color-pixels-png",
|
||||
title: "Show Color Pixels PNG",
|
||||
description:
|
||||
"Finds colored (non-gray) pixels beyond the channel tolerance and renders them as a mask.",
|
||||
category: "analyze",
|
||||
schema: showColorPixelsSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -173,12 +157,8 @@ export const lightPixelMaskSchema = toolSchema<LightPixelParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const lightPixelMask: ToolEntry<LightPixelParams> = {
|
||||
const lightPixelMask: Tool<LightPixelParams> = {
|
||||
id: "light-pixel-mask-png",
|
||||
slug: "light-pixel-mask-png",
|
||||
title: "Light Pixel Mask PNG",
|
||||
description: "Selects pixels brighter than the luminance threshold.",
|
||||
category: "analyze",
|
||||
schema: lightPixelMaskSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -201,12 +181,8 @@ export const darkPixelMaskSchema = toolSchema<DarkPixelParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const darkPixelMask: ToolEntry<DarkPixelParams> = {
|
||||
const darkPixelMask: Tool<DarkPixelParams> = {
|
||||
id: "dark-pixel-mask-png",
|
||||
slug: "dark-pixel-mask-png",
|
||||
title: "Dark Pixel Mask PNG",
|
||||
description: "Selects pixels darker than the luminance threshold.",
|
||||
category: "analyze",
|
||||
schema: darkPixelMaskSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -229,13 +205,8 @@ export const uniqueColorMaskSchema = toolSchema<UniqueColorParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const uniqueColorMask: ToolEntry<UniqueColorParams> = {
|
||||
const uniqueColorMask: Tool<UniqueColorParams> = {
|
||||
id: "unique-color-mask-png",
|
||||
slug: "unique-color-mask-png",
|
||||
title: "Unique Color Mask PNG",
|
||||
description:
|
||||
"Selects colors that occur no more than the given number of times — rare and one-off pixels.",
|
||||
category: "analyze",
|
||||
schema: uniqueColorMaskSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => renderMask(img, p, rarityPredicate(img, p.rarity))),
|
||||
@@ -245,13 +216,8 @@ interface NoParams {}
|
||||
|
||||
const emptySchema = toolSchema<NoParams>({});
|
||||
|
||||
const verifyIsPng: ToolEntry<NoParams> = {
|
||||
const verifyIsPng: Tool<NoParams> = {
|
||||
id: "verify-is-png",
|
||||
slug: "verify-is-png",
|
||||
title: "Verify If Image Is a PNG",
|
||||
description:
|
||||
"Checks the signature of pasted base64 / data-uri content and reports whether it is a real PNG.",
|
||||
category: "analyze",
|
||||
schema: emptySchema,
|
||||
input: "text",
|
||||
result: "verdict",
|
||||
@@ -260,24 +226,16 @@ const verifyIsPng: ToolEntry<NoParams> = {
|
||||
),
|
||||
};
|
||||
|
||||
const pngIsGrayscale: ToolEntry<NoParams> = {
|
||||
const pngIsGrayscale: Tool<NoParams> = {
|
||||
id: "png-is-grayscale",
|
||||
slug: "png-is-grayscale",
|
||||
title: "Check: is PNG grayscale?",
|
||||
description: "Reports whether the image consists only of shades of gray.",
|
||||
category: "analyze",
|
||||
schema: emptySchema,
|
||||
input: "image",
|
||||
result: "verdict",
|
||||
run: imgTool((img) => (isGrayscale(img) ? "grayscaleYes" : "grayscaleNo")),
|
||||
};
|
||||
|
||||
const pngFileSize: ToolEntry<NoParams> = {
|
||||
const pngFileSize: Tool<NoParams> = {
|
||||
id: "png-file-size",
|
||||
slug: "png-file-size",
|
||||
title: "PNG File Size",
|
||||
description: "Encodes the image as PNG and reports the resulting file size.",
|
||||
category: "analyze",
|
||||
schema: emptySchema,
|
||||
domOnly: true,
|
||||
input: "image",
|
||||
@@ -290,13 +248,8 @@ const pngFileSize: ToolEntry<NoParams> = {
|
||||
}),
|
||||
};
|
||||
|
||||
const pngIsTransparent: ToolEntry<NoParams> = {
|
||||
const pngIsTransparent: Tool<NoParams> = {
|
||||
id: "png-is-transparent",
|
||||
slug: "png-is-transparent",
|
||||
title: "Check: is PNG transparent?",
|
||||
description:
|
||||
"Reports whether the image contains transparent or semi-transparent pixels.",
|
||||
category: "analyze",
|
||||
schema: emptySchema,
|
||||
input: "image",
|
||||
result: "verdict",
|
||||
@@ -305,12 +258,8 @@ const pngIsTransparent: ToolEntry<NoParams> = {
|
||||
),
|
||||
};
|
||||
|
||||
const pngOrientation: ToolEntry<NoParams> = {
|
||||
const pngOrientation: Tool<NoParams> = {
|
||||
id: "png-orientation",
|
||||
slug: "png-orientation",
|
||||
title: "PNG orientation",
|
||||
description: "Reports whether it is portrait, landscape or square.",
|
||||
category: "analyze",
|
||||
schema: emptySchema,
|
||||
input: "image",
|
||||
result: "verdict",
|
||||
@@ -326,7 +275,7 @@ const pngOrientation: ToolEntry<NoParams> = {
|
||||
}),
|
||||
};
|
||||
|
||||
export const analyzeEntries = [
|
||||
export const analyzeTools = [
|
||||
extractColor,
|
||||
showTransparent,
|
||||
showGrayscalePixels,
|
||||
@@ -16,12 +16,12 @@ import {
|
||||
twoColors,
|
||||
type ChannelSwapPair,
|
||||
type RgbChannel,
|
||||
} from "../core/color";
|
||||
import { renderSpace, SPACES, type SpaceId } from "../core/channels";
|
||||
import { parseHexList } from "../core/palette";
|
||||
import { ditherImage, mapToNearest, quantizeImage } from "../core/quantize";
|
||||
import { field, toolSchema, type ColorPair } from "../registry-schema";
|
||||
import { imgTool, type ToolEntry } from "./types";
|
||||
} from "../../core/color";
|
||||
import { renderSpace, SPACES, type SpaceId } from "../../core/channels";
|
||||
import { parseHexList } from "../../core/palette";
|
||||
import { ditherImage, mapToNearest, quantizeImage } from "../../core/quantize";
|
||||
import { field, toolSchema, type ColorPair } from "../../registry-schema";
|
||||
import { imgTool, type Tool } from "../types";
|
||||
|
||||
interface TwoColorsParams {
|
||||
pair: ColorPair;
|
||||
@@ -43,13 +43,8 @@ export const twoColorsSchema = toolSchema<TwoColorsParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const twoColorsTool: ToolEntry<TwoColorsParams> = {
|
||||
const twoColorsTool: Tool<TwoColorsParams> = {
|
||||
id: "two-colors-png",
|
||||
slug: "two-colors-png",
|
||||
title: "Two colors PNG",
|
||||
description:
|
||||
"Recolors the image into two chosen colors by luminance threshold.",
|
||||
category: "color",
|
||||
schema: twoColorsSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => twoColors(img, p.pair.from, p.pair.to, p.threshold)),
|
||||
@@ -69,13 +64,8 @@ export const gammaSchema = toolSchema<GammaParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const gammaTool: ToolEntry<GammaParams> = {
|
||||
const gammaTool: Tool<GammaParams> = {
|
||||
id: "gamma-png",
|
||||
slug: "gamma-png",
|
||||
title: "Gamma correction PNG",
|
||||
description:
|
||||
"Corrects midtone brightness. <1 darker, >1 lighter, 1 — unchanged.",
|
||||
category: "color",
|
||||
schema: gammaSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => gammaCorrection(img, p.value)),
|
||||
@@ -95,13 +85,8 @@ export const temperatureSchema = toolSchema<TemperatureParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const temperatureTool: ToolEntry<TemperatureParams> = {
|
||||
const temperatureTool: Tool<TemperatureParams> = {
|
||||
id: "temperature-png",
|
||||
slug: "temperature-png",
|
||||
title: "Temperature PNG",
|
||||
description:
|
||||
"Positive values make the image warmer (more orange), negative ones cooler (more blue).",
|
||||
category: "color",
|
||||
schema: temperatureSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => temperature(img, p.percent)),
|
||||
@@ -123,13 +108,8 @@ export const tintSchema = toolSchema<TintParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const tintTool: ToolEntry<TintParams> = {
|
||||
const tintTool: Tool<TintParams> = {
|
||||
id: "tint-png",
|
||||
slug: "tint-png",
|
||||
title: "Tint PNG",
|
||||
description:
|
||||
"Multiplies color channels by the chosen tint with the given strength.",
|
||||
category: "color",
|
||||
schema: tintSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => tint(img, p.color, p.strength)),
|
||||
@@ -149,13 +129,8 @@ export const quantizeSchema = toolSchema<QuantizeParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const quantizeTool: ToolEntry<QuantizeParams> = {
|
||||
const quantizeTool: Tool<QuantizeParams> = {
|
||||
id: "quantize-png",
|
||||
slug: "quantize-png",
|
||||
title: "Quantize PNG",
|
||||
description:
|
||||
"Reduces the image to k colors via median-cut palette. Transparent pixels are preserved.",
|
||||
category: "color",
|
||||
schema: quantizeSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => quantizeImage(img, p.colors).image),
|
||||
@@ -169,13 +144,8 @@ export const customPaletteSchema = toolSchema<CustomPaletteParams>({
|
||||
colors: field.text({ label: "fields.hexList", default: "#000000,#ffffff" }),
|
||||
});
|
||||
|
||||
const customPalette: ToolEntry<CustomPaletteParams> = {
|
||||
const customPalette: Tool<CustomPaletteParams> = {
|
||||
id: "custom-palette-png",
|
||||
slug: "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,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => mapToNearest(img, parseHexList(p.colors))),
|
||||
@@ -204,13 +174,8 @@ export const ditheringSchema = toolSchema<DitheringParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const ditheringTool: ToolEntry<DitheringParams> = {
|
||||
const ditheringTool: Tool<DitheringParams> = {
|
||||
id: "dithering-png",
|
||||
slug: "dithering-png",
|
||||
title: "Dithering PNG",
|
||||
description:
|
||||
"Applies Floyd–Steinberg error diffusion or ordered Bayer dithering while reducing to k colors.",
|
||||
category: "color",
|
||||
schema: ditheringSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => ditherImage(img, p.colors, p.pattern)),
|
||||
@@ -220,13 +185,8 @@ interface EmptyParams {}
|
||||
|
||||
export const grayscaleSchema = toolSchema<EmptyParams>({});
|
||||
|
||||
const grayscaleTool: ToolEntry<EmptyParams> = {
|
||||
const grayscaleTool: Tool<EmptyParams> = {
|
||||
id: "grayscale-png",
|
||||
slug: "grayscale-png",
|
||||
title: "Grayscale PNG",
|
||||
description:
|
||||
"Converts the image to shades of gray using the BT.601 luminance formula. Alpha is preserved.",
|
||||
category: "color",
|
||||
schema: grayscaleSchema,
|
||||
input: "image",
|
||||
run: imgTool((img) => grayscale(img)),
|
||||
@@ -234,12 +194,8 @@ const grayscaleTool: ToolEntry<EmptyParams> = {
|
||||
|
||||
export const invertColorsSchema = toolSchema<EmptyParams>({});
|
||||
|
||||
const invertColorsTool: ToolEntry<EmptyParams> = {
|
||||
const invertColorsTool: Tool<EmptyParams> = {
|
||||
id: "invert-colors-png",
|
||||
slug: "invert-colors-png",
|
||||
title: "Invert colors PNG",
|
||||
description: "Inverts each color channel (255 − value). Alpha is unchanged.",
|
||||
category: "color",
|
||||
schema: invertColorsSchema,
|
||||
input: "image",
|
||||
run: imgTool((img) => invert(img)),
|
||||
@@ -276,13 +232,8 @@ export const brightnessContrastSchema = toolSchema<BrightnessContrastParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const brightnessContrastTool: ToolEntry<BrightnessContrastParams> = {
|
||||
const brightnessContrastTool: Tool<BrightnessContrastParams> = {
|
||||
id: "adjust-brightness-contrast-png",
|
||||
slug: "adjust-brightness-contrast-png",
|
||||
title: "Brightness & contrast PNG",
|
||||
description:
|
||||
"Adjusts brightness and contrast in the range from −100 to +100. Zero means no change.",
|
||||
category: "color",
|
||||
schema: brightnessContrastSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => brightnessContrast(img, p.brightness, p.contrast)),
|
||||
@@ -302,13 +253,8 @@ export const opacitySchema = toolSchema<OpacityParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const opacityTool: ToolEntry<OpacityParams> = {
|
||||
const opacityTool: Tool<OpacityParams> = {
|
||||
id: "change-png-opacity",
|
||||
slug: "change-png-opacity",
|
||||
title: "Change PNG opacity",
|
||||
description:
|
||||
"Multiplies the alpha channel by a percentage: 0% — fully transparent, 100% — unchanged.",
|
||||
category: "color",
|
||||
schema: opacitySchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => setOpacity(img, p.percent)),
|
||||
@@ -316,12 +262,8 @@ const opacityTool: ToolEntry<OpacityParams> = {
|
||||
|
||||
export const sepiaSchema = toolSchema<EmptyParams>({});
|
||||
|
||||
const sepiaTool: ToolEntry<EmptyParams> = {
|
||||
const sepiaTool: Tool<EmptyParams> = {
|
||||
id: "sepia-png",
|
||||
slug: "sepia-png",
|
||||
title: "Sepia effect",
|
||||
description: "Tints the image into the warm brown tones of classic sepia.",
|
||||
category: "color",
|
||||
schema: sepiaSchema,
|
||||
input: "image",
|
||||
run: imgTool((img) => sepia(img)),
|
||||
@@ -341,13 +283,8 @@ export const hueShiftSchema = toolSchema<HueShiftParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const hueShiftTool: ToolEntry<HueShiftParams> = {
|
||||
const hueShiftTool: Tool<HueShiftParams> = {
|
||||
id: "change-png-hue",
|
||||
slug: "change-png-hue",
|
||||
title: "Change hue PNG",
|
||||
description:
|
||||
"Shifts the hue around the circle. Saturation and lightness are preserved.",
|
||||
category: "color",
|
||||
schema: hueShiftSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => changeHue(img, p.degrees)),
|
||||
@@ -369,13 +306,8 @@ export const extractChannelSchema = toolSchema<ExtractChannelParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const extractChannelTool: ToolEntry<ExtractChannelParams> = {
|
||||
const extractChannelTool: Tool<ExtractChannelParams> = {
|
||||
id: "extract-channel-png",
|
||||
slug: "extract-channel-png",
|
||||
title: "Extract channel PNG",
|
||||
description:
|
||||
"Keeps only the chosen channel — red, green or blue — as shades of gray.",
|
||||
category: "color",
|
||||
schema: extractChannelSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => extractChannel(img, p.channel)),
|
||||
@@ -397,13 +329,8 @@ export const swapChannelsSchema = toolSchema<SwapChannelsParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const swapChannelsTool: ToolEntry<SwapChannelsParams> = {
|
||||
const swapChannelsTool: Tool<SwapChannelsParams> = {
|
||||
id: "swap-channels-png",
|
||||
slug: "swap-channels-png",
|
||||
title: "Swap channels PNG",
|
||||
description:
|
||||
"Swaps two color channels — a quick way to get unusual coloring.",
|
||||
category: "color",
|
||||
schema: swapChannelsSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => swapChannels(img, p.pair)),
|
||||
@@ -423,13 +350,8 @@ export const blackAndWhiteSchema = toolSchema<BlackAndWhiteParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const blackAndWhiteTool: ToolEntry<BlackAndWhiteParams> = {
|
||||
const blackAndWhiteTool: Tool<BlackAndWhiteParams> = {
|
||||
id: "black-and-white-png",
|
||||
slug: "black-and-white-png",
|
||||
title: "Black & white threshold PNG",
|
||||
description:
|
||||
"Hard binarization by luminance: every pixel becomes black or white.",
|
||||
category: "color",
|
||||
schema: blackAndWhiteSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => thresholdBlackWhite(img, p.threshold)),
|
||||
@@ -449,12 +371,8 @@ export const posterizeSchema = toolSchema<PosterizeParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const posterizeTool: ToolEntry<PosterizeParams> = {
|
||||
const posterizeTool: Tool<PosterizeParams> = {
|
||||
id: "posterize-png",
|
||||
slug: "posterize-png",
|
||||
title: "Posterize PNG",
|
||||
description: "Reduces the number of levels per channel — a poster effect.",
|
||||
category: "color",
|
||||
schema: posterizeSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => posterize(img, p.levels)),
|
||||
@@ -462,13 +380,8 @@ const posterizeTool: ToolEntry<PosterizeParams> = {
|
||||
|
||||
export const autoContrastSchema = toolSchema<EmptyParams>({});
|
||||
|
||||
const autoContrastTool: ToolEntry<EmptyParams> = {
|
||||
const autoContrastTool: Tool<EmptyParams> = {
|
||||
id: "auto-contrast-png",
|
||||
slug: "auto-contrast-png",
|
||||
title: "Auto contrast PNG",
|
||||
description:
|
||||
"Stretches each channel's range across the full available brightness range.",
|
||||
category: "color",
|
||||
schema: autoContrastSchema,
|
||||
input: "image",
|
||||
run: imgTool((img) => autoContrast(img)),
|
||||
@@ -499,13 +412,8 @@ export const decreaseColorCountSchema = toolSchema<DecreaseColorCountParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const decreaseColorCountTool: ToolEntry<DecreaseColorCountParams> = {
|
||||
const decreaseColorCountTool: Tool<DecreaseColorCountParams> = {
|
||||
id: "decrease-color-count-png",
|
||||
slug: "decrease-color-count-png",
|
||||
title: "Decrease Color Count PNG",
|
||||
description:
|
||||
"Median-cut engine as a quick way to drop to 2–256 colors. Presets marked (extreme/strong/balanced/light) match the classic compression levels.",
|
||||
category: "color",
|
||||
schema: decreaseColorCountSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => quantizeImage(img, Number(p.maxColors)).image),
|
||||
@@ -516,60 +424,15 @@ interface ChannelParams {
|
||||
display: "gray" | "color";
|
||||
}
|
||||
|
||||
type SpaceEntry = {
|
||||
id: SpaceId;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
// Пространства, для которых есть инструмент и страница; тексты страницы живут
|
||||
// в `registry/pages/color.ts`, поэтому здесь только идентификаторы.
|
||||
const CHANNEL_SPACES: SpaceId[] = ["hsl", "hsv", "hsi", "cmyk", "ycbcr", "lab"];
|
||||
|
||||
const CHANNEL_SPACES: SpaceEntry[] = [
|
||||
{
|
||||
id: "hsl",
|
||||
title: "Split PNG into HSL",
|
||||
description:
|
||||
"Decomposes the image into Hue, Saturation and Lightness components.",
|
||||
},
|
||||
{
|
||||
id: "hsv",
|
||||
title: "Split PNG into HSV",
|
||||
description:
|
||||
"Decomposes the image into Hue, Saturation and Value (brightness) components.",
|
||||
},
|
||||
{
|
||||
id: "hsi",
|
||||
title: "Split PNG into HSI",
|
||||
description:
|
||||
"Decomposes the image into Hue, Saturation and Intensity components.",
|
||||
},
|
||||
{
|
||||
id: "cmyk",
|
||||
title: "Convert PNG to CMYK Colors",
|
||||
description:
|
||||
"Decomposes the image into print-style Cyan, Magenta, Yellow and Key (black) components.",
|
||||
},
|
||||
{
|
||||
id: "ycbcr",
|
||||
title: "Convert PNG to YCbCr Colors",
|
||||
description:
|
||||
"Decomposes the image into Luma (Y) and Blue-difference / Red-difference chroma components.",
|
||||
},
|
||||
{
|
||||
id: "lab",
|
||||
title: "Convert PNG to LAB Colors",
|
||||
description:
|
||||
"Decomposes the image into perceptual Lightness and green–magenta / blue–yellow opponents.",
|
||||
},
|
||||
];
|
||||
|
||||
function channelEntries(): ToolEntry<ChannelParams>[] {
|
||||
function channelEntries(): Tool<ChannelParams>[] {
|
||||
return CHANNEL_SPACES.map((space) => {
|
||||
const components = SPACES[space.id].components;
|
||||
const components = SPACES[space].components;
|
||||
return {
|
||||
id: `png-to-${space.id}`,
|
||||
slug: `png-to-${space.id}`,
|
||||
title: space.title,
|
||||
description: space.description,
|
||||
category: "color",
|
||||
id: `png-to-${space}`,
|
||||
schema: toolSchema<ChannelParams>({
|
||||
component: field.select({
|
||||
label: "fields.component",
|
||||
@@ -589,14 +452,12 @@ function channelEntries(): ToolEntry<ChannelParams>[] {
|
||||
}),
|
||||
}),
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
renderSpace(img, space.id, p.component, p.display),
|
||||
),
|
||||
} satisfies ToolEntry<ChannelParams>;
|
||||
run: imgTool((img, p) => renderSpace(img, space, p.component, p.display)),
|
||||
} satisfies Tool<ChannelParams>;
|
||||
});
|
||||
}
|
||||
|
||||
export const colorEntries = [
|
||||
export const colorTools = [
|
||||
twoColorsTool,
|
||||
gammaTool,
|
||||
temperatureTool,
|
||||
@@ -1,6 +1,6 @@
|
||||
import { flattenOntoColor } from "../core/alpha";
|
||||
import { decodeBytes, decodeSvgText, toBase64, toDataUrl } from "../core/io";
|
||||
import { hexToPixels, pixelsToHex } from "../core/text";
|
||||
import { flattenOntoColor } from "../../core/alpha";
|
||||
import { decodeBytes, decodeSvgText, toBase64, toDataUrl } from "../../core/io";
|
||||
import { hexToPixels, pixelsToHex } from "../../core/text";
|
||||
import {
|
||||
base64ToBytes,
|
||||
bytesToImage,
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
imageToRgbValues,
|
||||
rgbValuesToImage,
|
||||
stripDataUri,
|
||||
} from "../core/textio";
|
||||
import { clonePixelImage } from "../core/types";
|
||||
import { field, toolSchema } from "../registry-schema";
|
||||
import { imgTool, textGen, type ToolEntry } from "./types";
|
||||
} from "../../core/textio";
|
||||
import { clonePixelImage } from "../../core/types";
|
||||
import { field, toolSchema } from "../../registry-schema";
|
||||
import { imgTool, textGen, type Tool } from "../types";
|
||||
|
||||
const widthProps = (defaultValue: number) =>
|
||||
field.number({
|
||||
@@ -38,13 +38,8 @@ export const convertToJpgSchema = toolSchema<ConvertToJpgParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const convertToJpg: ToolEntry<ConvertToJpgParams> = {
|
||||
const convertToJpg: Tool<ConvertToJpgParams> = {
|
||||
id: "convert-png-to-jpg",
|
||||
slug: "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,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => flattenOntoColor(img, p.background)),
|
||||
@@ -65,13 +60,8 @@ export const convertToWebpSchema = toolSchema<ConvertToWebpParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const convertToWebp: ToolEntry<ConvertToWebpParams> = {
|
||||
const convertToWebp: Tool<ConvertToWebpParams> = {
|
||||
id: "convert-png-to-webp",
|
||||
slug: "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,
|
||||
input: "image",
|
||||
run: imgTool((img) => clonePixelImage(img)),
|
||||
@@ -82,13 +72,8 @@ interface ConvertToBmpParams {}
|
||||
|
||||
export const convertToBmpSchema = toolSchema<ConvertToBmpParams>({});
|
||||
|
||||
const convertToBmp: ToolEntry<ConvertToBmpParams> = {
|
||||
const convertToBmp: Tool<ConvertToBmpParams> = {
|
||||
id: "png-to-bmp",
|
||||
slug: "png-to-bmp",
|
||||
title: "Convert PNG to BMP",
|
||||
description:
|
||||
"Saves the image as 24-bit BMP without an alpha channel: transparency is replaced with a black background.",
|
||||
category: "convert",
|
||||
schema: convertToBmpSchema,
|
||||
input: "image",
|
||||
run: imgTool((img) => flattenOntoColor(img, "#000000")),
|
||||
@@ -99,13 +84,8 @@ interface NoParams {}
|
||||
|
||||
const emptySchema = toolSchema<NoParams>({});
|
||||
|
||||
const pngToBase64: ToolEntry<NoParams> = {
|
||||
const pngToBase64: Tool<NoParams> = {
|
||||
id: "png-to-base64",
|
||||
slug: "png-to-base64",
|
||||
title: "PNG to Base64",
|
||||
description:
|
||||
"Encodes the image into a base64 string for embedding in code or styles.",
|
||||
category: "convert",
|
||||
schema: emptySchema,
|
||||
input: "image",
|
||||
result: "text",
|
||||
@@ -113,13 +93,8 @@ const pngToBase64: ToolEntry<NoParams> = {
|
||||
run: imgTool((img) => toBase64(img)),
|
||||
};
|
||||
|
||||
const pngToDataUri: ToolEntry<NoParams> = {
|
||||
const pngToDataUri: Tool<NoParams> = {
|
||||
id: "png-to-data-uri",
|
||||
slug: "png-to-data-uri",
|
||||
title: "PNG to Data URI",
|
||||
description:
|
||||
"Builds a full data-uri (data:image/png;base64,…) for embedding in HTML/CSS.",
|
||||
category: "convert",
|
||||
schema: emptySchema,
|
||||
input: "image",
|
||||
result: "text",
|
||||
@@ -127,63 +102,40 @@ const pngToDataUri: ToolEntry<NoParams> = {
|
||||
run: imgTool((img) => toDataUrl(img)),
|
||||
};
|
||||
|
||||
const pngToHex: ToolEntry<NoParams> = {
|
||||
const pngToHex: Tool<NoParams> = {
|
||||
id: "png-to-hex",
|
||||
slug: "png-to-hex",
|
||||
title: "PNG to HEX pixels",
|
||||
description:
|
||||
"Shows all pixels as rrggbbaa hex values — row by row, space separated.",
|
||||
category: "convert",
|
||||
schema: emptySchema,
|
||||
input: "image",
|
||||
result: "text",
|
||||
run: imgTool((img) => pixelsToHex(img)),
|
||||
};
|
||||
|
||||
const pngToBytes: ToolEntry<NoParams> = {
|
||||
const pngToBytes: Tool<NoParams> = {
|
||||
id: "png-to-bytes",
|
||||
slug: "png-to-bytes",
|
||||
title: "PNG to Bytes",
|
||||
description:
|
||||
"Lists every pixel as four decimal bytes (R G B A), one image row per line.",
|
||||
category: "convert",
|
||||
schema: emptySchema,
|
||||
input: "image",
|
||||
result: "text",
|
||||
run: imgTool((img) => imageToByteRows(img)),
|
||||
};
|
||||
|
||||
const pngToRgbValues: ToolEntry<NoParams> = {
|
||||
const pngToRgbValues: Tool<NoParams> = {
|
||||
id: "png-to-rgb-values",
|
||||
slug: "png-to-rgb-values",
|
||||
title: "PNG to RGB Values",
|
||||
description: "Lists every pixel as rgba(r, g, b, a), one image row per line.",
|
||||
category: "convert",
|
||||
schema: emptySchema,
|
||||
input: "image",
|
||||
result: "text",
|
||||
run: imgTool((img) => imageToRgbValues(img)),
|
||||
};
|
||||
|
||||
const base64ToPng: ToolEntry<NoParams> = {
|
||||
const base64ToPng: Tool<NoParams> = {
|
||||
id: "base64-to-png",
|
||||
slug: "base64-to-png",
|
||||
title: "Base64 to PNG",
|
||||
description:
|
||||
"Decodes a base64 string or data-uri back into an image. Paste the string on the left.",
|
||||
category: "convert",
|
||||
schema: emptySchema,
|
||||
input: "text",
|
||||
domOnly: true,
|
||||
run: textGen((text) => decodeBytes(base64ToBytes(stripDataUri(text)))),
|
||||
};
|
||||
|
||||
const dataUriToPng: ToolEntry<NoParams> = {
|
||||
const dataUriToPng: Tool<NoParams> = {
|
||||
id: "data-uri-to-png",
|
||||
slug: "data-uri-to-png",
|
||||
title: "Data URI to PNG",
|
||||
description: "Decodes data:image/…;base64,… back into an image file.",
|
||||
category: "convert",
|
||||
schema: emptySchema,
|
||||
input: "text",
|
||||
domOnly: true,
|
||||
@@ -198,13 +150,8 @@ export const hexToPngSchema = toolSchema<HexToPngParams>({
|
||||
width: widthProps(1),
|
||||
});
|
||||
|
||||
const hexToPng: ToolEntry<HexToPngParams> = {
|
||||
const hexToPng: Tool<HexToPngParams> = {
|
||||
id: "hex-to-png",
|
||||
slug: "hex-to-png",
|
||||
title: "HEX pixels to PNG",
|
||||
description:
|
||||
"Assembles an image from rrggbbaa hex values (space separated). Set the width — the height is computed automatically.",
|
||||
category: "convert",
|
||||
schema: hexToPngSchema,
|
||||
input: "text",
|
||||
run: textGen((text, p) => hexToPixels(text, Math.trunc(p.width))),
|
||||
@@ -218,13 +165,8 @@ export const bytesToPngSchema = toolSchema<BytesToPngParams>({
|
||||
width: widthProps(32),
|
||||
});
|
||||
|
||||
const bytesToPng: ToolEntry<BytesToPngParams> = {
|
||||
const bytesToPng: Tool<BytesToPngParams> = {
|
||||
id: "bytes-to-png",
|
||||
slug: "bytes-to-png",
|
||||
title: "Bytes to PNG",
|
||||
description:
|
||||
"Assembles an image from decimal RGBA byte numbers (any separators). Set the width — height is computed automatically.",
|
||||
category: "convert",
|
||||
schema: bytesToPngSchema,
|
||||
input: "text",
|
||||
run: textGen((text, p) => bytesToImage(text, Math.trunc(p.width))),
|
||||
@@ -238,13 +180,8 @@ export const rgbValuesToPngSchema = toolSchema<RgbValuesToPngParams>({
|
||||
width: widthProps(32),
|
||||
});
|
||||
|
||||
const rgbValuesToPng: ToolEntry<RgbValuesToPngParams> = {
|
||||
const rgbValuesToPng: Tool<RgbValuesToPngParams> = {
|
||||
id: "rgb-values-to-png",
|
||||
slug: "rgb-values-to-png",
|
||||
title: "RGB Values to PNG",
|
||||
description:
|
||||
"Assembles an image from rgba(r, g, b, a) numbers. Set the width — height is computed automatically.",
|
||||
category: "convert",
|
||||
schema: rgbValuesToPngSchema,
|
||||
input: "text",
|
||||
run: textGen((text, p) => rgbValuesToImage(text, Math.trunc(p.width))),
|
||||
@@ -258,20 +195,15 @@ export const svgToPngSchema = toolSchema<SvgToPngParams>({
|
||||
width: widthProps(512),
|
||||
});
|
||||
|
||||
const svgToPng: ToolEntry<SvgToPngParams> = {
|
||||
const svgToPng: Tool<SvgToPngParams> = {
|
||||
id: "svg-to-png",
|
||||
slug: "svg-to-png",
|
||||
title: "SVG to PNG",
|
||||
description:
|
||||
"Decodes SVG markup into a raster image. Paste the SVG code on the left.",
|
||||
category: "convert",
|
||||
schema: svgToPngSchema,
|
||||
input: "text",
|
||||
domOnly: true,
|
||||
run: textGen((text, p) => decodeSvgText(text, Math.trunc(p.width))),
|
||||
};
|
||||
|
||||
export const convertEntries = [
|
||||
export const convertTools = [
|
||||
convertToJpg,
|
||||
convertToWebp,
|
||||
convertToBmp,
|
||||
@@ -1,14 +1,14 @@
|
||||
import { gaussianBlur, sharpen as sharpenImage } from "../core/convolution";
|
||||
import { vignette } from "../core/effects";
|
||||
import { jpegRoundtrip } from "../core/io";
|
||||
import { gaussianBlur, sharpen as sharpenImage } from "../../core/convolution";
|
||||
import { vignette } from "../../core/effects";
|
||||
import { jpegRoundtrip } from "../../core/io";
|
||||
import {
|
||||
addNoise,
|
||||
pixelate,
|
||||
shuffleBlocks,
|
||||
silhouette,
|
||||
} from "../core/pixel-fx";
|
||||
import { field, toolSchema } from "../registry-schema";
|
||||
import { imgTool, type ToolEntry } from "./types";
|
||||
} from "../../core/pixel-fx";
|
||||
import { field, toolSchema } from "../../registry-schema";
|
||||
import { imgTool, type Tool } from "../types";
|
||||
|
||||
interface BlurParams {
|
||||
radius: number;
|
||||
@@ -24,13 +24,8 @@ export const blurSchema = toolSchema<BlurParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const blurTool: ToolEntry<BlurParams> = {
|
||||
const blurTool: Tool<BlurParams> = {
|
||||
id: "blur-png",
|
||||
slug: "blur-png",
|
||||
title: "Blur PNG",
|
||||
description:
|
||||
"Gaussian blur: three passes of separable box blur — fast at any radius. Transparent edges do not darken.",
|
||||
category: "filters",
|
||||
schema: blurSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => gaussianBlur(img, p.radius)),
|
||||
@@ -50,13 +45,8 @@ export const sharpenSchema = toolSchema<SharpenParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const sharpenTool: ToolEntry<SharpenParams> = {
|
||||
const sharpenTool: Tool<SharpenParams> = {
|
||||
id: "sharpen-png",
|
||||
slug: "sharpen-png",
|
||||
title: "Sharpen PNG",
|
||||
description:
|
||||
"Emphasizes edges with a sharpening kernel; strength sets the blend with the original. 0% means no change.",
|
||||
category: "filters",
|
||||
schema: sharpenSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => sharpenImage(img, p.strength)),
|
||||
@@ -78,13 +68,8 @@ export const silhouetteSchema = toolSchema<SilhouetteParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const silhouetteTool: ToolEntry<SilhouetteParams> = {
|
||||
const silhouetteTool: Tool<SilhouetteParams> = {
|
||||
id: "silhouette-png",
|
||||
slug: "silhouette-png",
|
||||
title: "Silhouette PNG",
|
||||
description:
|
||||
"Turns all visible pixels into a single solid color while keeping their transparency — instant silhouette.",
|
||||
category: "filters",
|
||||
schema: silhouetteSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => silhouette(img, p.color, p.threshold * 2.55)),
|
||||
@@ -104,13 +89,8 @@ export const vignetteSchema = toolSchema<VignetteParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const vignetteTool: ToolEntry<VignetteParams> = {
|
||||
const vignetteTool: Tool<VignetteParams> = {
|
||||
id: "vignette-png",
|
||||
slug: "vignette-png",
|
||||
title: "Vignette PNG",
|
||||
description:
|
||||
"Smoothly darkens the edges of the image, leaving the center untouched.",
|
||||
category: "filters",
|
||||
schema: vignetteSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => vignette(img, p.strength)),
|
||||
@@ -130,13 +110,8 @@ export const pixelateSchema = toolSchema<PixelateParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const pixelateTool: ToolEntry<PixelateParams> = {
|
||||
const pixelateTool: Tool<PixelateParams> = {
|
||||
id: "pixelate-png",
|
||||
slug: "pixelate-png",
|
||||
title: "Pixelate PNG",
|
||||
description:
|
||||
"Averages every blockSize×blockSize area into one color — classic mosaic.",
|
||||
category: "filters",
|
||||
schema: pixelateSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => pixelate(img, p.blockSize)),
|
||||
@@ -173,13 +148,8 @@ export const randomizePixelsSchema = toolSchema<RandomizePixelsParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const randomizePixels: ToolEntry<RandomizePixelsParams> = {
|
||||
const randomizePixels: Tool<RandomizePixelsParams> = {
|
||||
id: "randomize-pixels-png",
|
||||
slug: "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,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => shuffleBlocks(img, p.blockSize, p.seed)),
|
||||
@@ -226,13 +196,8 @@ export const addNoiseSchema = toolSchema<AddNoiseParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const addNoiseTool: ToolEntry<AddNoiseParams> = {
|
||||
const addNoiseTool: Tool<AddNoiseParams> = {
|
||||
id: "add-noise-png",
|
||||
slug: "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,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => addNoise(img, p.amount, p.mode, p.seed)),
|
||||
@@ -252,20 +217,15 @@ export const jpegArtifactsSchema = toolSchema<JpegArtifactsParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const jpegArtifacts: ToolEntry<JpegArtifactsParams> = {
|
||||
const jpegArtifacts: Tool<JpegArtifactsParams> = {
|
||||
id: "jpeg-artifacts-png",
|
||||
slug: "jpeg-artifacts-png",
|
||||
title: "JPEG artifacts",
|
||||
description:
|
||||
"Simulates low-quality JPEG re-compression — visible blocks and smeared colors.",
|
||||
category: "filters",
|
||||
schema: jpegArtifactsSchema,
|
||||
input: "image",
|
||||
domOnly: true,
|
||||
run: imgTool((img, p) => jpegRoundtrip(img, p.quality)),
|
||||
};
|
||||
|
||||
export const filtersEntries = [
|
||||
export const filtersTools = [
|
||||
vignetteTool,
|
||||
pixelateTool,
|
||||
randomizePixels,
|
||||
@@ -1,7 +1,11 @@
|
||||
import { renderEmoji, renderTextToImage } from "../core/domText";
|
||||
import { colorSpectrum, drawGrid, randomColorBlocks } from "../core/gen-tools";
|
||||
import { noiseImage, solidImage } from "../core/generate";
|
||||
import { changeCanvasSize } from "../core/geometry";
|
||||
import { renderEmoji, renderTextToImage } from "../../core/domText";
|
||||
import {
|
||||
colorSpectrum,
|
||||
drawGrid,
|
||||
randomColorBlocks,
|
||||
} from "../../core/gen-tools";
|
||||
import { noiseImage, solidImage } from "../../core/generate";
|
||||
import { changeCanvasSize } from "../../core/geometry";
|
||||
import {
|
||||
analogousSet,
|
||||
complementarySet,
|
||||
@@ -16,8 +20,8 @@ import {
|
||||
stepColors,
|
||||
tetradicSet,
|
||||
triadicSet,
|
||||
} from "../core/palette";
|
||||
import type { PixelImage } from "../core/types";
|
||||
} from "../../core/palette";
|
||||
import type { PixelImage } from "../../core/types";
|
||||
import {
|
||||
field,
|
||||
toolSchema,
|
||||
@@ -25,8 +29,8 @@ import {
|
||||
type Dimension,
|
||||
type FontStyle,
|
||||
type Gradient,
|
||||
} from "../registry-schema";
|
||||
import { genTool, type ToolEntry } from "./types";
|
||||
} from "../../registry-schema";
|
||||
import { genTool, type Tool } from "../types";
|
||||
|
||||
function rgba(hex: string): [number, number, number, number] {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
@@ -100,13 +104,8 @@ export const createEmptySchema = toolSchema<CreateEmptyParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const createEmpty: ToolEntry<CreateEmptyParams> = {
|
||||
const createEmpty: Tool<CreateEmptyParams> = {
|
||||
id: "create-empty-png",
|
||||
slug: "create-empty-png",
|
||||
title: "Create empty PNG",
|
||||
description:
|
||||
"Creates a blank canvas of the chosen dimensions, either transparent or filled with a solid color.",
|
||||
category: "generate",
|
||||
schema: createEmptySchema,
|
||||
input: "none",
|
||||
run: genTool((p) => {
|
||||
@@ -135,12 +134,8 @@ export const singleColorSchema = toolSchema<SingleColorParams>({
|
||||
color: field.color({ label: "fields.color", default: "#ff0000" }),
|
||||
});
|
||||
|
||||
const singleColor: ToolEntry<SingleColorParams> = {
|
||||
const singleColor: Tool<SingleColorParams> = {
|
||||
id: "single-color-png",
|
||||
slug: "single-color-png",
|
||||
title: "Create solid color PNG",
|
||||
description: "Generates a rectangle of the given size and color.",
|
||||
category: "generate",
|
||||
schema: singleColorSchema,
|
||||
input: "none",
|
||||
run: genTool((p) => {
|
||||
@@ -172,13 +167,8 @@ export const randomNoiseSchema = toolSchema<RandomNoiseParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const randomNoise: ToolEntry<RandomNoiseParams> = {
|
||||
const randomNoise: Tool<RandomNoiseParams> = {
|
||||
id: "random-noise-png",
|
||||
slug: "random-noise-png",
|
||||
title: "Create random noise PNG",
|
||||
description:
|
||||
"Generates an image with random pixels. The seed fixes the result: one seed — one image.",
|
||||
category: "generate",
|
||||
schema: randomNoiseSchema,
|
||||
input: "none",
|
||||
run: genTool((p) => {
|
||||
@@ -219,13 +209,8 @@ export const linearGradientSchema = toolSchema<LinearGradientParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const linearGradient: ToolEntry<LinearGradientParams> = {
|
||||
const linearGradient: Tool<LinearGradientParams> = {
|
||||
id: "linear-gradient-png",
|
||||
slug: "linear-gradient-png",
|
||||
title: "Create gradient PNG",
|
||||
description:
|
||||
"Generates a smooth transition between two colors along a chosen angle.",
|
||||
category: "generate",
|
||||
schema: linearGradientSchema,
|
||||
input: "none",
|
||||
run: genTool((p) => {
|
||||
@@ -293,13 +278,8 @@ export const colorSpectrumSchema = toolSchema<ColorSpectrumParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const colorSpectrumTool: ToolEntry<ColorSpectrumParams> = {
|
||||
const colorSpectrumTool: Tool<ColorSpectrumParams> = {
|
||||
id: "color-spectrum-png",
|
||||
slug: "color-spectrum-png",
|
||||
title: "Color Spectrum PNG",
|
||||
description:
|
||||
"Full hue rainbow 0–360° along the chosen axis with adjustable saturation and lightness.",
|
||||
category: "generate",
|
||||
schema: colorSpectrumSchema,
|
||||
input: "none",
|
||||
run: genTool((p) => {
|
||||
@@ -349,13 +329,8 @@ export const randomColorsSchema = toolSchema<RandomColorsParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const randomColors: ToolEntry<RandomColorsParams> = {
|
||||
const randomColors: Tool<RandomColorsParams> = {
|
||||
id: "random-colors-png",
|
||||
slug: "random-colors-png",
|
||||
title: "Random Color Blocks PNG",
|
||||
description:
|
||||
"Fills the canvas with random vivid color blocks. Deterministic by seed.",
|
||||
category: "generate",
|
||||
schema: randomColorsSchema,
|
||||
input: "none",
|
||||
run: genTool((p) => {
|
||||
@@ -424,13 +399,8 @@ export const drawGridSchema = toolSchema<DrawGridParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const drawGridTool: ToolEntry<DrawGridParams> = {
|
||||
const drawGridTool: Tool<DrawGridParams> = {
|
||||
id: "draw-grid-png",
|
||||
slug: "draw-grid-png",
|
||||
title: "Draw Grid PNG",
|
||||
description:
|
||||
"Draws a grid with custom columns, rows and line width on a transparent or white background.",
|
||||
category: "generate",
|
||||
schema: drawGridSchema,
|
||||
input: "none",
|
||||
run: genTool((p) => {
|
||||
@@ -486,13 +456,8 @@ export const placeholderSchema = toolSchema<PlaceholderParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const placeholder: ToolEntry<PlaceholderParams> = {
|
||||
const placeholder: Tool<PlaceholderParams> = {
|
||||
id: "placeholder-png",
|
||||
slug: "placeholder-png",
|
||||
title: "Create Placeholder PNG",
|
||||
description:
|
||||
"Generates a placeholder rectangle with its dimensions printed in the center.",
|
||||
category: "generate",
|
||||
schema: placeholderSchema,
|
||||
input: "none",
|
||||
domOnly: true,
|
||||
@@ -537,12 +502,8 @@ export const blendTwoSchema = toolSchema<BlendTwoParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const blendTwo: ToolEntry<BlendTwoParams> = {
|
||||
const blendTwo: Tool<BlendTwoParams> = {
|
||||
id: "blend-two-png",
|
||||
slug: "blend-two-png",
|
||||
title: "Blend Two Colors PNG",
|
||||
description: "A continuous horizontal gradient between two colors.",
|
||||
category: "generate",
|
||||
schema: blendTwoSchema,
|
||||
input: "none",
|
||||
run: genTool((p) => renderBlend(p.pair.from, p.pair.to, p.width)),
|
||||
@@ -599,12 +560,8 @@ export const stepColorsSchema = toolSchema<StepColorsParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const stepColorsTool: ToolEntry<StepColorsParams> = {
|
||||
const stepColorsTool: Tool<StepColorsParams> = {
|
||||
id: "step-colors-png",
|
||||
slug: "step-colors-png",
|
||||
title: "Color Steps PNG",
|
||||
description: "A discrete set of evenly spaced steps between two colors.",
|
||||
category: "generate",
|
||||
schema: stepColorsSchema,
|
||||
input: "none",
|
||||
run: genTool((p) =>
|
||||
@@ -632,13 +589,8 @@ export const emojiToPngSchema = toolSchema<EmojiToPngParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const emojiToPng: ToolEntry<EmojiToPngParams> = {
|
||||
const emojiToPng: Tool<EmojiToPngParams> = {
|
||||
id: "emoji-to-png",
|
||||
slug: "emoji-to-png",
|
||||
title: "Emoji to PNG",
|
||||
description:
|
||||
"Renders an emoji or any Unicode symbol as a transparent PNG of the chosen size.",
|
||||
category: "generate",
|
||||
domOnly: true,
|
||||
schema: emojiToPngSchema,
|
||||
input: "none",
|
||||
@@ -667,13 +619,8 @@ export const colorWheelSchema = toolSchema<ColorWheelParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const colorWheelTool: ToolEntry<ColorWheelParams> = {
|
||||
const colorWheelTool: Tool<ColorWheelParams> = {
|
||||
id: "color-wheel-png",
|
||||
slug: "color-wheel-png",
|
||||
title: "Color Wheel PNG",
|
||||
description:
|
||||
"Generates an HSL color wheel: hue around the circle, saturation from center to edge, chosen lightness.",
|
||||
category: "generate",
|
||||
schema: colorWheelSchema,
|
||||
input: "none",
|
||||
run: genTool((p) => renderWheel(p.size, p.lightness)),
|
||||
@@ -717,13 +664,8 @@ const paletteLayoutGroup = {
|
||||
},
|
||||
};
|
||||
|
||||
const complementaryTool: ToolEntry<PaletteBaseParams> = {
|
||||
const complementaryTool: Tool<PaletteBaseParams> = {
|
||||
id: "complementary-png",
|
||||
slug: "complementary-png",
|
||||
title: "Complementary Palette PNG",
|
||||
description:
|
||||
"Two opposite colors on the color wheel — the base and its complement.",
|
||||
category: "generate",
|
||||
schema: toolSchema<PaletteBaseParams>(
|
||||
{ ...paletteBaseSchema },
|
||||
{ ...paletteLayoutGroup },
|
||||
@@ -734,12 +676,8 @@ const complementaryTool: ToolEntry<PaletteBaseParams> = {
|
||||
),
|
||||
};
|
||||
|
||||
const triadicTool: ToolEntry<PaletteBaseParams> = {
|
||||
const triadicTool: Tool<PaletteBaseParams> = {
|
||||
id: "triadic-png",
|
||||
slug: "triadic-png",
|
||||
title: "Triadic Palette PNG",
|
||||
description: "Three colors evenly spaced 120° apart on the color wheel.",
|
||||
category: "generate",
|
||||
schema: toolSchema<PaletteBaseParams>(
|
||||
{
|
||||
...paletteBaseSchema,
|
||||
@@ -753,13 +691,8 @@ const triadicTool: ToolEntry<PaletteBaseParams> = {
|
||||
),
|
||||
};
|
||||
|
||||
const tetradicTool: ToolEntry<PaletteBaseParams> = {
|
||||
const tetradicTool: Tool<PaletteBaseParams> = {
|
||||
id: "tetradic-png",
|
||||
slug: "tetradic-png",
|
||||
title: "Tetradic Palette PNG",
|
||||
description:
|
||||
"Four colors in two complementary pairs, 90° apart on the wheel.",
|
||||
category: "generate",
|
||||
schema: toolSchema<PaletteBaseParams>(
|
||||
{
|
||||
...paletteBaseSchema,
|
||||
@@ -778,13 +711,8 @@ interface AnalogousParams extends PaletteBaseParams {
|
||||
count: number;
|
||||
}
|
||||
|
||||
const analogousTool: ToolEntry<AnalogousParams> = {
|
||||
const analogousTool: Tool<AnalogousParams> = {
|
||||
id: "analogous-png",
|
||||
slug: "analogous-png",
|
||||
title: "Analogous Palette PNG",
|
||||
description:
|
||||
"Neighboring hues around the base color — calm, related color scheme.",
|
||||
category: "generate",
|
||||
schema: toolSchema<AnalogousParams>(
|
||||
{
|
||||
...paletteBaseSchema,
|
||||
@@ -821,13 +749,8 @@ interface MonochromaticParams extends PaletteBaseParams {
|
||||
range: number;
|
||||
}
|
||||
|
||||
const monochromaticTool: ToolEntry<MonochromaticParams> = {
|
||||
const monochromaticTool: Tool<MonochromaticParams> = {
|
||||
id: "monochromatic-png",
|
||||
slug: "monochromatic-png",
|
||||
title: "Monochromatic Palette PNG",
|
||||
description:
|
||||
"Tones of a single hue: lightness varies within the chosen range, hue and saturation stay fixed.",
|
||||
category: "generate",
|
||||
schema: toolSchema<MonochromaticParams>(
|
||||
{
|
||||
...paletteBaseSchema,
|
||||
@@ -864,12 +787,8 @@ interface ShadesParams extends PaletteBaseParams {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
const shadesTool: ToolEntry<ShadesParams> = {
|
||||
const shadesTool: Tool<ShadesParams> = {
|
||||
id: "shades-png",
|
||||
slug: "shades-png",
|
||||
title: "Shade Ramp PNG",
|
||||
description: "A ramp of the base color getting darker step by step.",
|
||||
category: "generate",
|
||||
schema: toolSchema<ShadesParams>(
|
||||
{
|
||||
...paletteBaseSchema,
|
||||
@@ -926,13 +845,8 @@ export const mixColorsSchema = toolSchema<MixColorsParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const mixColorsTool: ToolEntry<MixColorsParams> = {
|
||||
const mixColorsTool: Tool<MixColorsParams> = {
|
||||
id: "mix-colors-png",
|
||||
slug: "mix-colors-png",
|
||||
title: "Mix Colors PNG",
|
||||
description:
|
||||
"Averages the selected colors into one swatch. Colors become one uniform fill.",
|
||||
category: "generate",
|
||||
schema: mixColorsSchema,
|
||||
input: "none",
|
||||
run: genTool((p) => renderSwatches([mixColors(p.colors)], p.width, "strip")),
|
||||
@@ -997,13 +911,8 @@ export const sortColorsSchema = toolSchema<SortColorsParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const sortColorsTool: ToolEntry<SortColorsParams> = {
|
||||
const sortColorsTool: Tool<SortColorsParams> = {
|
||||
id: "sort-colors-png",
|
||||
slug: "sort-colors-png",
|
||||
title: "Sort Colors PNG",
|
||||
description:
|
||||
"Renders the chosen colors as swatches sorted by hue, brightness or saturation.",
|
||||
category: "generate",
|
||||
schema: sortColorsSchema,
|
||||
input: "none",
|
||||
run: genTool((p) =>
|
||||
@@ -1065,13 +974,8 @@ export const textToPngSchema = toolSchema<TextToPngParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const textToPng: ToolEntry<TextToPngParams> = {
|
||||
const textToPng: Tool<TextToPngParams> = {
|
||||
id: "text-to-png",
|
||||
slug: "text-to-png",
|
||||
title: "Text to PNG",
|
||||
description:
|
||||
"Creates a PNG image from text: the canvas is sized to fit the label plus padding.",
|
||||
category: "generate",
|
||||
domOnly: true,
|
||||
schema: textToPngSchema,
|
||||
input: "none",
|
||||
@@ -1089,7 +993,7 @@ const textToPng: ToolEntry<TextToPngParams> = {
|
||||
),
|
||||
};
|
||||
|
||||
export const generateEntries = [
|
||||
export const generateTools = [
|
||||
createEmpty,
|
||||
singleColor,
|
||||
randomNoise,
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
skewImage,
|
||||
transformImage,
|
||||
zoomImage,
|
||||
} from "../core/affine";
|
||||
import { ToolError } from "../core/errors";
|
||||
} from "../../core/affine";
|
||||
import { ToolError } from "../../core/errors";
|
||||
import {
|
||||
centerByAlpha,
|
||||
changeCanvasSize,
|
||||
@@ -22,14 +22,14 @@ import {
|
||||
trimToContent,
|
||||
type Anchor9,
|
||||
type FlipAxis,
|
||||
} from "../core/geometry";
|
||||
import { field, toolSchema, type Dimension } from "../registry-schema";
|
||||
} from "../../core/geometry";
|
||||
import { field, toolSchema, type Dimension } from "../../registry-schema";
|
||||
import {
|
||||
imgTool,
|
||||
requireSource,
|
||||
type ToolEntry,
|
||||
type Tool,
|
||||
type ToolImageFile,
|
||||
} from "./types";
|
||||
} from "../types";
|
||||
|
||||
interface AddBorderParams {
|
||||
thickness: number;
|
||||
@@ -47,13 +47,8 @@ export const addBorderSchema = toolSchema<AddBorderParams>({
|
||||
color: field.color({ label: "fields.borderColor", default: "#000000" }),
|
||||
});
|
||||
|
||||
const addBorder: ToolEntry<AddBorderParams> = {
|
||||
const addBorder: Tool<AddBorderParams> = {
|
||||
id: "add-border-png",
|
||||
slug: "add-border-png",
|
||||
title: "Add border to PNG",
|
||||
description:
|
||||
"Draws a colored frame of the chosen thickness around the image.",
|
||||
category: "geometry",
|
||||
schema: addBorderSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -99,13 +94,8 @@ export const fitOnBackgroundSchema = toolSchema<FitOnBackgroundParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const fitOnBackground: ToolEntry<FitOnBackgroundParams> = {
|
||||
const fitOnBackground: Tool<FitOnBackgroundParams> = {
|
||||
id: "fit-on-background-png",
|
||||
slug: "fit-on-background-png",
|
||||
title: "Fit PNG onto background",
|
||||
description:
|
||||
"Places the image centered on a canvas of the given size with a transparent or colored background.",
|
||||
category: "geometry",
|
||||
schema: fitOnBackgroundSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => {
|
||||
@@ -167,13 +157,8 @@ export const changeCanvasSizeSchema = toolSchema<ChangeCanvasSizeParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const changeCanvasSizeTool: ToolEntry<ChangeCanvasSizeParams> = {
|
||||
const changeCanvasSizeTool: Tool<ChangeCanvasSizeParams> = {
|
||||
id: "change-canvas-size-png",
|
||||
slug: "change-canvas-size-png",
|
||||
title: "Change Canvas Size PNG",
|
||||
description:
|
||||
"Sets the exact canvas size: overflow is cropped, missing space is filled with transparency. Anchor picks which part of the image stays.",
|
||||
category: "geometry",
|
||||
schema: changeCanvasSizeSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -213,13 +198,8 @@ export const resizeSchema = toolSchema<ResizeParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const resizeTool: ToolEntry<ResizeParams> = {
|
||||
const resizeTool: Tool<ResizeParams> = {
|
||||
id: "resize-png",
|
||||
slug: "resize-png",
|
||||
title: "Resize PNG",
|
||||
description:
|
||||
"Scales the image with bilinear interpolation. With aspect kept, one side defines the scale; if both are set, the image fits inside them.",
|
||||
category: "geometry",
|
||||
schema: resizeSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => {
|
||||
@@ -285,13 +265,8 @@ export const cropSchema = toolSchema<CropParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const cropTool: ToolEntry<CropParams> = {
|
||||
const cropTool: Tool<CropParams> = {
|
||||
id: "crop-png",
|
||||
slug: "crop-png",
|
||||
title: "Crop PNG",
|
||||
description:
|
||||
"Cuts out a rectangular area. Coordinates and sizes may go beyond the image — the area is clipped to the intersection.",
|
||||
category: "geometry",
|
||||
schema: cropSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => {
|
||||
@@ -320,12 +295,8 @@ export const rotateSchema = toolSchema<RotateParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const rotateTool: ToolEntry<RotateParams> = {
|
||||
const rotateTool: Tool<RotateParams> = {
|
||||
id: "rotate-png",
|
||||
slug: "rotate-png",
|
||||
title: "Rotate PNG",
|
||||
description: "Rotates by 90°, 180° or 270° clockwise without quality loss.",
|
||||
category: "geometry",
|
||||
schema: rotateSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => rotate90(img, Number(p.angle) / 90)),
|
||||
@@ -346,12 +317,8 @@ export const flipSchema = toolSchema<FlipParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const flipTool: ToolEntry<FlipParams> = {
|
||||
const flipTool: Tool<FlipParams> = {
|
||||
id: "flip-png",
|
||||
slug: "flip-png",
|
||||
title: "Flip PNG",
|
||||
description: "Mirrors horizontally or vertically without quality loss.",
|
||||
category: "geometry",
|
||||
schema: flipSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => flip(img, p.axis)),
|
||||
@@ -385,13 +352,8 @@ export const addPaddingSchema = toolSchema<AddPaddingParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const addPaddingTool: ToolEntry<AddPaddingParams> = {
|
||||
const addPaddingTool: Tool<AddPaddingParams> = {
|
||||
id: "add-padding-png",
|
||||
slug: "add-padding-png",
|
||||
title: "Add padding to PNG",
|
||||
description:
|
||||
"Expands the canvas on all sides by the chosen number of pixels.",
|
||||
category: "geometry",
|
||||
schema: addPaddingSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -428,12 +390,8 @@ export const tileSchema = toolSchema<TileParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const tileTool: ToolEntry<TileParams> = {
|
||||
const tileTool: Tool<TileParams> = {
|
||||
id: "tile-png",
|
||||
slug: "tile-png",
|
||||
title: "Tile PNG",
|
||||
description: "Repeats the image in a grid of the chosen columns and rows.",
|
||||
category: "geometry",
|
||||
schema: tileSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => tile(img, p.columns, p.rows)),
|
||||
@@ -463,13 +421,8 @@ export const splitPartsSchema = toolSchema<SplitPartsParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const splitPartsTool: ToolEntry<SplitPartsParams> = {
|
||||
const splitPartsTool: Tool<SplitPartsParams> = {
|
||||
id: "split-into-parts-png",
|
||||
slug: "split-into-parts-png",
|
||||
title: "Split PNG into parts",
|
||||
description:
|
||||
"Divides the image into a grid of equal-sized parts. The canvas is padded with transparency to keep every part the same size.",
|
||||
category: "geometry",
|
||||
schema: splitPartsSchema,
|
||||
input: "image",
|
||||
result: "files",
|
||||
@@ -501,13 +454,8 @@ interface EmptyParams {}
|
||||
|
||||
export const centerByAlphaSchema = toolSchema<EmptyParams>({});
|
||||
|
||||
const centerByAlphaTool: ToolEntry<EmptyParams> = {
|
||||
const centerByAlphaTool: Tool<EmptyParams> = {
|
||||
id: "center-by-alpha-png",
|
||||
slug: "center-by-alpha-png",
|
||||
title: "Center PNG by content",
|
||||
description:
|
||||
"Finds the opaque part of the image and centers it on the original canvas.",
|
||||
category: "geometry",
|
||||
schema: centerByAlphaSchema,
|
||||
input: "image",
|
||||
run: imgTool((img) => centerByAlpha(img)),
|
||||
@@ -535,13 +483,8 @@ export const skewSchema = toolSchema<SkewParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const skewTool: ToolEntry<SkewParams> = {
|
||||
const skewTool: Tool<SkewParams> = {
|
||||
id: "skew-png",
|
||||
slug: "skew-png",
|
||||
title: "Skew PNG",
|
||||
description:
|
||||
"Shifts content horizontally and vertically — a perspective effect.",
|
||||
category: "geometry",
|
||||
schema: skewSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => skewImage(img, p.degX, p.degY)),
|
||||
@@ -561,13 +504,8 @@ export const rotateFreeSchema = toolSchema<RotateFreeParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const rotateFreeTool: ToolEntry<RotateFreeParams> = {
|
||||
const rotateFreeTool: Tool<RotateFreeParams> = {
|
||||
id: "rotate-free-png",
|
||||
slug: "rotate-free-png",
|
||||
title: "Rotate by custom angle",
|
||||
description:
|
||||
"Rotation by any angle. The canvas grows to fit the new bounds; corners stay transparent.",
|
||||
category: "geometry",
|
||||
schema: rotateFreeSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => rotateFreeImage(img, p.angle)),
|
||||
@@ -587,13 +525,8 @@ export const zoomSchema = toolSchema<ZoomParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const zoomTool: ToolEntry<ZoomParams> = {
|
||||
const zoomTool: Tool<ZoomParams> = {
|
||||
id: "zoom-png",
|
||||
slug: "zoom-png",
|
||||
title: "Zoom PNG",
|
||||
description:
|
||||
"Magnifies content toward the center. The canvas keeps its size — edges are cropped.",
|
||||
category: "geometry",
|
||||
schema: zoomSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => zoomImage(img, p.scale)),
|
||||
@@ -613,13 +546,8 @@ export const trimEmptySpaceSchema = toolSchema<TrimEmptySpaceParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const trimEmptySpaceTool: ToolEntry<TrimEmptySpaceParams> = {
|
||||
const trimEmptySpaceTool: Tool<TrimEmptySpaceParams> = {
|
||||
id: "trim-empty-space-png",
|
||||
slug: "trim-empty-space-png",
|
||||
title: "Trim Empty Space PNG",
|
||||
description:
|
||||
"Crops transparent borders around the content. Pixels with alpha above the threshold count as content.",
|
||||
category: "geometry",
|
||||
schema: trimEmptySpaceSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => trimToContent(img, p.threshold)),
|
||||
@@ -656,13 +584,8 @@ export const changeAspectRatioSchema = toolSchema<ChangeAspectRatioParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const changeAspectRatioTool: ToolEntry<ChangeAspectRatioParams> = {
|
||||
const changeAspectRatioTool: Tool<ChangeAspectRatioParams> = {
|
||||
id: "change-aspect-ratio-png",
|
||||
slug: "change-aspect-ratio-png",
|
||||
title: "Change Aspect Ratio PNG",
|
||||
description:
|
||||
"Fits the image into a target aspect ratio: crop the center to fill, or pad with transparency.",
|
||||
category: "geometry",
|
||||
schema: changeAspectRatioSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => {
|
||||
@@ -687,13 +610,8 @@ export const swapOrientationSchema = toolSchema<SwapOrientationParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const swapOrientationTool: ToolEntry<SwapOrientationParams> = {
|
||||
const swapOrientationTool: Tool<SwapOrientationParams> = {
|
||||
id: "swap-orientation-png",
|
||||
slug: "swap-orientation-png",
|
||||
title: "Swap Orientation PNG",
|
||||
description:
|
||||
"Rotates the image by 90° when its orientation differs from the target — landscape becomes portrait and back. Square images are untouched.",
|
||||
category: "geometry",
|
||||
schema: swapOrientationSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => forceOrientation(img, p.target)),
|
||||
@@ -728,13 +646,8 @@ export const symmetricCopySchema = toolSchema<SymmetricCopyParams>({
|
||||
}),
|
||||
});
|
||||
|
||||
const symmetricCopyTool: ToolEntry<SymmetricCopyParams> = {
|
||||
const symmetricCopyTool: Tool<SymmetricCopyParams> = {
|
||||
id: "symmetric-copy-png",
|
||||
slug: "symmetric-copy-png",
|
||||
title: "Symmetric Copy PNG",
|
||||
description:
|
||||
"Doubles the canvas by mirroring the kept side onto the empty half — instant symmetric pattern.",
|
||||
category: "geometry",
|
||||
schema: symmetricCopySchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) => symmetricCopy(img, p.axis, p.keepSide)),
|
||||
@@ -764,12 +677,8 @@ export const shiftSchema = toolSchema<ShiftParams>({
|
||||
color: field.color({ label: "fields.fillColor", default: "#ffffff" }),
|
||||
});
|
||||
|
||||
const shiftTool: ToolEntry<ShiftParams> = {
|
||||
const shiftTool: Tool<ShiftParams> = {
|
||||
id: "shift-png",
|
||||
slug: "shift-png",
|
||||
title: "Shift PNG",
|
||||
description: "Moves content by the given X and Y offset.",
|
||||
category: "geometry",
|
||||
schema: shiftSchema,
|
||||
input: "image",
|
||||
run: imgTool((img, p) =>
|
||||
@@ -783,7 +692,7 @@ const shiftTool: ToolEntry<ShiftParams> = {
|
||||
),
|
||||
};
|
||||
|
||||
export const geometryEntries = [
|
||||
export const geometryTools = [
|
||||
addBorder,
|
||||
fitOnBackground,
|
||||
changeCanvasSizeTool,
|
||||
@@ -1,9 +1,9 @@
|
||||
import { formatStamp } from "../core/datefmt";
|
||||
import { drawTextBlock, drawTextTile } from "../core/domText";
|
||||
import type { Position9 } from "../core/textdraw";
|
||||
import type { FontStyle, Plate } from "../registry-schema";
|
||||
import { field, toolSchema } from "../registry-schema";
|
||||
import { imgTool, type ToolEntry } from "./types";
|
||||
import { formatStamp } from "../../core/datefmt";
|
||||
import { drawTextBlock, drawTextTile } from "../../core/domText";
|
||||
import type { Position9 } from "../../core/textdraw";
|
||||
import type { FontStyle, Plate } from "../../registry-schema";
|
||||
import { field, toolSchema } from "../../registry-schema";
|
||||
import { imgTool, type Tool } from "../types";
|
||||
|
||||
interface AddTextParams {
|
||||
text: string;
|
||||
@@ -58,13 +58,8 @@ const addTextSchema = toolSchema<AddTextParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const addText: ToolEntry<AddTextParams> = {
|
||||
const addText: Tool<AddTextParams> = {
|
||||
id: "add-text-png",
|
||||
slug: "add-text-png",
|
||||
title: "Add text to PNG",
|
||||
description:
|
||||
"Draws a text label on the image: font, size, color, bold, position on a 3×3 grid and an optional backing plate.",
|
||||
category: "text",
|
||||
domOnly: true,
|
||||
schema: addTextSchema,
|
||||
input: "image",
|
||||
@@ -137,13 +132,8 @@ const dateStampSchema = toolSchema<DateStampParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const dateStamp: ToolEntry<DateStampParams> = {
|
||||
const dateStamp: Tool<DateStampParams> = {
|
||||
id: "date-stamp-png",
|
||||
slug: "date-stamp-png",
|
||||
title: "Date stamp PNG",
|
||||
description:
|
||||
"Stamps the current date and time using a format string (YYYY MM DD hh mm ss tokens). Same styling options as Add text.",
|
||||
category: "text",
|
||||
domOnly: true,
|
||||
schema: dateStampSchema,
|
||||
input: "image",
|
||||
@@ -231,13 +221,8 @@ const watermarkTileSchema = toolSchema<WatermarkTileParams>(
|
||||
},
|
||||
);
|
||||
|
||||
const watermarkTile: ToolEntry<WatermarkTileParams> = {
|
||||
const watermarkTile: Tool<WatermarkTileParams> = {
|
||||
id: "watermark-tile-png",
|
||||
slug: "watermark-tile-png",
|
||||
title: "Watermark Tile PNG",
|
||||
description:
|
||||
"Covers the image with a repeating diagonal semi-transparent text tile — a protection watermark.",
|
||||
category: "text",
|
||||
domOnly: true,
|
||||
schema: watermarkTileSchema,
|
||||
input: "image",
|
||||
@@ -256,4 +241,4 @@ const watermarkTile: ToolEntry<WatermarkTileParams> = {
|
||||
),
|
||||
};
|
||||
|
||||
export const textEntries = [addText, dateStamp, watermarkTile];
|
||||
export const textTools = [addText, dateStamp, watermarkTile];
|
||||
@@ -63,23 +63,18 @@ export interface VerdictResult {
|
||||
export type ToolResult = PixelImage | string | FileResult | VerdictResult;
|
||||
|
||||
/**
|
||||
* Инструмент нового registry: полностью типизирован на `Params`, схема —
|
||||
* обязательна (единый источник дефолтов/валидации/UI). У каждого инструмента
|
||||
* ровно один метод `run(ctx)`; контракт входа декларируется через `input`
|
||||
* (обязательное поле), а результат — через `result`. Отдельных методов
|
||||
* `generate`/`runFromText`/`toText`/`textToText` нет.
|
||||
* Инструмент: уникальная реализация без адреса, текстов и категории — всё это
|
||||
* у страницы. `id` уникален, но обслуживает сколько угодно страниц, поэтому
|
||||
* одного инструмента на две страницы хватает, а страница из нескольких
|
||||
* инструментов — это уже цепочка. Схема обязательна (единый источник
|
||||
* дефолтов/валидации/UI). У каждого инструмента ровно один метод `run(ctx)`;
|
||||
* контракт входа декларируется через `input` (обязательное поле), а результат —
|
||||
* через `result`. Отдельных методов `generate`/`runFromText`/`toText`/
|
||||
* `textToText` нет.
|
||||
*/
|
||||
export type ToolEntry<P = Record<string, unknown>> = {
|
||||
/** Внутреннее имя: ключ i18n `tools.<id>.*`, `toolId` worker-протокола, запись пайплайна. */
|
||||
export type Tool<P = Record<string, unknown>> = {
|
||||
/** Внутреннее имя: ключ словаря инструмента, `toolId` worker-протокола, запись пайплайна. */
|
||||
id: string;
|
||||
/**
|
||||
* Адрес страницы `/tools/<slug>` и база имени скачиваемого файла. Инструмент
|
||||
* работает с любым форматом, поэтому png-интент держит slug, а не id.
|
||||
*/
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: CategoryId;
|
||||
schema: ToolSchema<P>;
|
||||
/** Чем управляется инструмент: входным изображением, текстом или ничем. */
|
||||
input: InputMode;
|
||||
@@ -91,7 +86,28 @@ export type ToolEntry<P = Record<string, unknown>> = {
|
||||
domOnly?: boolean;
|
||||
/** Формат/качество скачивания результата; по умолчанию — PNG. */
|
||||
output?: OutputFormat;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
/** Шаг страницы: инструмент и зафиксированные под него значения параметров. */
|
||||
export type PageStep = {
|
||||
id: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Страница: уникальный адрес, тексты и то, что на ней выполняется. `steps` —
|
||||
* упорядоченный список инструментов, потому что страница может быть готовой
|
||||
* цепочкой; исполняет их `SchemaToolView`, и на сегодня шаг ровно один.
|
||||
*/
|
||||
export type Page = {
|
||||
/** Адрес `/tools/<slug>`, имя скачиваемого файла и ключ иконки. */
|
||||
slug: string;
|
||||
/** EN-строка страницы; перевод лежит в словаре по slug. */
|
||||
title: string;
|
||||
/** EN-строка страницы; перевод лежит в словаре по slug. */
|
||||
description: string;
|
||||
category: CategoryId;
|
||||
steps: PageStep[];
|
||||
};
|
||||
|
||||
/** Гарантирует наличие входного изображения для трансформаторов. */
|
||||
|
||||
Reference in New Issue
Block a user