Files
easy-png-tools/web/src/lib/registry-new/generate.ts
T
2026-09-07 10:16:12 +05:00

816 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 type { PixelImage } from "../core/types";
import {
analogousSet,
complementarySet,
hexToRgb,
mixColors,
monochromaticSet,
renderBlend,
renderSwatches,
renderWheel,
shadeSet,
sortPalette,
stepColors,
tetradicSet,
triadicSet,
} from "../core/palette";
import {
field,
toolSchema,
type ColorPair,
type Dimension,
type FontStyle,
type Gradient,
} from "../registry-schema";
import type { ToolEntry } from "./types";
function rgba(hex: string): [number, number, number, number] {
const { r, g, b } = hexToRgb(hex);
return [r, g, b, 255];
}
/**
* Линейный градиент по произвольному углу. 0° — слева направо, 90° — сверху
* вниз (рост угла по часовой, ось Y вниз). Угол задаёт направление оси
* градиента; t пикселя — нормализованная проекция на эту ось.
*/
function angleGradient(
width: number,
height: number,
fromRgba: [number, number, number, number],
toRgba: [number, number, number, number],
angle: number,
): PixelImage {
const rad = (angle * Math.PI) / 180;
const vx = Math.cos(rad);
const vy = Math.sin(rad);
// Минимум и максимум проекции на ось достигаются в противоположных углах.
const rightX = vx >= 0 ? width - 1 : 0;
const bottomY = vy >= 0 ? height - 1 : 0;
const projMax = rightX * vx + bottomY * vy;
const projMin = (width - 1 - rightX) * vx + (height - 1 - bottomY) * vy;
const span = projMax - projMin;
const out: PixelImage = {
width,
height,
data: new Uint8ClampedArray(width * height * 4),
};
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const t = span === 0 ? 0 : (x * vx + y * vy - projMin) / span;
const i = (y * width + x) * 4;
out.data[i] = fromRgba[0] + (toRgba[0] - fromRgba[0]) * t;
out.data[i + 1] = fromRgba[1] + (toRgba[1] - fromRgba[1]) * t;
out.data[i + 2] = fromRgba[2] + (toRgba[2] - fromRgba[2]) * t;
out.data[i + 3] = fromRgba[3] + (toRgba[3] - fromRgba[3]) * t;
}
}
return out;
}
interface CreateEmptyParams {
size: Dimension;
transparent: boolean;
color: string;
}
export const createEmptySchema = toolSchema<CreateEmptyParams>(
{
size: field.dimension({ min: 1, max: 20000, width: 800, height: 600 }),
transparent: field.checkbox({ default: true }),
color: field.color({ default: "#ffffff" }),
},
{
layout: {
groups: [
{ title: "Canvas", fields: ["size"] },
{ title: "Fill", fields: ["transparent", "color"] },
],
},
},
);
const createEmpty: ToolEntry<CreateEmptyParams> = {
id: "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,
generate: (p) => {
const w = Math.trunc(p.size.width);
const h = Math.trunc(p.size.height);
if (p.transparent) {
return solidImage(w, h, [0, 0, 0, 0]);
}
return solidImage(w, h, rgba(p.color));
},
};
interface SingleColorParams {
size: Dimension;
color: string;
}
export const singleColorSchema = toolSchema<SingleColorParams>({
size: field.dimension({ min: 1, max: 20000, width: 256, height: 256 }),
color: field.color({ default: "#ff0000" }),
});
const singleColor: ToolEntry<SingleColorParams> = {
id: "single-color-png",
title: "Create solid color PNG",
description: "Generates a rectangle of the given size and color.",
category: "generate",
schema: singleColorSchema,
generate: (p) => {
const w = Math.trunc(p.size.width);
const h = Math.trunc(p.size.height);
return solidImage(w, h, rgba(p.color));
},
};
interface RandomNoiseParams {
size: Dimension;
seed: number;
}
export const randomNoiseSchema = toolSchema<RandomNoiseParams>({
size: field.dimension({ min: 1, max: 5000, width: 512, height: 512 }),
seed: field.number({ min: 0, max: 999999999, step: 1, default: 1 }),
});
const randomNoise: ToolEntry<RandomNoiseParams> = {
id: "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,
generate: (p) => {
const w = Math.trunc(p.size.width);
const h = Math.trunc(p.size.height);
return noiseImage(w, h, p.seed);
},
};
interface LinearGradientParams {
size: Dimension;
gradient: Gradient;
}
export const linearGradientSchema = toolSchema<LinearGradientParams>(
{
size: field.dimension({ min: 1, max: 20000, width: 800, height: 600 }),
gradient: field.gradient({ from: "#000000", to: "#ffffff", angle: 0 }),
},
{
layout: {
groups: [
{ title: "Canvas", fields: ["size"] },
{ title: "Colors", fields: ["gradient"] },
],
},
},
);
const linearGradient: ToolEntry<LinearGradientParams> = {
id: "linear-gradient-png",
title: "Create gradient PNG",
description:
"Generates a smooth transition between two colors along a chosen angle.",
category: "generate",
schema: linearGradientSchema,
generate: (p) => {
const w = Math.trunc(p.size.width);
const h = Math.trunc(p.size.height);
return angleGradient(
w,
h,
rgba(p.gradient.from),
rgba(p.gradient.to),
p.gradient.angle,
);
},
};
interface ColorSpectrumParams {
size: Dimension;
direction: "horizontal" | "vertical";
saturation: number;
lightness: number;
}
export const colorSpectrumSchema = toolSchema<ColorSpectrumParams>(
{
size: field.dimension({ min: 1, max: 5000, width: 1024, height: 128 }),
direction: field.select({
default: "horizontal",
options: [
{ value: "horizontal", label: "Horizontal" },
{ value: "vertical", label: "Vertical" },
],
}),
saturation: field.slider({ min: 0, max: 100, step: 1, default: 100 }),
lightness: field.slider({ min: 0, max: 100, step: 1, default: 50 }),
},
{
layout: {
groups: [
{ title: "Canvas", fields: ["size"] },
{ title: "Spectrum", fields: ["direction", "saturation", "lightness"] },
],
},
},
);
const colorSpectrumTool: ToolEntry<ColorSpectrumParams> = {
id: "color-spectrum-png",
title: "Color Spectrum PNG",
description:
"Full hue rainbow 0360° along the chosen axis with adjustable saturation and lightness.",
category: "generate",
schema: colorSpectrumSchema,
generate: (p) => {
const w = Math.trunc(p.size.width);
const h = Math.trunc(p.size.height);
return colorSpectrum(w, h, p.direction, p.saturation, p.lightness);
},
};
interface RandomColorsParams {
size: Dimension;
blockSize: number;
seed: number;
}
export const randomColorsSchema = toolSchema<RandomColorsParams>(
{
size: field.dimension({ min: 1, max: 5000, width: 512, height: 512 }),
blockSize: field.slider({ min: 4, max: 256, step: 2, default: 64 }),
seed: field.number({ min: 0, max: 999999999, step: 1, default: 7 }),
},
{
layout: {
groups: [
{ title: "Canvas", fields: ["size"] },
{ title: "Random", cols: 2, fields: ["blockSize", "seed"] },
],
},
},
);
const randomColors: ToolEntry<RandomColorsParams> = {
id: "random-colors-png",
title: "Random Color Blocks PNG",
description:
"Fills the canvas with random vivid color blocks. Deterministic by seed.",
category: "generate",
schema: randomColorsSchema,
generate: (p) => {
const w = Math.trunc(p.size.width);
const h = Math.trunc(p.size.height);
return randomColorBlocks(w, h, p.blockSize, p.seed);
},
};
interface DrawGridParams {
size: Dimension;
cols: number;
rows: number;
lineWidth: number;
color: string;
transparentBg: boolean;
}
export const drawGridSchema = toolSchema<DrawGridParams>(
{
size: field.dimension({ min: 1, max: 5000, width: 512, height: 512 }),
cols: field.slider({ min: 1, max: 64, step: 1, default: 8 }),
rows: field.slider({ min: 1, max: 64, step: 1, default: 8 }),
lineWidth: field.slider({ min: 1, max: 40, step: 1, default: 2 }),
color: field.color({ default: "#111318" }),
transparentBg: field.checkbox({ default: true }),
},
{
layout: {
groups: [
{ title: "Canvas", fields: ["size"] },
{
title: "Grid",
cols: 2,
fields: ["cols", "rows", "lineWidth", "color", "transparentBg"],
},
],
},
},
);
const drawGridTool: ToolEntry<DrawGridParams> = {
id: "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,
generate: (p) => {
const w = Math.trunc(p.size.width);
const h = Math.trunc(p.size.height);
return drawGrid(
w,
h,
p.cols,
p.rows,
p.lineWidth,
p.color,
p.transparentBg,
);
},
};
interface PlaceholderParams {
size: Dimension;
backgroundColor: string;
color: string;
showText: boolean;
}
export const placeholderSchema = toolSchema<PlaceholderParams>(
{
size: field.dimension({ min: 1, max: 5000, width: 800, height: 400 }),
backgroundColor: field.color({ default: "#dfe2e8" }),
color: field.color({ default: "#5c6470" }),
showText: field.checkbox({ default: true }),
},
{
layout: {
groups: [
{ title: "Canvas", fields: ["size"] },
{ title: "Colors", cols: 2, fields: ["backgroundColor", "color"] },
{ title: "Text", fields: ["showText"] },
],
},
},
);
const placeholder: ToolEntry<PlaceholderParams> = {
id: "placeholder-png",
title: "Create Placeholder PNG",
description:
"Generates a placeholder rectangle with its dimensions printed in the center.",
category: "generate",
schema: placeholderSchema,
generate: (p) => {
const w = Math.trunc(p.size.width);
const h = Math.trunc(p.size.height);
let out = solidImage(w, h, rgba(p.backgroundColor));
if (p.showText) {
const label = renderTextToImage({
text: `${w} × ${h}`,
fontSize: Math.max(12, Math.round(Math.min(w, h) * 0.14)),
font: "sans",
bold: true,
color: p.color,
backgroundColor: p.backgroundColor,
transparentBg: false,
padding: 0,
});
out = changeCanvasSize(label, w, h, "center");
}
return out;
},
};
interface BlendTwoParams {
pair: ColorPair;
width: number;
}
export const blendTwoSchema = toolSchema<BlendTwoParams>({
pair: field.colorPair({ from: "#000000", to: "#ffffff" }),
width: field.slider({ min: 128, max: 1024, step: 16, default: 512 }),
});
const blendTwo: ToolEntry<BlendTwoParams> = {
id: "blend-two-png",
title: "Blend Two Colors PNG",
description: "A continuous horizontal gradient between two colors.",
category: "generate",
schema: blendTwoSchema,
generate: (p) => renderBlend(p.pair.from, p.pair.to, p.width),
};
interface StepColorsParams {
pair: ColorPair;
steps: number;
width: number;
layout: "strip" | "grid";
}
export const stepColorsSchema = toolSchema<StepColorsParams>(
{
pair: field.colorPair({ from: "#000000", to: "#ffffff" }),
steps: field.slider({ min: 2, max: 12, step: 1, default: 6 }),
width: field.slider({ min: 128, max: 1024, step: 16, default: 512 }),
layout: field.select({
default: "grid",
options: [
{ value: "grid", label: "Grid" },
{ value: "strip", label: "Strip" },
],
}),
},
{
layout: {
groups: [
{ title: "Colors", fields: ["pair"] },
{ title: "Output", cols: 2, fields: ["steps", "width", "layout"] },
],
},
},
);
const stepColorsTool: ToolEntry<StepColorsParams> = {
id: "step-colors-png",
title: "Color Steps PNG",
description: "A discrete set of evenly spaced steps between two colors.",
category: "generate",
schema: stepColorsSchema,
generate: (p) =>
renderSwatches(
stepColors(p.pair.from, p.pair.to, p.steps),
p.width,
p.layout,
),
};
interface EmojiToPngParams {
emoji: string;
size: number;
}
export const emojiToPngSchema = toolSchema<EmojiToPngParams>({
emoji: field.text({ default: "😀" }),
size: field.slider({ min: 32, max: 1024, step: 16, default: 256 }),
});
const emojiToPng: ToolEntry<EmojiToPngParams> = {
id: "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,
generate: (p) => renderEmoji(p.emoji, Math.round(p.size)),
};
interface ColorWheelParams {
size: number;
lightness: number;
}
export const colorWheelSchema = toolSchema<ColorWheelParams>({
size: field.slider({ min: 128, max: 1024, step: 16, default: 512 }),
lightness: field.slider({ min: 0, max: 100, step: 1, default: 50 }),
});
const colorWheelTool: ToolEntry<ColorWheelParams> = {
id: "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,
generate: (p) => renderWheel(p.size, p.lightness),
};
interface PaletteBaseParams {
baseColor: string;
width: number;
layout: "strip" | "grid";
}
const paletteBaseSchema = {
baseColor: field.color({ default: "#2563eb" }),
width: field.slider({ min: 128, max: 1024, step: 16, default: 512 }),
layout: field.select({
default: "grid",
options: [
{ value: "grid", label: "Grid" },
{ value: "strip", label: "Strip" },
],
}),
};
const paletteLayoutGroup = {
layout: {
groups: [
{ title: "Base color", fields: ["baseColor"] },
{
title: "Output",
cols: 2,
fields: ["width", "layout"],
},
],
},
};
const complementaryTool: ToolEntry<PaletteBaseParams> = {
id: "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 },
),
generate: (p) =>
renderSwatches(complementarySet(p.baseColor), p.width, p.layout),
};
const triadicTool: ToolEntry<PaletteBaseParams> = {
id: "triadic-png",
title: "Triadic Palette PNG",
description: "Three colors evenly spaced 120° apart on the color wheel.",
category: "generate",
schema: toolSchema<PaletteBaseParams>(
{
...paletteBaseSchema,
baseColor: field.color({ default: "#ff0000" }),
},
{ ...paletteLayoutGroup },
),
generate: (p) => renderSwatches(triadicSet(p.baseColor), p.width, p.layout),
};
const tetradicTool: ToolEntry<PaletteBaseParams> = {
id: "tetradic-png",
title: "Tetradic Palette PNG",
description:
"Four colors in two complementary pairs, 90° apart on the wheel.",
category: "generate",
schema: toolSchema<PaletteBaseParams>(
{
...paletteBaseSchema,
baseColor: field.color({ default: "#8000ff" }),
},
{ ...paletteLayoutGroup },
),
generate: (p) => renderSwatches(tetradicSet(p.baseColor), p.width, p.layout),
};
interface AnalogousParams extends PaletteBaseParams {
spread: number;
count: number;
}
const analogousTool: ToolEntry<AnalogousParams> = {
id: "analogous-png",
title: "Analogous Palette PNG",
description:
"Neighboring hues around the base color — calm, related color scheme.",
category: "generate",
schema: toolSchema<AnalogousParams>(
{
...paletteBaseSchema,
baseColor: field.color({ default: "#22c55e" }),
spread: field.slider({ min: 10, max: 90, step: 5, default: 30 }),
count: field.slider({ min: 3, max: 9, step: 1, default: 5 }),
},
{ ...paletteLayoutGroup },
),
generate: (p) =>
renderSwatches(
analogousSet(p.baseColor, p.spread, p.count),
p.width,
p.layout,
),
};
interface MonochromaticParams extends PaletteBaseParams {
count: number;
range: number;
}
const monochromaticTool: ToolEntry<MonochromaticParams> = {
id: "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,
baseColor: field.color({ default: "#0ea5e9" }),
count: field.slider({ min: 2, max: 9, step: 1, default: 5 }),
range: field.slider({ min: 10, max: 90, step: 5, default: 40 }),
},
{ ...paletteLayoutGroup },
),
generate: (p) =>
renderSwatches(
monochromaticSet(p.baseColor, p.count, p.range),
p.width,
p.layout,
),
};
interface ShadesParams extends PaletteBaseParams {
count: number;
depth: number;
}
const shadesTool: ToolEntry<ShadesParams> = {
id: "shades-png",
title: "Shade Ramp PNG",
description: "A ramp of the base color getting darker step by step.",
category: "generate",
schema: toolSchema<ShadesParams>(
{
...paletteBaseSchema,
baseColor: field.color({ default: "#f59e0b" }),
count: field.slider({ min: 2, max: 9, step: 1, default: 5 }),
depth: field.slider({ min: 10, max: 90, step: 5, default: 50 }),
},
{ ...paletteLayoutGroup },
),
generate: (p) =>
renderSwatches(shadeSet(p.baseColor, p.count, p.depth), p.width, p.layout),
};
interface MixColorsParams {
colors: string[];
width: number;
}
export const mixColorsSchema = toolSchema<MixColorsParams>(
{
colors: field.colors({ default: ["#ff0000", "#00ff00", "#0000ff"] }),
width: field.slider({ min: 128, max: 1024, step: 16, default: 512 }),
},
{
layout: {
groups: [
{ title: "Colors", fields: ["colors"] },
{ title: "Output", fields: ["width"] },
],
},
},
);
const mixColorsTool: ToolEntry<MixColorsParams> = {
id: "mix-colors-png",
title: "Mix Colors PNG",
description:
"Averages the selected colors into one swatch. Colors become one uniform fill.",
category: "generate",
schema: mixColorsSchema,
generate: (p) => renderSwatches([mixColors(p.colors)], p.width, "strip"),
};
interface SortColorsParams {
colors: string[];
order: "hue" | "luma" | "sat";
width: number;
layout: "strip" | "grid";
}
export const sortColorsSchema = toolSchema<SortColorsParams>(
{
colors: field.colors({
default: [
"#ff0000",
"#ff8800",
"#ffff00",
"#00cc44",
"#0066ff",
"#8800ff",
],
}),
order: field.select({
default: "hue",
options: [
{ value: "hue", label: "Hue" },
{ value: "luma", label: "Brightness" },
{ value: "sat", label: "Saturation" },
],
}),
width: field.slider({ min: 128, max: 1024, step: 16, default: 512 }),
layout: field.select({
default: "grid",
options: [
{ value: "grid", label: "Grid" },
{ value: "strip", label: "Strip" },
],
}),
},
{
layout: {
groups: [
{ title: "Colors", fields: ["colors"] },
{
title: "Output",
cols: 2,
fields: ["order", "width", "layout"],
},
],
},
},
);
const sortColorsTool: ToolEntry<SortColorsParams> = {
id: "sort-colors-png",
title: "Sort Colors PNG",
description:
"Renders the chosen colors as swatches sorted by hue, brightness or saturation.",
category: "generate",
schema: sortColorsSchema,
generate: (p) =>
renderSwatches(sortPalette(p.colors, p.order), p.width, p.layout),
};
interface TextToPngParams {
text: string;
style: FontStyle;
transparentBg: boolean;
backgroundColor: string;
padding: number;
}
export const textToPngSchema = toolSchema<TextToPngParams>(
{
text: field.text({ default: "Hello!", placeholder: "Your text" }),
style: field.fontStyle({
min: 8,
max: 300,
size: 96,
font: "sans",
bold: true,
color: "#111318",
}),
transparentBg: field.checkbox({ default: false }),
backgroundColor: field.color({ default: "#ffffff" }),
padding: field.slider({ min: 0, max: 200, step: 2, default: 24 }),
},
{
layout: {
groups: [
{ title: "Text", fields: ["text", "style"] },
{ title: "Background", fields: ["transparentBg", "backgroundColor"] },
{ title: "Padding", fields: ["padding"] },
],
},
},
);
const textToPng: ToolEntry<TextToPngParams> = {
id: "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,
generate: (p) =>
renderTextToImage({
text: p.text,
fontSize: p.style.size,
font: p.style.font,
bold: p.style.bold,
color: p.style.color,
backgroundColor: p.backgroundColor,
transparentBg: p.transparentBg,
padding: p.padding,
}),
};
export const generateEntries = [
createEmpty,
singleColor,
randomNoise,
linearGradient,
colorSpectrumTool,
randomColors,
drawGridTool,
placeholder,
blendTwo,
stepColorsTool,
emojiToPng,
colorWheelTool,
complementaryTool,
triadicTool,
tetradicTool,
analogousTool,
monochromaticTool,
shadesTool,
mixColorsTool,
sortColorsTool,
textToPng,
];