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 ); }