diff --git a/docs/plan-composite-params.md b/docs/plan-composite-params.md index 383ea08..57e20db 100644 --- a/docs/plan-composite-params.md +++ b/docs/plan-composite-params.md @@ -11,11 +11,12 @@ > полностью переведён** (4 инструмента: circle-mask, square-mask, > star-mask, wavy-mask); **составной тип `position9` — переведены > add-text-png и date-stamp-png** (водяной знак-картинка — отдельный шаг: -> требует overlay-механику в новом превью); preview +> требует overlay-механику в новом превью); **составной тип `font-style` +> полностью переведён** (text-to-png, add-text, date-stamp); preview > научен применять **генераторы** (`executeGenerate`, кнопка Generate) и > рендерить все kinds схемы (slider/number/color/select/checkbox/dimension/ -> color-pair/offset/position9). -> Следующее: `font-style` (text-to-png → add-text → date-stamp). +> color-pair/offset/position9/font-style). +> Следующее: `plate` (add-text → date-stamp). > > Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts` > (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate) @@ -373,8 +374,13 @@ Typed field builders + `Field` + `toolSchema

()` + `ToolSchema

` — ему нужен overlay-source (`getOverlay`/store), которого в новом превью пока нет. Тесты: +2 (602 passed). `font-style` и `plate` на этих инструментах сводятся в шаги 28-29. -28. `font-style` — перевести text-to-png → add-text (если ещё не) → - date-stamp +28. `font-style` — **все 3 инструмента переведены** ✔ (`text-to-png` — + генератор в `registry-new/generate.ts` (domOnly), `add-text-png`/ + `date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип + во всех видах: вложенный объект `style: { font, size, bold, color }` + + `field.fontStyle`, виджет `kit/fields/schema/FontStyleControl.svelte`, + kind `font-style` в схеме (default/sanitize, clamp размера к min/max). + Тесты: +1 (603 passed). 29. `plate` — перевести add-text → date-stamp 30. `gradient` — собрать из dimension + color-pair + direction на linear-gradient (зависит от решения по gradient, см. план) diff --git a/web/src/lib/components/kit/SchemaFields.svelte b/web/src/lib/components/kit/SchemaFields.svelte index bca96c0..33d173d 100644 --- a/web/src/lib/components/kit/SchemaFields.svelte +++ b/web/src/lib/components/kit/SchemaFields.svelte @@ -15,6 +15,7 @@ import TextControl from "./fields/schema/TextControl.svelte"; import OffsetControl from "./fields/schema/OffsetControl.svelte"; import PositionControl from "./fields/schema/PositionControl.svelte"; + import FontStyleControl from "./fields/schema/FontStyleControl.svelte"; interface FieldControlProps { label: string; @@ -34,6 +35,7 @@ dimension: DimensionField, offset: OffsetControl, position9: PositionControl, + "font-style": FontStyleControl, }; interface Props { diff --git a/web/src/lib/components/kit/fields/schema/FontStyleControl.svelte b/web/src/lib/components/kit/fields/schema/FontStyleControl.svelte new file mode 100644 index 0000000..64c9182 --- /dev/null +++ b/web/src/lib/components/kit/fields/schema/FontStyleControl.svelte @@ -0,0 +1,162 @@ + + +

+ {label} +
+ + + + +
+
+ + diff --git a/web/src/lib/registry-new/generate.ts b/web/src/lib/registry-new/generate.ts index 6e8d088..bbe2b6b 100644 --- a/web/src/lib/registry-new/generate.ts +++ b/web/src/lib/registry-new/generate.ts @@ -13,6 +13,7 @@ import { toolSchema, type ColorPair, type Dimension, + type FontStyle, } from "../registry-schema"; import type { ToolEntry } from "./types"; @@ -324,6 +325,50 @@ const stepColorsTool: ToolEntry = { ), }; +interface TextToPngParams { + text: string; + style: FontStyle; + transparentBg: boolean; + backgroundColor: string; + padding: number; +} + +export const textToPngSchema = toolSchema({ + 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 }), +}); + +const textToPng: ToolEntry = { + 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, @@ -335,4 +380,5 @@ export const generateEntries = [ placeholder, blendTwo, stepColorsTool, + textToPng, ]; diff --git a/web/src/lib/registry-new/registry-new.test.ts b/web/src/lib/registry-new/registry-new.test.ts index 356f2f8..bd6ce5c 100644 --- a/web/src/lib/registry-new/registry-new.test.ts +++ b/web/src/lib/registry-new/registry-new.test.ts @@ -305,22 +305,30 @@ describe("registry-new (переведённые инструменты)", () => expect(s.offset).toEqual({ x: 50, y: 0 }); }); - it("add-text: дефолты position9 и текстовых полей", () => { + it("add-text: дефолты position9, font-style и текстовых полей", () => { const tool = TOOLS.find((t) => t.id === "add-text-png")!; const d = defaultSchemaParams(tool.schema); expect(d.position).toBe("bottom-right"); expect(d.text).toBe("Hello!"); expect(d.plate).toBe(false); + expect(d.style).toEqual({ + font: "sans", + size: 48, + bold: true, + color: "#ffffff", + }); }); - it("sanitize чинит мусор в position9 и оставляет валидные значения", () => { + it("sanitize чинит мусор в position9, font-style и оставляет валидные значения", () => { const tool = TOOLS.find((t) => t.id === "date-stamp-png")!; const s = sanitizeSchemaParams(tool.schema, { format: "YYYY-MM-DD", - fontSize: 32, - color: "#ffffff", - font: "mono", - bold: "no" as unknown as boolean, + style: { + font: "comic-sans", + size: -500, + bold: "no", + color: "red", + }, position: "somewhere-outside", margin: 20, plate: true, @@ -328,12 +336,46 @@ describe("registry-new (переведённые инструменты)", () => plateOpacity: 55, }); expect(s.position).toBe("bottom-right"); - expect(s.bold).toBe(false); + expect(s.style).toEqual({ + font: "mono", + size: 8, + bold: false, + color: "#ffffff", + }); const valid = sanitizeSchemaParams(tool.schema, { ...defaultSchemaParams(tool.schema), position: "top-center", + style: { font: "serif", size: 120, bold: true, color: "#00ff00" }, }); expect(valid.position).toBe("top-center"); + expect(valid.style).toEqual({ + font: "serif", + size: 120, + bold: true, + color: "#00ff00", + }); + }); + + it("text-to-png: дефолты font-style и sanitize", () => { + const tool = TOOLS.find((t) => t.id === "text-to-png")!; + const d = defaultSchemaParams(tool.schema); + expect(d.style).toEqual({ + font: "sans", + size: 96, + bold: true, + color: "#111318", + }); + expect(d.transparentBg).toBe(false); + const s = sanitizeSchemaParams(tool.schema, { + style: { font: "sans", size: 9999, bold: false, color: "#123abc" }, + padding: 200, + }); + expect(s.style).toEqual({ + font: "sans", + size: 300, + bold: false, + color: "#123abc", + }); }); }); diff --git a/web/src/lib/registry-new/text.ts b/web/src/lib/registry-new/text.ts index 2de4748..95a1f6b 100644 --- a/web/src/lib/registry-new/text.ts +++ b/web/src/lib/registry-new/text.ts @@ -1,21 +1,13 @@ -import type { ToolEntry } from "./types"; -import type { Position9 } from "../core/textdraw"; -import { drawTextBlock, type TextFont } from "../core/domText"; import { formatStamp } from "../core/datefmt"; +import { drawTextBlock } from "../core/domText"; +import type { Position9 } from "../core/textdraw"; +import type { FontStyle } from "../registry-schema"; import { field, toolSchema } from "../registry-schema"; - -const FONT_OPTIONS = [ - { value: "sans", label: "Sans-serif" }, - { value: "serif", label: "Serif" }, - { value: "mono", label: "Monospace" }, -] satisfies { value: TextFont; label: string }[]; +import type { ToolEntry } from "./types"; interface AddTextParams { text: string; - fontSize: number; - color: string; - font: TextFont; - bold: boolean; + style: FontStyle; position: Position9; margin: number; plate: boolean; @@ -25,10 +17,14 @@ interface AddTextParams { const addTextSchema = toolSchema({ text: field.text({ default: "Hello!", placeholder: "Your text" }), - fontSize: field.slider({ min: 8, max: 200, step: 1, default: 48 }), - color: field.color({ default: "#ffffff" }), - font: field.select({ default: "sans", options: FONT_OPTIONS }), - bold: field.checkbox({ default: true }), + style: field.fontStyle({ + min: 8, + max: 200, + size: 48, + font: "sans", + bold: true, + color: "#ffffff", + }), position: field.position9({ default: "bottom-right" }), margin: field.slider({ min: 0, max: 200, step: 1, default: 24 }), plate: field.checkbox({ default: false }), @@ -47,10 +43,10 @@ const addText: ToolEntry = { run: (img, p) => drawTextBlock(img, { text: p.text, - fontSize: p.fontSize, - font: p.font, - bold: p.bold, - color: p.color, + fontSize: p.style.size, + font: p.style.font, + bold: p.style.bold, + color: p.style.color, opacityPercent: 100, position: p.position, margin: p.margin, @@ -61,10 +57,7 @@ const addText: ToolEntry = { interface DateStampParams { format: string; - fontSize: number; - color: string; - font: TextFont; - bold: boolean; + style: FontStyle; position: Position9; margin: number; plate: boolean; @@ -77,10 +70,14 @@ const dateStampSchema = toolSchema({ default: "YYYY-MM-DD", placeholder: "YYYY-MM-DD hh:mm", }), - fontSize: field.slider({ min: 8, max: 200, step: 1, default: 32 }), - color: field.color({ default: "#ffffff" }), - font: field.select({ default: "mono", options: FONT_OPTIONS }), - bold: field.checkbox({ default: false }), + style: field.fontStyle({ + min: 8, + max: 200, + size: 32, + font: "mono", + bold: false, + color: "#ffffff", + }), position: field.position9({ default: "bottom-right" }), margin: field.slider({ min: 0, max: 200, step: 1, default: 20 }), plate: field.checkbox({ default: true }), @@ -99,10 +96,10 @@ const dateStamp: ToolEntry = { run: (img, p) => drawTextBlock(img, { text: formatStamp(new Date(), p.format), - fontSize: p.fontSize, - font: p.font, - bold: p.bold, - color: p.color, + fontSize: p.style.size, + font: p.style.font, + bold: p.style.bold, + color: p.style.color, opacityPercent: 100, position: p.position, margin: p.margin, diff --git a/web/src/lib/registry-schema.ts b/web/src/lib/registry-schema.ts index 62a8c91..088af36 100644 --- a/web/src/lib/registry-schema.ts +++ b/web/src/lib/registry-schema.ts @@ -12,6 +12,7 @@ // Ошибки компилятора ловят расхождения между интерфейсом Params и схемой. import type { Position9 } from "./core/textdraw"; +import type { TextFont } from "./core/domText"; export interface NumberSpec { kind: "number"; @@ -114,6 +115,28 @@ export interface Position9Spec { default: Position9; } +/** + * Стиль текстовой надписи: шрифт, размер, жирность и цвет как один объект + * (переиспользуется text-to-png, add-text, date-stamp). + */ +export interface FontStyle { + font: TextFont; + size: number; + bold: boolean; + color: string; +} + +export interface FontStyleSpec { + kind: "font-style"; + /** Диапазон размера шрифта. */ + min: number; + max: number; + size: number; + font: TextFont; + bold: boolean; + color: string; +} + /** * «Объект as const» kind → спека поля. Единственный источник правды для * перечня kinds: `FieldSpecKind` = ключи map, `FieldSpec` = значение по любому @@ -131,6 +154,7 @@ export const fieldSpecs = { "color-pair": {} as ColorPairSpec, offset: {} as OffsetSpec, position9: {} as Position9Spec, + "font-style": {} as FontStyleSpec, } as const; export type FieldSpecKind = keyof typeof fieldSpecs; @@ -198,6 +222,16 @@ export const field = { position9: (s: { default: Position9 }): Field => ({ spec: { kind: "position9", ...s }, }), + fontStyle: (s: { + min: number; + max: number; + size: number; + font: TextFont; + bold: boolean; + color: string; + }): Field => ({ + spec: { kind: "font-style", ...s }, + }), }; /** @@ -230,6 +264,13 @@ export function defaultSchemaParams

( out[key] = { from: spec.from, to: spec.to }; } else if (spec.kind === "offset") { out[key] = { x: spec.x, y: spec.y }; + } else if (spec.kind === "font-style") { + out[key] = { + font: spec.font, + size: spec.size, + bold: spec.bold, + color: spec.color, + }; } else { out[key] = spec.default; } @@ -320,6 +361,33 @@ export function sanitizeSchemaParams

( ? (raw as Position9) : spec.default; break; + case "font-style": { + const r = + typeof raw === "object" && + raw !== null && + "font" in raw && + "size" in raw && + "bold" in raw && + "color" in raw + ? (raw as Record) + : undefined; + out[key] = { + font: + r?.font === "sans" || r?.font === "serif" || r?.font === "mono" + ? r.font + : spec.font, + size: + typeof r?.size === "number" && Number.isFinite(r.size) + ? clamp(r.size, spec.min, spec.max) + : spec.size, + bold: typeof r?.bold === "boolean" ? r.bold : spec.bold, + color: + typeof r?.color === "string" && /^#[0-9a-f]{6}$/i.test(r.color) + ? r.color + : spec.color, + }; + break; + } case "offset": { const r = typeof raw === "object" && raw !== null && "x" in raw && "y" in raw