feat: add masks core tools

This commit is contained in:
2026-08-26 09:26:26 +05:00
parent 5c37eb2e7b
commit 553601acc1
7 changed files with 467 additions and 21 deletions
+186 -5
View File
@@ -77,6 +77,13 @@ import {
triadicSet,
type SortKey
} from './core/palette';
import {
extractByColor,
isGrayscaleish,
luma01,
rarityPredicate,
renderPredicateMask
} from './core/masks';
import { renderSpace, SPACES, type SpaceId } from './core/channels';
import { hexToPixels, pixelsToHex } from './core/text';
import { clonePixelImage, type PixelImage } from './core/types';
@@ -174,6 +181,167 @@ function bool(params: Record<string, unknown>, id: string): boolean {
return v;
}
type MaskToolSpec = {
id: string;
title: string;
description: string;
defaultMode: 'binary' | 'highlight';
predicate: (
img: PixelImage,
p: Record<string, unknown>
) => (r: number, g: number, b: number, a: number) => boolean;
extraParams?: ToolEntry['params'];
};
const MASK_TOOLS: MaskToolSpec[] = [
{
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.',
defaultMode: 'highlight',
predicate:
(_img, _p) =>
(_r, _g, _b, a) =>
a < 255
},
{
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.',
defaultMode: 'binary',
predicate:
(_i, p) =>
(r, g, b) =>
isGrayscaleish(r, g, b, num(p, 'tolerance')),
extraParams: [
{
id: 'tolerance',
label: 'Channel tolerance',
type: 'slider',
min: 0,
max: 64,
step: 1,
default: 0
}
]
},
{
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.',
defaultMode: 'binary',
predicate:
(_i, p) =>
(r, g, b) =>
!isGrayscaleish(r, g, b, num(p, 'tolerance')),
extraParams: [
{
id: 'tolerance',
label: 'Channel tolerance',
type: 'slider',
min: 0,
max: 64,
step: 1,
default: 8
}
]
},
{
id: 'light-pixel-mask-png',
title: 'Light Pixel Mask PNG',
description: 'Selects pixels brighter than the luminance threshold.',
defaultMode: 'binary',
predicate:
(_i, p) =>
(r, g, b) =>
luma01(r, g, b) >= num(p, 'threshold') / 100,
extraParams: [
{
id: 'threshold',
label: 'Luminance threshold, %',
type: 'slider',
min: 0,
max: 100,
step: 1,
default: 70
}
]
},
{
id: 'dark-pixel-mask-png',
title: 'Dark Pixel Mask PNG',
description: 'Selects pixels darker than the luminance threshold.',
defaultMode: 'binary',
predicate:
(_i, p) =>
(r, g, b) =>
luma01(r, g, b) <= num(p, 'threshold') / 100,
extraParams: [
{
id: 'threshold',
label: 'Luminance threshold, %',
type: 'slider',
min: 0,
max: 100,
step: 1,
default: 30
}
]
},
{
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.',
defaultMode: 'binary',
predicate: (img, p) => rarityPredicate(img, num(p, 'rarity')),
extraParams: [
{ id: 'rarity', label: 'Max occurrences', type: 'slider', min: 1, max: 50, step: 1, default: 1 }
]
}
];
function maskEntries(): ToolEntry[] {
return MASK_TOOLS.map((spec) => ({
id: spec.id,
title: spec.title,
description: spec.description,
category: 'analyze' as const,
params: [
...(spec.extraParams ?? []),
{
id: 'mode',
label: 'Mask mode',
type: 'select' as const,
default: spec.defaultMode,
options: [
{ value: 'binary', label: 'Black & white mask' },
{ value: 'highlight', label: 'Color highlight' }
]
},
{ id: 'color', label: 'Highlight color', type: 'color', default: '#ff00aa' },
{
id: 'opacity',
label: 'Highlight opacity, %',
type: 'slider',
min: 0,
max: 100,
step: 5,
default: 70
}
],
run: (img, p) =>
renderPredicateMask(img, spec.predicate(img, p), {
mode: str(p, 'mode') === 'highlight' ? 'highlight' : 'binary',
color: str(p, 'color'),
opacityPercent: num(p, 'opacity')
})
}));
}
type SpaceEntry = {
id: SpaceId;
suffix: string;
@@ -312,7 +480,8 @@ function hexToRgba(hex: string, alpha = 255): [number, number, number, number] {
}
export const TOOLS: ToolEntry[] = [
...channelEntries(),
...channelEntries(),
...maskEntries(),
decodeToPng(
'jpg-to-png',
'Convert JPG to PNG',
@@ -910,10 +1079,22 @@ export const TOOLS: ToolEntry[] = [
{ id: 'targetColor', label: 'Color to remove', type: 'color', default: '#00ff00' },
{ id: 'tolerance', label: 'Similarity threshold, %', type: 'slider', min: 0, max: 100, step: 1, default: 10 }
],
run: (img, p) => removeColorToAlpha(img, str(p, 'targetColor'), num(p, 'tolerance')),
preview: (img, p) => colorMask(img, str(p, 'targetColor'), num(p, 'tolerance'))
},
{
run: (img, p) => removeColorToAlpha(img, str(p, 'targetColor'), num(p, 'tolerance')),
preview: (img, p) => colorMask(img, str(p, 'targetColor'), num(p, 'tolerance'))
},
{
id: 'extract-color-from-png',
title: 'Extract Color from PNG',
description:
'Keeps only pixels close to the chosen color and makes everything else transparent — the inverse of Remove Color.',
category: 'analyze',
params: [
{ id: 'color', label: 'Color to keep', type: 'color', default: '#00ff88' },
{ id: 'tolerance', label: 'Similarity tolerance, %', type: 'slider', min: 0, max: 50, step: 1, default: 10 }
],
run: (img, p) => extractByColor(img, str(p, 'color'), num(p, 'tolerance'))
},
{
id: 'png-info',
title: 'PNG info',
description: