diff --git a/docs/backlog.md b/docs/backlog.md index 24a269b..93f1401 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -81,8 +81,6 @@ - [ ] сделать несколько проходное сжатие png для получения необходимого размера файлов, посмотреть реализацию в [twitch-emote-resizer](https://github.com/tma02/twitch-emote-resizer) -- [ ] Пересмотреть прозрачность в инструментах - Create empty PNG - прозрачность - должны быть настраиваемой 0-100%, а не переключателем - [ ] Font Style - обдумать выбор шрифта (google fonts???) как организовать шрифты с кириллицей? сделать галочку "только с кириллицей"??? (решение о наборе и лицензиях: `docs/plan-platform.md` §4 — только OFL/Apache с diff --git a/web/src/lib/core/domText.ts b/web/src/lib/core/domText.ts index 9898d81..b9ef61c 100644 --- a/web/src/lib/core/domText.ts +++ b/web/src/lib/core/domText.ts @@ -127,7 +127,7 @@ export interface TextToImageOptions { bold: boolean; color: string; backgroundColor: string; - transparentBg: boolean; + bgOpacity: number; padding: number; maxTextWidth?: number; } @@ -148,9 +148,11 @@ export function renderTextToImage(o: TextToImageOptions): PixelImage { const h = Math.max(1, Math.ceil(lineH + o.padding * 2)); const { canvas, ctx } = ctx2d(w, h); - if (!o.transparentBg) { + if (o.bgOpacity > 0) { + ctx.globalAlpha = Math.min(100, Math.max(0, o.bgOpacity)) / 100; ctx.fillStyle = o.backgroundColor; ctx.fillRect(0, 0, w, h); + ctx.globalAlpha = 1; } ctx.font = fontString(o.fontSize, o.font, o.bold); ctx.fillStyle = o.color; diff --git a/web/src/lib/core/gen-tools.test.ts b/web/src/lib/core/gen-tools.test.ts index 4de4be6..fbe0f2a 100644 --- a/web/src/lib/core/gen-tools.test.ts +++ b/web/src/lib/core/gen-tools.test.ts @@ -29,16 +29,22 @@ describe("randomColorBlocks", () => { describe("drawGrid", () => { it("линии на пересечениях непрозрачны, фон прозрачен", () => { - const out = drawGrid(100, 100, 4, 4, 2, "#000000", true); + const out = drawGrid(100, 100, 4, 4, 2, "#000000", 0); expect(out.data[0]).toBe(0); expect(out.data[3]).toBe(255); // (0,0) на линии const mid = (53 * 100 + 53) * 4; expect(out.data[mid + 3]).toBe(0); // между линиями прозрачн }); - it("белый непрозрачный фон при transparentBg=false", () => { - const out = drawGrid(20, 20, 2, 2, 1, "#000000", false); + it("белый непрозрачный фон при bgOpacity=100", () => { + const out = drawGrid(20, 20, 2, 2, 1, "#000000", 100); const mid = (5 * 20 + 5) * 4; expect(out.data[mid]).toBe(255); }); + + it("полупрозрачный фон при bgOpacity=50", () => { + const out = drawGrid(20, 20, 2, 2, 1, "#000000", 50); + const mid = (5 * 20 + 5) * 4; + expect(out.data[mid + 3]).toBe(128); + }); }); diff --git a/web/src/lib/core/gen-tools.ts b/web/src/lib/core/gen-tools.ts index cf26070..5d64e05 100644 --- a/web/src/lib/core/gen-tools.ts +++ b/web/src/lib/core/gen-tools.ts @@ -79,7 +79,7 @@ function lineMask( return mask; } -/** Сетка линий на прозрачном или белом фоне. */ +/** Сетка линий на белом фоне заданной непрозрачности (0 — прозрачный). */ export function drawGrid( width: number, height: number, @@ -87,10 +87,20 @@ export function drawGrid( rows: number, lineWidth: number, colorHex: string, - transparentBg: boolean, + bgOpacity: number, ): PixelImage { const out = createPixelImage(width, height); - if (!transparentBg) out.data.fill(255); + const bgAlpha = Math.round( + (Math.max(0, Math.min(100, bgOpacity)) / 100) * 255, + ); + if (bgAlpha > 0) { + for (let i = 0; i < out.data.length; i += 4) { + out.data[i] = 255; + out.data[i + 1] = 255; + out.data[i + 2] = 255; + out.data[i + 3] = bgAlpha; + } + } const m = /^#([0-9a-f]{6})$/i.exec(colorHex.trim()); const r = m ? parseInt(m[1].slice(0, 2), 16) : 0; diff --git a/web/src/lib/core/geometry.ts b/web/src/lib/core/geometry.ts index cc551d1..6c280f2 100644 --- a/web/src/lib/core/geometry.ts +++ b/web/src/lib/core/geometry.ts @@ -9,19 +9,20 @@ export function expandCanvas( right: number, bottom: number, backgroundHex?: string, + backgroundAlpha = 255, ): PixelImage { const l = Math.max(0, Math.trunc(left)); const t = Math.max(0, Math.trunc(top)); const r = Math.max(0, Math.trunc(right)); const b = Math.max(0, Math.trunc(bottom)); const out = createPixelImage(img.width + l + r, img.height + t + b); - if (backgroundHex !== undefined) { + if (backgroundHex !== undefined && backgroundAlpha > 0) { const [cr, cg, cb] = parseHex(backgroundHex); for (let i = 0; i < out.data.length; i += 4) { out.data[i] = cr; out.data[i + 1] = cg; out.data[i + 2] = cb; - out.data[i + 3] = 255; + out.data[i + 3] = backgroundAlpha; } } for (let y = 0; y < img.height; y++) { diff --git a/web/src/lib/i18n/en.ts b/web/src/lib/i18n/en.ts index 3008cd7..ab157e7 100644 --- a/web/src/lib/i18n/en.ts +++ b/web/src/lib/i18n/en.ts @@ -298,8 +298,6 @@ export const en: Dict = { textStyle: "Text style", thickness: "Thickness", threshold: "Threshold", - transparent: "Transparent", - transparentBg: "Transparent bg", waves: "Waves", width: "Width", x: "X", diff --git a/web/src/lib/i18n/ru.ts b/web/src/lib/i18n/ru.ts index c1b11c4..df2e0b5 100644 --- a/web/src/lib/i18n/ru.ts +++ b/web/src/lib/i18n/ru.ts @@ -299,8 +299,6 @@ export const ru: Dict = { textStyle: "Стиль текста", thickness: "Толщина", threshold: "Порог", - transparent: "Прозрачный", - transparentBg: "Прозрачный фон", waves: "Волны", width: "Ширина", x: "X", @@ -972,7 +970,7 @@ export const ru: Dict = { "add-padding": { params: { padding: "Поля, px", - transparent: "Прозрачные поля", + opacity: "Непрозрачность полей, %", color: "Цвет полей", }, }, @@ -983,7 +981,7 @@ export const ru: Dict = { params: { width: "Ширина полотна", height: "Высота полотна", - transparent: "Прозрачный фон", + opacity: "Непрозрачность фона, %", color: "Цвет фона", }, }, @@ -1113,7 +1111,7 @@ export const ru: Dict = { params: { width: "Ширина", height: "Высота", - transparent: "Прозрачный", + opacity: "Непрозрачность", color: "Цвет", }, }, @@ -1352,7 +1350,7 @@ export const ru: Dict = { font: "Шрифт", bold: "Жирный", color: "Цвет текста", - transparentBg: "Прозрачный фон", + bgOpacity: "Непрозрачность фона, %", backgroundColor: "Цвет фона", padding: "Отступ, px", }, @@ -1397,7 +1395,7 @@ export const ru: Dict = { rows: "Строки", lineWidth: "Толщина линий, px", color: "Цвет линий", - transparentBg: "Прозрачный фон", + bgOpacity: "Непрозрачность фона, %", }, }, quantize: { diff --git a/web/src/lib/registry/registry.test.ts b/web/src/lib/registry/registry.test.ts index 16cdb81..661551d 100644 --- a/web/src/lib/registry/registry.test.ts +++ b/web/src/lib/registry/registry.test.ts @@ -199,14 +199,14 @@ describe("registry-new (переведённые инструменты)", () => const tool = TOOLS.find((t) => t.id === "create-empty")!; const d = defaultSchemaParams(tool.schema); expect(d.size).toEqual({ width: 800, height: 600 }); - expect(d.transparent).toBe(true); + expect(d.opacity).toBe(0); }); it("sanitize клампит dimension к min/max и чинит мусор", () => { const tool = TOOLS.find((t) => t.id === "create-empty")!; const s = sanitizeSchemaParams(tool.schema, { size: { width: 999999, height: -5 }, - transparent: false, + opacity: 50, color: "#ff0000", }); expect(s.size).toEqual({ width: 20000, height: 1 }); @@ -218,7 +218,7 @@ describe("registry-new (переведённые инструменты)", () => const tool = TOOLS.find((t) => t.id === "create-empty")!; const params = sanitizeSchemaParams(tool.schema, { size: { width: 320, height: 200 }, - transparent: true, + opacity: 0, color: "#ff0000", }); const img = asImage(await tool.run({ params })); @@ -262,7 +262,7 @@ describe("registry-new (переведённые инструменты)", () => rows: 3, lineWidth: 2, color: "#000000", - transparentBg: true, + bgOpacity: 0, }, ], ])("генератор %s даёт картинку по dimension", async (id, params) => { @@ -336,7 +336,7 @@ describe("registry-new (переведённые инструменты)", () => source: img, params: sanitizeSchemaParams(tool.schema, { size: { width: 60, height: 30 }, - transparent: true, + opacity: 0, color: "#ffffff", }), }), @@ -633,7 +633,7 @@ describe("registry-new (переведённые инструменты)", () => bold: true, color: "#111318", }); - expect(d.transparentBg).toBe(false); + expect(d.bgOpacity).toBe(100); const s = sanitizeSchemaParams(tool.schema, { style: { font: "sans", size: 9999, bold: false, color: "#123abc" }, padding: 200, diff --git a/web/src/lib/registry/tools/generate.ts b/web/src/lib/registry/tools/generate.ts index e5c4192..4360bae 100644 --- a/web/src/lib/registry/tools/generate.ts +++ b/web/src/lib/registry/tools/generate.ts @@ -78,7 +78,7 @@ function angleGradient( interface CreateEmptyParams { size: Dimension; - transparent: boolean; + opacity: number; color: string; } @@ -92,14 +92,20 @@ export const createEmptySchema = toolSchema( height: 600, presets: true, }), - transparent: field.checkbox({ label: "fields.transparent", default: true }), + opacity: field.slider({ + label: "fields.opacity", + min: 0, + max: 100, + step: 1, + default: 0, + }), color: field.color({ label: "fields.fillColor", default: "#ffffff" }), }, { layout: { groups: [ { title: "groups.canvas", fields: ["size"] }, - { title: "groups.fill", fields: ["transparent", "color"] }, + { title: "groups.fill", fields: ["opacity", "color"] }, ], }, }, @@ -112,10 +118,9 @@ const createEmpty: Tool = { run: genTool((p) => { const w = Math.trunc(p.size.width); const h = Math.trunc(p.size.height); - if (p.transparent) { - return solidImage(w, h, [0, 0, 0, 0]); - } - return solidImage(w, h, rgba(p.color)); + const [r, g, b] = rgba(p.color).slice(0, 3); + const alpha = Math.round((Math.trunc(p.opacity) / 100) * 255); + return solidImage(w, h, [r, g, b, alpha]); }), }; @@ -352,7 +357,7 @@ interface DrawGridParams { rows: number; lineWidth: number; color: string; - transparentBg: boolean; + bgOpacity: number; } export const drawGridSchema = toolSchema( @@ -387,9 +392,12 @@ export const drawGridSchema = toolSchema( default: 2, }), color: field.color({ label: "fields.lineColor", default: "#111318" }), - transparentBg: field.checkbox({ - label: "fields.transparentBg", - default: true, + bgOpacity: field.slider({ + label: "fields.opacity", + min: 0, + max: 100, + step: 1, + default: 0, }), }, { @@ -399,7 +407,7 @@ export const drawGridSchema = toolSchema( { title: "groups.grid", cols: 2, - fields: ["cols", "rows", "lineWidth", "color", "transparentBg"], + fields: ["cols", "rows", "lineWidth", "color", "bgOpacity"], }, ], }, @@ -413,15 +421,7 @@ const drawGridTool: Tool = { run: genTool((p) => { const w = Math.trunc(p.size.width); const h = Math.trunc(p.size.height); - return drawGrid( - w, - h, - p.cols, - p.rows, - p.lineWidth, - p.color, - p.transparentBg, - ); + return drawGrid(w, h, p.cols, p.rows, p.lineWidth, p.color, p.bgOpacity); }), }; @@ -481,7 +481,7 @@ const placeholder: Tool = { bold: true, color: p.color, backgroundColor: p.backgroundColor, - transparentBg: false, + bgOpacity: 100, padding: 0, }); out = changeCanvasSize(label, w, h, "center"); @@ -931,7 +931,7 @@ const sortColorsTool: Tool = { interface TextToPngParams { text: string; style: FontStyle; - transparentBg: boolean; + bgOpacity: number; backgroundColor: string; padding: number; } @@ -952,9 +952,12 @@ export const textToPngSchema = toolSchema( bold: true, color: "#111318", }), - transparentBg: field.checkbox({ - label: "fields.transparentBg", - default: false, + bgOpacity: field.slider({ + label: "fields.opacity", + min: 0, + max: 100, + step: 1, + default: 100, }), backgroundColor: field.color({ label: "fields.backgroundColor", @@ -974,7 +977,7 @@ export const textToPngSchema = toolSchema( { title: "groups.text", fields: ["text", "style"] }, { title: "groups.background", - fields: ["transparentBg", "backgroundColor"], + fields: ["bgOpacity", "backgroundColor"], }, { title: "groups.padding", fields: ["padding"] }, ], @@ -995,7 +998,7 @@ const textToPng: Tool = { bold: p.style.bold, color: p.style.color, backgroundColor: p.backgroundColor, - transparentBg: p.transparentBg, + bgOpacity: p.bgOpacity, padding: p.padding, }), ), diff --git a/web/src/lib/registry/tools/geometry.ts b/web/src/lib/registry/tools/geometry.ts index 29dc80a..926ee35 100644 --- a/web/src/lib/registry/tools/geometry.ts +++ b/web/src/lib/registry/tools/geometry.ts @@ -65,7 +65,7 @@ const addBorder: Tool = { interface FitOnBackgroundParams { size: Dimension; - transparent: boolean; + opacity: number; color: string; } @@ -79,9 +79,12 @@ export const fitOnBackgroundSchema = toolSchema( height: 600, presets: true, }), - transparent: field.checkbox({ - label: "fields.transparent", - default: false, + opacity: field.slider({ + label: "fields.opacity", + min: 0, + max: 100, + step: 1, + default: 100, }), color: field.color({ label: "fields.background", default: "#ffffff" }), }, @@ -89,7 +92,7 @@ export const fitOnBackgroundSchema = toolSchema( layout: { groups: [ { title: "groups.canvas", fields: ["size"] }, - { title: "groups.background", fields: ["transparent", "color"] }, + { title: "groups.background", fields: ["opacity", "color"] }, ], }, }, @@ -113,7 +116,8 @@ const fitOnBackground: Tool = { top, Math.max(0, width - img.width - left), Math.max(0, height - img.height - top), - p.transparent ? undefined : p.color, + p.color, + Math.round((Math.trunc(p.opacity) / 100) * 255), ); }), }; @@ -331,7 +335,7 @@ const flipTool: Tool = { interface AddPaddingParams { padding: number; - transparent: boolean; + opacity: number; color: string; } @@ -344,14 +348,20 @@ export const addPaddingSchema = toolSchema( step: 1, default: 10, }), - transparent: field.checkbox({ label: "fields.transparent", default: true }), + opacity: field.slider({ + label: "fields.opacity", + min: 0, + max: 100, + step: 1, + default: 0, + }), color: field.color({ label: "fields.fillColor", default: "#ffffff" }), }, { layout: { groups: [ { title: "groups.padding", fields: ["padding"] }, - { title: "groups.fill", fields: ["transparent", "color"] }, + { title: "groups.fill", fields: ["opacity", "color"] }, ], }, }, @@ -368,7 +378,8 @@ const addPaddingTool: Tool = { p.padding, p.padding, p.padding, - p.transparent ? undefined : p.color, + p.color, + Math.round((Math.trunc(p.opacity) / 100) * 255), ), ), };