feat: add analyze, generate, text tools and update plan
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
# План: дожать перевод инструментов в preview и переезд ветки `old/`
|
# План: дожать перевод инструментов в preview и переезд ветки `old/`
|
||||||
|
|
||||||
> Статус: **план.** Основа — `plan-composite-params.md` (Фазы 1–5). Текущее
|
> Статус: **переведено всё простое, осталось 23 спец-случая.** Основа —
|
||||||
> состояние: **40 из 125 инструментов переведены в `registry-new`**, старый UI
|
> `plan-composite-params.md` (Фазы 1–5). Текущее
|
||||||
|
> состояние: **101 инструмент из 125 переведены в `registry-new`** (включая
|
||||||
|
> ранее переведённые 40; из оставшихся 85 перенесены 62 простых), старый UI
|
||||||
> уже живёт на `(old)/`-маршрутах, линтер-изоляция old↔preview на месте
|
> уже живёт на `(old)/`-маршрутах, линтер-изоляция old↔preview на месте
|
||||||
> (плагин `isolation/no-mixed-imports`). Здесь — что сделать, чтобы:
|
> (плагин `isolation/no-mixed-imports`). Здесь — что сделать, чтобы:
|
||||||
>
|
>
|
||||||
@@ -16,13 +18,14 @@
|
|||||||
|
|
||||||
## Текущая картина (факты)
|
## Текущая картина (факты)
|
||||||
|
|
||||||
- `registry.ts` + `registry/` — **125 инструментов**; `registry-new/` — **40**;
|
- `registry.ts` + `registry/` — **125 инструментов**; `registry-new/` — **101**;
|
||||||
остаток — **85**. (Ранний подсчёт «104» был занижен: извлечение суффиксом
|
остаток — **23 спец-случая**. (Ранний подсчёт «104» был занижен: извлечение
|
||||||
`-png` теряло каналы `png-to-hsl/…`, текстовые вердикты `png-info` и т.п. и
|
суффиксом `-png` теряло каналы `png-to-hsl/…`, текстовые вердикты
|
||||||
обратные конвертеры `png-to-base64/…`.)
|
`png-info` и т.п. и обратные конвертеры `png-to-base64/…`.)
|
||||||
- Preview (`/preview`) показывает только переведённые 40; старый UI работает
|
- Preview (`/preview`) показывает переведённые 101; старый UI работает
|
||||||
на `/`-маршрутах (группа `(old)/`): `+page`, `demo`, `list-tools`,
|
на `/`-маршрутах (группа `(old)/`): `+page`, `demo`, `list-tools`,
|
||||||
`tools/[id]`.
|
`tools/[id]`. Все «простые» инструменты переведены (color 18, filters 3,
|
||||||
|
geometry 13, alpha 13, generate 8, analyze-маски 6, text 1).
|
||||||
- Плагин `isolation/no-mixed-imports` (`web/eslint-plugins/isolation/`)
|
- Плагин `isolation/no-mixed-imports` (`web/eslint-plugins/isolation/`)
|
||||||
полностью разделяет ветки: старый `lib/registry.ts|/registry/**`,
|
полностью разделяет ветки: старый `lib/registry.ts|/registry/**`,
|
||||||
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
|
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
|
||||||
@@ -39,48 +42,20 @@
|
|||||||
`ColorField.svelte`, пустой ruleset в `Toggle.svelte`, неиспользуемые токены
|
`ColorField.svelte`, пустой ruleset в `Toggle.svelte`, неиспользуемые токены
|
||||||
в `preview.css`.
|
в `preview.css`.
|
||||||
|
|
||||||
## Скоуп перевода: 85 инструментов осталось
|
## Скоуп перевода: осталось 23 особых случая
|
||||||
|
|
||||||
По категориям (в скобках — сколько из категории уже переведено; подсчёт по
|
Все инструменты без специальных фич переведены (суммарно 62 простых из 85,
|
||||||
`registry/` vs `registry-new/`, без учёта генерируемых `SpaceId` и params):
|
плюс ранее переведённые 40). Остаток — 23 инструмента, требующих достройки
|
||||||
|
фундамента preview. По категориям:
|
||||||
|
|
||||||
- **color — 18** (из 25, переведено 7): grayscale-png, invert-colors-png,
|
- **alpha — 1**: `watermark-image-png`.
|
||||||
adjust-brightness-contrast-png, auto-contrast-png, sepia-png, posterize-png,
|
- **analyze — 6**: text-вердикты `png-info`, `png-is-grayscale`,
|
||||||
black-and-white-png, decrease-color-count-png, extract-channel-png,
|
`png-file-size`, `png-is-transparent`, `png-orientation`, `verify-is-png`.
|
||||||
swap-channels-png, change-png-opacity, change-png-hue, плюс 6 канальных
|
- **generate — 2**: `mix-colors-png`, `sort-colors-png` (списки значений).
|
||||||
`png-to-{hsl,hsv,hsi,cmyk,ycbcr,lab}` (генерируются из `SPACES`);
|
- **convert — 14**: text-source-конвертеры
|
||||||
`compress-png`/`reduce-to-size-png` — нет, они в convert. Простое; канальные —
|
`base64/data-uri/hex/bytes/rgb-values-to-png` + обратные
|
||||||
на `core/channels.ts` (`renderSpace`). Нюанс: `change-png-opacity`/`hue`
|
`png-to-base64/…`; output/формат `compress-png`, `reduce-to-size-png`,
|
||||||
похожи на переведённые, но это отдельные инструменты, не дубли.
|
`png-to-bmp`; `svg-to-png` (text-source + DOM).
|
||||||
- **geometry — 13** (из 18): rotate, flip, skew, zoom, shift, add-padding, tile,
|
|
||||||
trim-empty-space, change-aspect-ratio, swap-orientation, symmetric-copy,
|
|
||||||
center-by-alpha, rotate-free. В основном простое (slider/select/color/checkbox).
|
|
||||||
Нюансы: `rotate-free` — угол 0..360 (посмотреть reuse `AngleControl` или
|
|
||||||
slider); `tile` — cols/rows (два числа, можно `dimension` или два slider);
|
|
||||||
`shift` — offsetX/Y в px + цвет (не путать с `offset`-процентами).
|
|
||||||
- **alpha — 14** (из 21): remove-alpha, set-alpha, extract-alpha-mask,
|
|
||||||
round-corners, invert-alpha, remove-background, make-thicker, make-thinner,
|
|
||||||
feather-edges, clean-edges, harden-alpha, despeckle-alpha, close-holes +
|
|
||||||
**`watermark-image-png`** (см. особый случай). Остальное простое.
|
|
||||||
- **generate — 10** (из 21): emoji-to-png (emoji+size); палитровые генераторы
|
|
||||||
color-wheel, complementary, analogous, triadic, tetradic, monochromatic,
|
|
||||||
shades (baseColor + count/spread — простое); **`mix-colors`** и
|
|
||||||
**`sort-colors`** — особые случаи (списки).
|
|
||||||
- **convert — 14** (из 16): text-source-инструменты
|
|
||||||
base64/data-uri/hex/bytes/rgb-values-to-png + обратные
|
|
||||||
png-to-base64/data-uri/hex/bytes/rgb-values/bmp — **особый случай** (нужен
|
|
||||||
text-источник в preview); `compress-png`/`reduce-to-size-png` — **особый
|
|
||||||
случай** (output/формат, связан с backlog №7); `svg-to-png` — width.
|
|
||||||
- **analyze — 12** (из 13): show-transparent, show-grayscale, show-color,
|
|
||||||
light/dark-pixel-mask, unique-color-mask (color+opacity+tolerance+mode) —
|
|
||||||
простое, частично переиспользуемый предикат; **`verify-is-png`** — текстовый
|
|
||||||
вердикт, особый случай; плюс 5 text-вердиктов `png-info`, `png-is-grayscale`,
|
|
||||||
`png-file-size`, `png-is-transparent`, `png-orientation` — тоже текстовый
|
|
||||||
вывод (см. text-source/вердикт-механику).
|
|
||||||
- **filters — 3** (из 8): blur (radius), sharpen (strength), silhouette
|
|
||||||
(color+threshold). Простое.
|
|
||||||
- **text — 1** (из 3): **`watermark-tile-png`** — text+font-style+opacity+angle+
|
|
||||||
stepX/stepY. Переиспользует font-style; угол — AngleControl-подобный.
|
|
||||||
|
|
||||||
### Особые случаи (требуют достройки фундамента, не просто перевод)
|
### Особые случаи (требуют достройки фундамента, не просто перевод)
|
||||||
|
|
||||||
@@ -89,19 +64,20 @@
|
|||||||
применяется поверх. В новом preview это единственный непереведённый
|
применяется поверх. В новом preview это единственный непереведённый
|
||||||
инструмент с источником-картинкой помимо основного входа. Учесть связь с
|
инструмент с источником-картинкой помимо основного входа. Учесть связь с
|
||||||
`overlay-store` (упомянут в `plan-redesign.md`).
|
`overlay-store` (упомянут в `plan-redesign.md`).
|
||||||
2. **Text-source / text-вердикты** (base64/data-uri/hex/bytes/rgb-values-to-png
|
2. **Text-source / text-вердикты** (вход: `base64/data-uri/hex/bytes/
|
||||||
и обратные png-to-base64/…; аналитические `png-info`, `png-is-grayscale`,
|
rgb-values-to-png`, `svg-to-png`; выход-вердикт: `png-to-base64/…`,
|
||||||
`png-file-size`, `png-is-transparent`, `png-orientation`, `verify-is-png`) —
|
аналитические `png-info`, `png-is-grayscale`, `png-file-size`,
|
||||||
в старом UI это `text`-параметры, из которых собирается изображение, либо
|
`png-is-transparent`, `png-orientation`, `verify-is-png`) — тип `ToolEntry`
|
||||||
текстовый результат вместо картинки. В новом preview нужно решить, как
|
уже имеет `toText`/`textToText`/`runFromText`, но executor превью их **не
|
||||||
подаётся текст-вход (текстовое поле/паста на странице инструмента) и как
|
вызывает** (только `run`/`generate`). Нужно решить, как подаётся текст-вход
|
||||||
показывается результат для вердиктных инструментов (в конверсиях — не
|
(текстовое поле/паста на странице инструмента) и как показывается результат
|
||||||
картинка, а текст/бейдж).
|
для вердиктных инструментов (в конверсиях — не картинка, а текст/бейдж), и
|
||||||
3. **Output/формат** (`compress-png`, `reduce-to-size-png`; пересекается с
|
протащить эти поля через executor/UI.
|
||||||
`convert-png-to-jpg`/`webp`) — по `backlog.md` №7 формат переезжает в
|
3. **Output/формат** (`compress-png`, `reduce-to-size-png`, `png-to-bmp`;
|
||||||
кнопку Download, а не в отдельный инструмент. Перед переводом решить:
|
пересекается с `convert-png-to-jpg`/`webp`) — по `backlog.md` №7 формат
|
||||||
переводим как инструменты с quality-параметром или дожидаемся достройки
|
переезжает в кнопку Download, а не в отдельный инструмент. Перед переводом
|
||||||
download-формата.
|
решить: переводим как инструменты с quality-параметром или дожидаемся
|
||||||
|
достройки download-формата.
|
||||||
4. **`mix-colors` / `sort-colors`** — нужен список значений (цвета/веса или
|
4. **`mix-colors` / `sort-colors`** — нужен список значений (цвета/веса или
|
||||||
источник). В схеме нет list-kind: либо новый kind, либо фиксированное
|
источник). В схеме нет list-kind: либо новый kind, либо фиксированное
|
||||||
число слотов, либо отложить до отдельной задачи про списки.
|
число слотов, либо отложить до отдельной задачи про списки.
|
||||||
@@ -119,20 +95,16 @@ a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающ
|
|||||||
|
|
||||||
## Шаги перевода остатка (порядок)
|
## Шаги перевода остатка (порядок)
|
||||||
|
|
||||||
1. **Простые пачки** (основной объём, 1 инструмент = 1 диф, паттерн Фазы 2):
|
1. **Простые пачки** — **выполнено**: color (18) → filters (3) → geometry (13)
|
||||||
color (18) → filters (3) → geometry простое → alpha (кроме watermark-image).
|
→ alpha (13/14, кроме watermark-image) → generate (8/10) → analyze-маски
|
||||||
2. **Analyze-маски и вердикты** (12): переиспользование предикатов
|
(6/12) → text `watermark-tile-png`. Каждая пачка после себя гнала гейты
|
||||||
`core/masks` в новом `registry-new/analyze.ts`; `show-color-pixels` — аналог
|
(eslint/svelte-check/тесты/prettier); в тестах только предсуществующий фейл
|
||||||
переведённого `extract-color-from-png` (уже в registry-new); text-вердикты
|
`palette.test.ts`.
|
||||||
(png-info, png-is-grayscale, png-file-size, png-is-transparent,
|
2. **Особые случаи** — отдельным переговоренным решением (overlay для
|
||||||
png-orientation, verify-is-png) — вместе с text-source-механикой (п. 5).
|
watermark-image, text-source/вердикты — через `toText`/`runFromText` в
|
||||||
3. **Generate-палитры** (8 без mix/sort): генераторы с палитрой (как
|
executor, output-формат, списки для mix/sort). Это единственный реальный
|
||||||
`color-spectrum`/`step-colors`, кнопка Generate).
|
объём для дожатия каталога.
|
||||||
4. **Raw-пакеты** (watermark-tile): font-style + угол + dimension(+),
|
3. После категории — `schema.layout` для сгруппированных инструментов
|
||||||
сверить с `add-text` (переведён).
|
|
||||||
5. **Особые случаи** — отдельным переговоренным решением (overlay для
|
|
||||||
watermark-image, text-source, output-формат, списки для mix/sort).
|
|
||||||
6. После категории — `schema.layout` для сгруппированных инструментов
|
|
||||||
(шаг 32-33 паттерн) и ручная проверка в `/preview`.
|
(шаг 32-33 паттерн) и ручная проверка в `/preview`.
|
||||||
|
|
||||||
## Переезд в `old/` (шаг 37 основного плана)
|
## Переезд в `old/` (шаг 37 основного плана)
|
||||||
@@ -188,15 +160,12 @@ a11y `ColorField`, пустой ruleset `Toggle`. Плюс два, мешающ
|
|||||||
|
|
||||||
| Часть | Сложность | Оценка |
|
| Часть | Сложность | Оценка |
|
||||||
| ---------------------------------------------------------------- | --------------- | --------------------- |
|
| ---------------------------------------------------------------- | --------------- | --------------------- |
|
||||||
| Простые инструменты (color/filters/geometry/alpha без watermark) | Низкая-Средняя | ~5-8ч (≈47 шт) |
|
| Простые инструменты (color/filters/geometry/alpha/generate/маски/text) | Низкая-Средняя | **выполнено** (62 шт) |
|
||||||
| Analyze-маски (12, часть в п.5 с text-source) | Средняя | ~2ч |
|
|
||||||
| Generate-палитры (8) | Средняя | ~2-3ч |
|
|
||||||
| watermark-tile (text) | Средняя | ~1ч |
|
|
||||||
| Особые случаи (overlay/text-source/format/списки) | Средняя-Высокая | ~4-6ч + ревью решения |
|
| Особые случаи (overlay/text-source/format/списки) | Средняя-Высокая | ~4-6ч + ревью решения |
|
||||||
| Долги (ToolCard, palette.test.ts, lint:all остаток) | Средняя | ~2-4ч |
|
| Долги (ToolCard, palette.test.ts, lint:all остаток) | Средняя | ~2-4ч |
|
||||||
| Переезд в `old/` + конфиг плагина + гейты | Средняя-Низкая | ~2-4ч |
|
| Переезд в `old/` + конфиг плагина + гейты | Средняя-Низкая | ~2-4ч |
|
||||||
| Проверка/фидбек + правки | Зависит | ~3-6ч |
|
| Проверка/фидбек + правки | Зависит | ~3-6ч |
|
||||||
| **Итого** | | **~21-34ч** по фазам |
|
| **Итого** | | **~11-20ч** по фазам |
|
||||||
|
|
||||||
> Оценки ориентировочные; особые случаи — с ревью решения, объём финальных
|
> Оценки ориентировочные; особые случаи — с ревью решения, объём финальных
|
||||||
> правок по фидбеку не предсказуем.
|
> правок по фидбеку не предсказуем.
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { ToolEntry } from "./types";
|
import type { ToolEntry } from "./types";
|
||||||
import { field, toolSchema } from "../registry-schema";
|
import { field, toolSchema } from "../registry-schema";
|
||||||
import { extractByColor } from "../core/masks";
|
import {
|
||||||
|
extractByColor,
|
||||||
|
isGrayscaleish,
|
||||||
|
luma01,
|
||||||
|
rarityPredicate,
|
||||||
|
renderPredicateMask,
|
||||||
|
} from "../core/masks";
|
||||||
|
|
||||||
interface ExtractColorParams {
|
interface ExtractColorParams {
|
||||||
color: string;
|
color: string;
|
||||||
@@ -22,4 +28,160 @@ const extractColor: ToolEntry<ExtractColorParams> = {
|
|||||||
run: (img, p) => extractByColor(img, p.color, p.tolerance),
|
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 { colorSpectrum, drawGrid, randomColorBlocks } from "../core/gen-tools";
|
||||||
import { noiseImage, solidImage } from "../core/generate";
|
import { noiseImage, solidImage } from "../core/generate";
|
||||||
import { changeCanvasSize } from "../core/geometry";
|
import { changeCanvasSize } from "../core/geometry";
|
||||||
import type { PixelImage } from "../core/types";
|
import type { PixelImage } from "../core/types";
|
||||||
import {
|
import {
|
||||||
|
analogousSet,
|
||||||
|
complementarySet,
|
||||||
hexToRgb,
|
hexToRgb,
|
||||||
|
monochromaticSet,
|
||||||
renderBlend,
|
renderBlend,
|
||||||
renderSwatches,
|
renderSwatches,
|
||||||
|
renderWheel,
|
||||||
|
shadeSet,
|
||||||
stepColors,
|
stepColors,
|
||||||
|
tetradicSet,
|
||||||
|
triadicSet,
|
||||||
} from "../core/palette";
|
} from "../core/palette";
|
||||||
import {
|
import {
|
||||||
field,
|
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 {
|
interface TextToPngParams {
|
||||||
text: string;
|
text: string;
|
||||||
style: FontStyle;
|
style: FontStyle;
|
||||||
@@ -505,5 +708,13 @@ export const generateEntries = [
|
|||||||
placeholder,
|
placeholder,
|
||||||
blendTwo,
|
blendTwo,
|
||||||
stepColorsTool,
|
stepColorsTool,
|
||||||
|
emojiToPng,
|
||||||
|
colorWheelTool,
|
||||||
|
complementaryTool,
|
||||||
|
triadicTool,
|
||||||
|
tetradicTool,
|
||||||
|
analogousTool,
|
||||||
|
monochromaticTool,
|
||||||
|
shadesTool,
|
||||||
textToPng,
|
textToPng,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { formatStamp } from "../core/datefmt";
|
import { formatStamp } from "../core/datefmt";
|
||||||
import { drawTextBlock } from "../core/domText";
|
import { drawTextBlock, drawTextTile } from "../core/domText";
|
||||||
import type { Position9 } from "../core/textdraw";
|
import type { Position9 } from "../core/textdraw";
|
||||||
import type { FontStyle, Plate } from "../registry-schema";
|
import type { FontStyle, Plate } from "../registry-schema";
|
||||||
import { field, toolSchema } 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