mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 21:46:35 +00:00
feat: add analyze, generate, text tools and update plan
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import type { ToolEntry } from "./types";
|
||||
import { field, toolSchema } from "../registry-schema";
|
||||
import { extractByColor } from "../core/masks";
|
||||
import {
|
||||
extractByColor,
|
||||
isGrayscaleish,
|
||||
luma01,
|
||||
rarityPredicate,
|
||||
renderPredicateMask,
|
||||
} from "../core/masks";
|
||||
|
||||
interface ExtractColorParams {
|
||||
color: string;
|
||||
@@ -22,4 +28,160 @@ const extractColor: ToolEntry<ExtractColorParams> = {
|
||||
run: (img, p) => extractByColor(img, p.color, p.tolerance),
|
||||
};
|
||||
|
||||
export const analyzeEntries = [extractColor];
|
||||
interface MaskParams {
|
||||
mode: "binary" | "highlight";
|
||||
color: string;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
const maskBaseFields = {
|
||||
mode: field.select({
|
||||
default: "binary",
|
||||
options: [
|
||||
{ value: "binary", label: "Black & white mask" },
|
||||
{ value: "highlight", label: "Color highlight" },
|
||||
],
|
||||
}),
|
||||
color: field.color({ default: "#ff00aa" }),
|
||||
opacity: field.slider({ min: 0, max: 100, step: 5, default: 70 }),
|
||||
};
|
||||
|
||||
function renderMask(
|
||||
img: Parameters<typeof renderPredicateMask>[0],
|
||||
p: MaskParams,
|
||||
predicate: (r: number, g: number, b: number, a: number) => boolean,
|
||||
) {
|
||||
return renderPredicateMask(img, predicate, {
|
||||
mode: p.mode,
|
||||
color: p.color,
|
||||
opacityPercent: p.opacity,
|
||||
});
|
||||
}
|
||||
|
||||
export const showTransparentSchema = toolSchema<MaskParams>({
|
||||
...maskBaseFields,
|
||||
mode: field.select({
|
||||
default: "highlight",
|
||||
options: [
|
||||
{ value: "binary", label: "Black & white mask" },
|
||||
{ value: "highlight", label: "Color highlight" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const showTransparent: ToolEntry<MaskParams> = {
|
||||
id: "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,
|
||||
run: (img, p) => renderMask(img, p, (_r, _g, _b, a) => a < 255),
|
||||
};
|
||||
|
||||
interface GrayscalePixelsParams extends MaskParams {
|
||||
tolerance: number;
|
||||
}
|
||||
|
||||
export const showGrayscalePixelsSchema = toolSchema<GrayscalePixelsParams>({
|
||||
...maskBaseFields,
|
||||
tolerance: field.slider({ min: 0, max: 64, step: 1, default: 0 }),
|
||||
});
|
||||
|
||||
const showGrayscalePixels: ToolEntry<GrayscalePixelsParams> = {
|
||||
id: "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,
|
||||
run: (img, p) =>
|
||||
renderMask(img, p, (r, g, b) => isGrayscaleish(r, g, b, p.tolerance)),
|
||||
};
|
||||
|
||||
interface ColorPixelsParams extends MaskParams {
|
||||
tolerance: number;
|
||||
}
|
||||
|
||||
export const showColorPixelsSchema = toolSchema<ColorPixelsParams>({
|
||||
...maskBaseFields,
|
||||
tolerance: field.slider({ min: 0, max: 64, step: 1, default: 8 }),
|
||||
});
|
||||
|
||||
const showColorPixels: ToolEntry<ColorPixelsParams> = {
|
||||
id: "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,
|
||||
run: (img, p) =>
|
||||
renderMask(img, p, (r, g, b) => !isGrayscaleish(r, g, b, p.tolerance)),
|
||||
};
|
||||
|
||||
interface LightPixelParams extends MaskParams {
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
export const lightPixelMaskSchema = toolSchema<LightPixelParams>({
|
||||
...maskBaseFields,
|
||||
threshold: field.slider({ min: 0, max: 100, step: 1, default: 70 }),
|
||||
});
|
||||
|
||||
const lightPixelMask: ToolEntry<LightPixelParams> = {
|
||||
id: "light-pixel-mask-png",
|
||||
title: "Light Pixel Mask PNG",
|
||||
description: "Selects pixels brighter than the luminance threshold.",
|
||||
category: "analyze",
|
||||
schema: lightPixelMaskSchema,
|
||||
run: (img, p) =>
|
||||
renderMask(img, p, (r, g, b) => luma01(r, g, b) >= p.threshold / 100),
|
||||
};
|
||||
|
||||
interface DarkPixelParams extends MaskParams {
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
export const darkPixelMaskSchema = toolSchema<DarkPixelParams>({
|
||||
...maskBaseFields,
|
||||
threshold: field.slider({ min: 0, max: 100, step: 1, default: 30 }),
|
||||
});
|
||||
|
||||
const darkPixelMask: ToolEntry<DarkPixelParams> = {
|
||||
id: "dark-pixel-mask-png",
|
||||
title: "Dark Pixel Mask PNG",
|
||||
description: "Selects pixels darker than the luminance threshold.",
|
||||
category: "analyze",
|
||||
schema: darkPixelMaskSchema,
|
||||
run: (img, p) =>
|
||||
renderMask(img, p, (r, g, b) => luma01(r, g, b) <= p.threshold / 100),
|
||||
};
|
||||
|
||||
interface UniqueColorParams extends MaskParams {
|
||||
rarity: number;
|
||||
}
|
||||
|
||||
export const uniqueColorMaskSchema = toolSchema<UniqueColorParams>({
|
||||
...maskBaseFields,
|
||||
rarity: field.slider({ min: 1, max: 50, step: 1, default: 1 }),
|
||||
});
|
||||
|
||||
const uniqueColorMask: ToolEntry<UniqueColorParams> = {
|
||||
id: "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,
|
||||
run: (img, p) => renderMask(img, p, rarityPredicate(img, p.rarity)),
|
||||
};
|
||||
|
||||
export const analyzeEntries = [
|
||||
extractColor,
|
||||
showTransparent,
|
||||
showGrayscalePixels,
|
||||
showColorPixels,
|
||||
lightPixelMask,
|
||||
darkPixelMask,
|
||||
uniqueColorMask,
|
||||
];
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { renderTextToImage } from "../core/domText";
|
||||
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,
|
||||
monochromaticSet,
|
||||
renderBlend,
|
||||
renderSwatches,
|
||||
renderWheel,
|
||||
shadeSet,
|
||||
stepColors,
|
||||
tetradicSet,
|
||||
triadicSet,
|
||||
} from "../core/palette";
|
||||
import {
|
||||
field,
|
||||
@@ -439,6 +446,202 @@ const stepColorsTool: ToolEntry<StepColorsParams> = {
|
||||
),
|
||||
};
|
||||
|
||||
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 TextToPngParams {
|
||||
text: string;
|
||||
style: FontStyle;
|
||||
@@ -505,5 +708,13 @@ export const generateEntries = [
|
||||
placeholder,
|
||||
blendTwo,
|
||||
stepColorsTool,
|
||||
emojiToPng,
|
||||
colorWheelTool,
|
||||
complementaryTool,
|
||||
triadicTool,
|
||||
tetradicTool,
|
||||
analogousTool,
|
||||
monochromaticTool,
|
||||
shadesTool,
|
||||
textToPng,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { formatStamp } from "../core/datefmt";
|
||||
import { drawTextBlock } from "../core/domText";
|
||||
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";
|
||||
@@ -122,4 +122,65 @@ const dateStamp: ToolEntry<DateStampParams> = {
|
||||
}),
|
||||
};
|
||||
|
||||
export const textEntries = [addText, dateStamp];
|
||||
interface WatermarkTileParams {
|
||||
text: string;
|
||||
style: FontStyle;
|
||||
opacity: number;
|
||||
angle: number;
|
||||
stepX: number;
|
||||
stepY: number;
|
||||
}
|
||||
|
||||
const watermarkTileSchema = toolSchema<WatermarkTileParams>(
|
||||
{
|
||||
text: field.text({ default: "DRAFT", placeholder: "Watermark text" }),
|
||||
style: field.fontStyle({
|
||||
min: 12,
|
||||
max: 160,
|
||||
size: 56,
|
||||
font: "sans",
|
||||
bold: true,
|
||||
color: "#ffffff",
|
||||
}),
|
||||
opacity: field.slider({ min: 5, max: 100, step: 5, default: 30 }),
|
||||
angle: field.slider({ min: -90, max: 90, step: 1, default: -30 }),
|
||||
stepX: field.slider({ min: 40, max: 600, step: 10, default: 220 }),
|
||||
stepY: field.slider({ min: 40, max: 600, step: 10, default: 180 }),
|
||||
},
|
||||
{
|
||||
layout: {
|
||||
groups: [
|
||||
{ title: "Watermark", fields: ["text", "style", "opacity"] },
|
||||
{
|
||||
title: "Tile",
|
||||
cols: 2,
|
||||
fields: ["angle", "stepX", "stepY"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const watermarkTile: ToolEntry<WatermarkTileParams> = {
|
||||
id: "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,
|
||||
run: (img, p) =>
|
||||
drawTextTile(img, {
|
||||
text: p.text,
|
||||
fontSize: p.style.size,
|
||||
font: p.style.font,
|
||||
bold: p.style.bold,
|
||||
color: p.style.color,
|
||||
opacityPercent: p.opacity,
|
||||
stepX: p.stepX,
|
||||
stepY: p.stepY,
|
||||
angleDeg: p.angle,
|
||||
}),
|
||||
};
|
||||
|
||||
export const textEntries = [addText, dateStamp, watermarkTile];
|
||||
|
||||
Reference in New Issue
Block a user