feat: add transparency range slider to opacity fields

This commit is contained in:
2026-10-05 15:39:47 +05:00
parent a0bc72ef5d
commit ba533e67fd
10 changed files with 92 additions and 65 deletions
+4 -2
View File
@@ -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;
+9 -3
View File
@@ -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);
});
});
+13 -3
View File
@@ -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;
+3 -2
View File
@@ -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++) {
-2
View File
@@ -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",
+5 -7
View File
@@ -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: {
+6 -6
View File
@@ -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,
+31 -28
View File
@@ -78,7 +78,7 @@ function angleGradient(
interface CreateEmptyParams {
size: Dimension;
transparent: boolean;
opacity: number;
color: string;
}
@@ -92,14 +92,20 @@ export const createEmptySchema = toolSchema<CreateEmptyParams>(
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<CreateEmptyParams> = {
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<DrawGridParams>(
@@ -387,9 +392,12 @@ export const drawGridSchema = toolSchema<DrawGridParams>(
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<DrawGridParams>(
{
title: "groups.grid",
cols: 2,
fields: ["cols", "rows", "lineWidth", "color", "transparentBg"],
fields: ["cols", "rows", "lineWidth", "color", "bgOpacity"],
},
],
},
@@ -413,15 +421,7 @@ const drawGridTool: Tool<DrawGridParams> = {
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<PlaceholderParams> = {
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<SortColorsParams> = {
interface TextToPngParams {
text: string;
style: FontStyle;
transparentBg: boolean;
bgOpacity: number;
backgroundColor: string;
padding: number;
}
@@ -952,9 +952,12 @@ export const textToPngSchema = toolSchema<TextToPngParams>(
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<TextToPngParams>(
{ 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<TextToPngParams> = {
bold: p.style.bold,
color: p.style.color,
backgroundColor: p.backgroundColor,
transparentBg: p.transparentBg,
bgOpacity: p.bgOpacity,
padding: p.padding,
}),
),
+21 -10
View File
@@ -65,7 +65,7 @@ const addBorder: Tool<AddBorderParams> = {
interface FitOnBackgroundParams {
size: Dimension;
transparent: boolean;
opacity: number;
color: string;
}
@@ -79,9 +79,12 @@ export const fitOnBackgroundSchema = toolSchema<FitOnBackgroundParams>(
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<FitOnBackgroundParams>(
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<FitOnBackgroundParams> = {
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<FlipParams> = {
interface AddPaddingParams {
padding: number;
transparent: boolean;
opacity: number;
color: string;
}
@@ -344,14 +348,20 @@ export const addPaddingSchema = toolSchema<AddPaddingParams>(
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<AddPaddingParams> = {
p.padding,
p.padding,
p.padding,
p.transparent ? undefined : p.color,
p.color,
Math.round((Math.trunc(p.opacity) / 100) * 255),
),
),
};