From 6750d5a373c9f10b44fa75c7c407f12e2d8d6680 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Mon, 7 Sep 2026 03:07:46 +0500 Subject: [PATCH] feat: add colors tools --- web/src/lib/registry-new/color.ts | 348 +++++++++++++++++++++++++++++- 1 file changed, 347 insertions(+), 1 deletion(-) diff --git a/web/src/lib/registry-new/color.ts b/web/src/lib/registry-new/color.ts index c584194..e1595a7 100644 --- a/web/src/lib/registry-new/color.ts +++ b/web/src/lib/registry-new/color.ts @@ -1,4 +1,23 @@ -import { gammaCorrection, temperature, twoColors, tint } from "../core/color"; +import { + autoContrast, + brightnessContrast, + changeHue, + extractChannel, + gammaCorrection, + grayscale, + invert, + posterize, + sepia, + setOpacity, + swapChannels, + temperature, + thresholdBlackWhite, + tint, + 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"; @@ -142,6 +161,320 @@ const ditheringTool: ToolEntry = { run: (img, p) => ditherImage(img, p.colors, p.pattern), }; +interface EmptyParams {} + +export const grayscaleSchema = toolSchema({}); + +const grayscaleTool: ToolEntry = { + id: "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, + run: (img) => grayscale(img), +}; + +export const invertColorsSchema = toolSchema({}); + +const invertColorsTool: ToolEntry = { + id: "invert-colors-png", + title: "Invert colors PNG", + description: "Inverts each color channel (255 − value). Alpha is unchanged.", + category: "color", + schema: invertColorsSchema, + run: (img) => invert(img), +}; + +interface BrightnessContrastParams { + brightness: number; + contrast: number; +} + +export const brightnessContrastSchema = toolSchema( + { + brightness: field.slider({ min: -100, max: 100, step: 1, default: 0 }), + contrast: field.slider({ min: -100, max: 100, step: 1, default: 0 }), + }, + { + layout: { + groups: [ + { title: "Adjust", cols: 2, fields: ["brightness", "contrast"] }, + ], + }, + }, +); + +const brightnessContrastTool: ToolEntry = { + id: "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, + run: (img, p) => brightnessContrast(img, p.brightness, p.contrast), +}; + +interface OpacityParams { + percent: number; +} + +export const opacitySchema = toolSchema({ + percent: field.slider({ min: 0, max: 100, step: 1, default: 100 }), +}); + +const opacityTool: ToolEntry = { + id: "change-png-opacity", + title: "Change PNG opacity", + description: + "Multiplies the alpha channel by a percentage: 0% — fully transparent, 100% — unchanged.", + category: "color", + schema: opacitySchema, + run: (img, p) => setOpacity(img, p.percent), +}; + +export const sepiaSchema = toolSchema({}); + +const sepiaTool: ToolEntry = { + id: "sepia-png", + title: "Sepia effect", + description: "Tints the image into the warm brown tones of classic sepia.", + category: "color", + schema: sepiaSchema, + run: (img) => sepia(img), +}; + +interface HueShiftParams { + degrees: number; +} + +export const hueShiftSchema = toolSchema({ + degrees: field.slider({ min: -180, max: 180, step: 1, default: 0 }), +}); + +const hueShiftTool: ToolEntry = { + id: "change-png-hue", + title: "Change hue PNG", + description: + "Shifts the hue around the circle. Saturation and lightness are preserved.", + category: "color", + schema: hueShiftSchema, + run: (img, p) => changeHue(img, p.degrees), +}; + +interface ExtractChannelParams { + channel: RgbChannel; +} + +export const extractChannelSchema = toolSchema({ + channel: field.select({ + default: "red", + options: [ + { value: "red", label: "Red" }, + { value: "green", label: "Green" }, + { value: "blue", label: "Blue" }, + ], + }), +}); + +const extractChannelTool: ToolEntry = { + id: "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, + run: (img, p) => extractChannel(img, p.channel), +}; + +interface SwapChannelsParams { + pair: ChannelSwapPair; +} + +export const swapChannelsSchema = toolSchema({ + pair: field.select({ + default: "r-g", + options: [ + { value: "r-g", label: "Red ↔ Green" }, + { value: "r-b", label: "Red ↔ Blue" }, + { value: "g-b", label: "Green ↔ Blue" }, + ], + }), +}); + +const swapChannelsTool: ToolEntry = { + id: "swap-channels-png", + title: "Swap channels PNG", + description: + "Swaps two color channels — a quick way to get unusual coloring.", + category: "color", + schema: swapChannelsSchema, + run: (img, p) => swapChannels(img, p.pair), +}; + +interface BlackAndWhiteParams { + threshold: number; +} + +export const blackAndWhiteSchema = toolSchema({ + threshold: field.slider({ min: 0, max: 100, step: 1, default: 50 }), +}); + +const blackAndWhiteTool: ToolEntry = { + id: "black-and-white-png", + title: "Black & white threshold PNG", + description: + "Hard binarization by luminance: every pixel becomes black or white.", + category: "color", + schema: blackAndWhiteSchema, + run: (img, p) => thresholdBlackWhite(img, p.threshold), +}; + +interface PosterizeParams { + levels: number; +} + +export const posterizeSchema = toolSchema({ + levels: field.slider({ min: 2, max: 16, step: 1, default: 4 }), +}); + +const posterizeTool: ToolEntry = { + id: "posterize-png", + title: "Posterize PNG", + description: "Reduces the number of levels per channel — a poster effect.", + category: "color", + schema: posterizeSchema, + run: (img, p) => posterize(img, p.levels), +}; + +export const autoContrastSchema = toolSchema({}); + +const autoContrastTool: ToolEntry = { + id: "auto-contrast-png", + title: "Auto contrast PNG", + description: + "Stretches each channel's range across the full available brightness range.", + category: "color", + schema: autoContrastSchema, + run: (img) => autoContrast(img), +}; + +interface DecreaseColorCountParams { + maxColors: "2" | "4" | "8" | "16" | "32" | "64" | "128" | "256"; +} + +export const decreaseColorCountSchema = toolSchema({ + maxColors: field.select({ + default: "16", + options: [ + { value: "2", label: "2" }, + { value: "4", label: "4" }, + { value: "8", label: "8" }, + { value: "16", label: "16" }, + { value: "32", label: "32" }, + { value: "64", label: "64" }, + { value: "128", label: "128" }, + { value: "256", label: "256" }, + ], + }), +}); + +const decreaseColorCountTool: ToolEntry = { + id: "decrease-color-count-png", + title: "Decrease Color Count PNG", + description: + "Same median-cut engine with fixed power-of-two presets — quick way to drop to 2–256 colors.", + category: "color", + schema: decreaseColorCountSchema, + run: (img, p) => quantizeImage(img, Number(p.maxColors)).image, +}; + +interface ChannelParams { + component: string; + display: "gray" | "color"; +} + +type SpaceEntry = { + id: SpaceId; + suffix: string; + title: string; + description: string; +}; + +const CHANNEL_SPACES: SpaceEntry[] = [ + { + id: "hsl", + suffix: "hsl", + title: "Split PNG into HSL", + description: + "Decomposes the image into Hue, Saturation and Lightness components.", + }, + { + id: "hsv", + suffix: "hsv", + title: "Split PNG into HSV", + description: + "Decomposes the image into Hue, Saturation and Value (brightness) components.", + }, + { + id: "hsi", + suffix: "hsi", + title: "Split PNG into HSI", + description: + "Decomposes the image into Hue, Saturation and Intensity components.", + }, + { + id: "cmyk", + suffix: "cmyk", + title: "Convert PNG to CMYK Colors", + description: + "Decomposes the image into print-style Cyan, Magenta, Yellow and Key (black) components.", + }, + { + id: "ycbcr", + suffix: "ycbcr", + title: "Convert PNG to YCbCr Colors", + description: + "Decomposes the image into Luma (Y) and Blue-difference / Red-difference chroma components.", + }, + { + id: "lab", + suffix: "lab", + title: "Convert PNG to LAB Colors", + description: + "Decomposes the image into perceptual Lightness and green–magenta / blue–yellow opponents.", + }, +]; + +function channelEntries(): ToolEntry[] { + return CHANNEL_SPACES.map((space) => { + const components = SPACES[space.id].components; + return { + id: `png-to-${space.suffix}`, + title: space.title, + description: space.description, + category: "color", + schema: toolSchema({ + component: field.select({ + default: components[0], + options: components.map((c) => ({ + value: c, + label: c.toUpperCase(), + })), + }), + display: field.select({ + default: "gray", + options: [ + { value: "gray", label: "Grayscale" }, + { value: "color", label: "Space as RGB" }, + ], + }), + }), + run: (img, p) => renderSpace(img, space.id, p.component, p.display), + } satisfies ToolEntry; + }); +} + export const colorEntries = [ twoColorsTool, gammaTool, @@ -150,4 +483,17 @@ export const colorEntries = [ quantizeTool, customPalette, ditheringTool, + grayscaleTool, + invertColorsTool, + brightnessContrastTool, + opacityTool, + sepiaTool, + hueShiftTool, + extractChannelTool, + swapChannelsTool, + blackAndWhiteTool, + posterizeTool, + autoContrastTool, + decreaseColorCountTool, + ...channelEntries(), ];