Files
kk_card_gen/lib/draw-id.ts
T
2026-09-13 14:58:27 +05:30

209 lines
4.9 KiB
TypeScript

import { buildCanvasFont, hexToRgba, type IdDrawStyle } from "./id-config";
export type TextMetricsLike = {
width: number;
actualBoundingBoxAscent?: number;
actualBoundingBoxDescent?: number;
};
export type Canvas2D = {
font: string;
fillStyle: string | CanvasGradient | CanvasPattern;
strokeStyle: string | CanvasGradient | CanvasPattern;
lineWidth: number;
textAlign: CanvasTextAlign;
textBaseline: CanvasTextBaseline;
shadowColor: string;
shadowBlur: number;
shadowOffsetX: number;
shadowOffsetY: number;
lineJoin: CanvasLineJoin;
miterLimit: number;
fillText(text: string, x: number, y: number): void;
strokeText(text: string, x: number, y: number): void;
measureText(text: string): TextMetricsLike;
save(): void;
restore(): void;
setLineDash(segments: number[]): void;
strokeRect(x: number, y: number, w: number, h: number): void;
};
export type TextBox = {
left: number;
top: number;
width: number;
height: number;
};
export type GlyphMetrics = {
width: number;
ascent: number;
descent: number;
height: number;
};
export function positionFromPercent(
xPercent: number,
yPercent: number,
imageWidth: number,
imageHeight: number,
): { x: number; y: number } {
return {
x: (xPercent / 100) * imageWidth,
y: (yPercent / 100) * imageHeight,
};
}
function applyFont(ctx: Canvas2D, style: IdDrawStyle): void {
ctx.font = buildCanvasFont(style);
ctx.textAlign = style.align;
ctx.textBaseline = "alphabetic";
}
export function measureGlyphs(
ctx: Canvas2D,
text: string,
style: IdDrawStyle,
): GlyphMetrics {
applyFont(ctx, style);
const metrics = ctx.measureText(text);
const ascent = metrics.actualBoundingBoxAscent ?? style.fontSize * 0.8;
const descent = metrics.actualBoundingBoxDescent ?? style.fontSize * 0.2;
return {
width: metrics.width,
ascent,
descent,
height: ascent + descent,
};
}
export function alphabeticBaselineY(
visualCenterY: number,
glyphs: GlyphMetrics,
): number {
return visualCenterY + (glyphs.ascent - glyphs.descent) / 2;
}
export function applyIdStyle(ctx: Canvas2D, style: IdDrawStyle): void {
applyFont(ctx, style);
ctx.fillStyle = style.color;
}
function applyShadow(ctx: Canvas2D, style: IdDrawStyle): void {
if (!style.shadowEnabled) {
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
return;
}
ctx.shadowColor = hexToRgba(style.shadowColor, style.shadowOpacity);
ctx.shadowBlur = style.shadowBlur;
ctx.shadowOffsetX = style.shadowOffsetX;
ctx.shadowOffsetY = style.shadowOffsetY;
}
export function drawIdText(
ctx: Canvas2D,
text: string,
imageWidth: number,
imageHeight: number,
style: IdDrawStyle,
): void {
const { x, y } = positionFromPercent(
style.xPercent,
style.yPercent,
imageWidth,
imageHeight,
);
const glyphs = measureGlyphs(ctx, text, style);
const baselineY = alphabeticBaselineY(y, glyphs);
ctx.save();
applyIdStyle(ctx, style);
applyShadow(ctx, style);
if (style.outlineEnabled && style.outlineWidth > 0) {
ctx.strokeStyle = hexToRgba(style.outlineColor, style.outlineOpacity);
ctx.lineWidth = style.outlineWidth;
ctx.lineJoin = "round";
ctx.miterLimit = 2;
ctx.strokeText(text, x, baselineY);
ctx.shadowColor = "transparent";
}
ctx.fillText(text, x, baselineY);
ctx.restore();
}
export function measureIdTextBox(
ctx: Canvas2D,
text: string,
imageWidth: number,
imageHeight: number,
style: IdDrawStyle,
): TextBox {
const glyphs = measureGlyphs(ctx, text, style);
const { x, y } = positionFromPercent(
style.xPercent,
style.yPercent,
imageWidth,
imageHeight,
);
let left = x;
if (style.align === "center") left = x - glyphs.width / 2;
if (style.align === "right") left = x - glyphs.width;
const outlinePad =
style.outlineEnabled && style.outlineWidth > 0
? style.outlineWidth / 2
: 0;
return {
left: left - outlinePad,
top: y - glyphs.height / 2 - outlinePad,
width: glyphs.width + outlinePad * 2,
height: glyphs.height + outlinePad * 2,
};
}
export function drawIdSelection(
ctx: Canvas2D,
box: TextBox,
imageWidth: number,
): void {
const pad = Math.max(6, imageWidth / 400);
ctx.save();
ctx.shadowColor = "transparent";
ctx.strokeStyle = "rgba(99, 102, 241, 0.95)";
ctx.lineWidth = Math.max(1, imageWidth / 700);
ctx.setLineDash([
Math.max(4, imageWidth / 180),
Math.max(3, imageWidth / 220),
]);
ctx.strokeRect(
box.left - pad,
box.top - pad,
box.width + pad * 2,
box.height + pad * 2,
);
ctx.restore();
}
export function hitTestTextBox(
box: TextBox,
x: number,
y: number,
imageWidth: number,
): boolean {
const pad = Math.max(10, imageWidth / 80);
return (
x >= box.left - pad &&
x <= box.left + box.width + pad &&
y >= box.top - pad &&
y <= box.top + box.height + pad
);
}