style: fix formatting via prettier

This commit is contained in:
2026-08-28 14:29:54 +05:00
parent 10278981ba
commit ada96b3534
124 changed files with 8211 additions and 5662 deletions
+94 -33
View File
@@ -1,33 +1,44 @@
import { ToolError } from './errors';
import type { PixelImage } from './types';
import { anchorOrigin, tileGrid, wrapText, type Position9 } from './textdraw';
import { ToolError } from "./errors";
import type { PixelImage } from "./types";
import { anchorOrigin, tileGrid, wrapText, type Position9 } from "./textdraw";
export type TextFont = 'sans' | 'serif' | 'mono';
export type TextFont = "sans" | "serif" | "mono";
const FONT_STACKS: Record<TextFont, string> = {
sans: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
serif: 'Georgia, "Times New Roman", serif',
mono: 'ui-monospace, "Cascadia Code", Consolas, monospace'
mono: 'ui-monospace, "Cascadia Code", Consolas, monospace',
};
function ctx2d(w: number, h: number): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
const canvas = document.createElement('canvas');
function ctx2d(
w: number,
h: number,
): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (!ctx) throw new ToolError('errors.noCanvasCtx');
const ctx = canvas.getContext("2d", { willReadFrequently: true });
if (!ctx) throw new ToolError("errors.noCanvasCtx");
return { canvas, ctx };
}
function toPixelImage(canvas: HTMLCanvasElement): PixelImage {
const ctx = canvas.getContext('2d');
if (!ctx) throw new ToolError('errors.noCanvasCtx');
const ctx = canvas.getContext("2d");
if (!ctx) throw new ToolError("errors.noCanvasCtx");
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
return { width: imageData.width, height: imageData.height, data: imageData.data };
return {
width: imageData.width,
height: imageData.height,
data: imageData.data,
};
}
export function fontString(size: number, font: TextFont, bold: boolean): string {
return `${bold ? '700 ' : '400 '}${size}px ${FONT_STACKS[font]}`;
export function fontString(
size: number,
font: TextFont,
bold: boolean,
): string {
return `${bold ? "700 " : "400 "}${size}px ${FONT_STACKS[font]}`;
}
export interface TextBlockOptions {
@@ -46,9 +57,16 @@ export interface TextBlockOptions {
}
/** Одна надпись (с автопереносом и опциональной плашкой) поверх изображения. */
export function drawTextBlock(img: PixelImage, o: TextBlockOptions): PixelImage {
export function drawTextBlock(
img: PixelImage,
o: TextBlockOptions,
): PixelImage {
const { canvas, ctx } = ctx2d(img.width, img.height);
ctx.putImageData(new ImageData(new Uint8ClampedArray(img.data), img.width, img.height), 0, 0);
ctx.putImageData(
new ImageData(new Uint8ClampedArray(img.data), img.width, img.height),
0,
0,
);
ctx.font = fontString(o.fontSize, o.font, o.bold);
const maxWidth = ((o.maxWidthPercent ?? 90) / 100) * img.width;
@@ -57,11 +75,18 @@ export function drawTextBlock(img: PixelImage, o: TextBlockOptions): PixelImage
const ascent = o.fontSize * 0.8;
const blockW = Math.min(
maxWidth,
lines.reduce((max, s) => Math.max(max, ctx.measureText(s).width), 0)
lines.reduce((max, s) => Math.max(max, ctx.measureText(s).width), 0),
);
const blockH = lines.length * lineH;
const origin = anchorOrigin(o.position, blockW, blockH, img.width, img.height, o.margin);
const origin = anchorOrigin(
o.position,
blockW,
blockH,
img.width,
img.height,
o.margin,
);
ctx.save();
ctx.globalAlpha = o.opacityPercent / 100;
@@ -77,7 +102,7 @@ export function drawTextBlock(img: PixelImage, o: TextBlockOptions): PixelImage
ctx.globalAlpha = o.opacityPercent / 100;
}
ctx.fillStyle = o.color;
ctx.textBaseline = 'alphabetic';
ctx.textBaseline = "alphabetic";
lines.forEach((line, i) => {
ctx.fillText(line, origin.x, origin.y + ascent + i * lineH);
});
@@ -86,7 +111,10 @@ export function drawTextBlock(img: PixelImage, o: TextBlockOptions): PixelImage
return toPixelImage(canvas);
}
export interface TileTextOptions extends Omit<TextBlockOptions, 'position' | 'margin' | 'angleDeg'> {
export interface TileTextOptions extends Omit<
TextBlockOptions,
"position" | "margin" | "angleDeg"
> {
stepX: number;
stepY: number;
angleDeg: number;
@@ -110,7 +138,10 @@ export function renderTextToImage(o: TextToImageOptions): PixelImage {
measure.font = fontString(o.fontSize, o.font, o.bold);
const maxWidth = o.maxTextWidth ?? 4000;
const lines = [o.text];
const textW = Math.min(maxWidth, Math.max(measure.measureText(o.text).width, 1));
const textW = Math.min(
maxWidth,
Math.max(measure.measureText(o.text).width, 1),
);
const lineH = o.fontSize * 1.25;
const w = Math.max(1, Math.ceil(textW + o.padding * 2));
@@ -123,7 +154,7 @@ export function renderTextToImage(o: TextToImageOptions): PixelImage {
}
ctx.font = fontString(o.fontSize, o.font, o.bold);
ctx.fillStyle = o.color;
ctx.textBaseline = 'top';
ctx.textBaseline = "top";
lines.forEach((line) => ctx.fillText(line, o.padding, o.padding));
return toPixelImage(canvas);
}
@@ -132,8 +163,8 @@ export function renderTextToImage(o: TextToImageOptions): PixelImage {
export function renderEmoji(symbol: string, size: number): PixelImage {
const { canvas, ctx } = ctx2d(size, size);
ctx.font = `${Math.round(size * 0.72)}px "Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(symbol, size / 2, size / 2 + size * 0.04);
return toPixelImage(canvas);
}
@@ -147,19 +178,37 @@ export interface ImageWatermarkOptions {
}
/** Картинка-знак поверх изображения: масштаб от ширины холста, позиция 3×3. */
export function drawImageWatermark(img: PixelImage, o: ImageWatermarkOptions): PixelImage {
export function drawImageWatermark(
img: PixelImage,
o: ImageWatermarkOptions,
): PixelImage {
const { canvas, ctx } = ctx2d(img.width, img.height);
ctx.putImageData(new ImageData(new Uint8ClampedArray(img.data), img.width, img.height), 0, 0);
ctx.putImageData(
new ImageData(new Uint8ClampedArray(img.data), img.width, img.height),
0,
0,
);
const w = Math.max(1, Math.round((img.width * o.scalePercent) / 100));
const h = Math.max(1, Math.round((w * o.mark.height) / o.mark.width));
const origin = anchorOrigin(o.position, w, h, img.width, img.height, o.margin);
const origin = anchorOrigin(
o.position,
w,
h,
img.width,
img.height,
o.margin,
);
const markCanvas = ctx2d(o.mark.width, o.mark.height);
markCanvas.ctx.putImageData(
new ImageData(new Uint8ClampedArray(o.mark.data), o.mark.width, o.mark.height),
new ImageData(
new Uint8ClampedArray(o.mark.data),
o.mark.width,
o.mark.height,
),
0,
0,
0
);
ctx.save();
@@ -173,21 +222,33 @@ export function drawImageWatermark(img: PixelImage, o: ImageWatermarkOptions): P
/** Повторяющаяся диагональная плитка текста на весь холст. */
export function drawTextTile(img: PixelImage, o: TileTextOptions): PixelImage {
const { canvas, ctx } = ctx2d(img.width, img.height);
ctx.putImageData(new ImageData(new Uint8ClampedArray(img.data), img.width, img.height), 0, 0);
ctx.putImageData(
new ImageData(new Uint8ClampedArray(img.data), img.width, img.height),
0,
0,
);
ctx.font = fontString(o.fontSize, o.font, o.bold);
const sample = o.text.length > 0 ? o.text : ' ';
const sample = o.text.length > 0 ? o.text : " ";
const blockW = ctx.measureText(sample).width;
const blockH = o.fontSize * 1.4;
const points = tileGrid(img.width, img.height, o.angleDeg, o.stepX, o.stepY, blockW, blockH);
const points = tileGrid(
img.width,
img.height,
o.angleDeg,
o.stepX,
o.stepY,
blockW,
blockH,
);
ctx.save();
ctx.translate(img.width / 2, img.height / 2);
ctx.rotate((o.angleDeg * Math.PI) / 180);
ctx.globalAlpha = o.opacityPercent / 100;
ctx.fillStyle = o.color;
ctx.textBaseline = 'middle';
ctx.textBaseline = "middle";
for (const p of points) {
ctx.fillText(sample, p.x - blockW / 2, p.y);
}