export const fillTextCentered = ( ctx: CanvasRenderingContext2D, text: string, x: number, y: number, width: number, ): number => { const measure = ctx.measureText(text); const textX = Math.floor(x + width / 2 - measure.actualBoundingBoxRight / 2); const textY = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent; ctx.fillText(text, textX, y + textY); return measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent + 1; }; export const fillImageTextHCentered = ( ctx: CanvasRenderingContext2D, image: AtlasImage, text: string, x: number, y: number, width: number, gap: number, ) => { const { actualBoundingBoxRight: textWidth } = ctx.measureText(text); const totalWidth = textWidth + gap + image.rect.width; const groupX = Math.floor(x + width / 2 - totalWidth / 2); image.draw(ctx, groupX, y); ctx.fillText(text, groupX + gap + image.rect.width, y); }; export const fillTextHCentered = ( ctx: CanvasRenderingContext2D, text: string, x: number, y: number, width: number, ) => { const measure = ctx.measureText(text); const textX = Math.floor(x + width / 2 - measure.actualBoundingBoxRight / 2); ctx.fillText(text, textX, y); }; export const fillTextHCenteredMultiline = ( ctx: CanvasRenderingContext2D, text: string, x: number, y: number, width: number, lineHeight: number, ) => { const lines = text.split("\n"); for (let i = 0, y = 20; i < lines.length; i += 1, y += lineHeight) { fillTextHCentered(ctx, lines[i]!, 0, y, width); } }; export const CHECKBOX_SIZE = 7; export const CHECKBOX_TEXT_GAP = 5; export const drawCheckbox = ( ctx: CanvasRenderingContext2D, x: number, y: number, checked: boolean, ) => { ctx.fillRect(x, y, CHECKBOX_SIZE, 1); ctx.fillRect(x, y + CHECKBOX_SIZE - 1, CHECKBOX_SIZE, 1); ctx.fillRect(x, y + 1, 1, CHECKBOX_SIZE - 2); ctx.fillRect(x + CHECKBOX_SIZE - 1, y + 1, 1, CHECKBOX_SIZE - 2); if (checked) { for (let i = 2; i < CHECKBOX_SIZE - 2; i++) { ctx.fillRect(x + i, y + i, 1, 1); ctx.fillRect(x + CHECKBOX_SIZE - 1 - i, y + i, 1, 1); } } }; export const fillCirclePixelated = ( ctx: CanvasRenderingContext2D, cx: number, cy: number, radius: number, ) => { const r = Math.floor(radius); for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { if (dx * dx + dy * dy <= r * r) { ctx.fillRect(Math.floor(cx) + dx, Math.floor(cy) + dy, 1, 1); } } } }; export const strokeCirclePixelated = ( ctx: CanvasRenderingContext2D, cx: number, cy: number, radius: number, thickness: number = 2, ) => { const outerR = Math.floor(radius); const innerR = Math.floor(radius - thickness); for (let dy = -outerR; dy <= outerR; dy++) { for (let dx = -outerR; dx <= outerR; dx++) { const distSq = dx * dx + dy * dy; if (distSq <= outerR * outerR && distSq > innerR * innerR) { ctx.fillRect(Math.floor(cx) + dx, Math.floor(cy) + dy, 1, 1); } } } }; export const fillTextWordWrapped = ( ctx: CanvasRenderingContext2D, text: string, x: number, y: number, width: number, lineHeight?: number, ): number => { const words = text.split(" "); let line = ""; let currentY = y; let lineCount = 0; const height = lineHeight || ctx.measureText("M").actualBoundingBoxAscent + ctx.measureText("M").actualBoundingBoxDescent; currentY += height; for (let i = 0; i < words.length; i++) { const testLine = line + (line ? " " : "") + words[i]; const metrics = ctx.measureText(testLine); const testWidth = metrics.width; if (testWidth > width && line) { ctx.fillText(line, x, currentY); line = words[i]!; currentY += height; lineCount++; } else { line = testLine; } } if (line) { ctx.fillText(line, x, currentY); lineCount++; } return lineCount * height; }; export const drawButton = ( ctx: CanvasRenderingContext2D, text: string, x: number, y: number, width: number, pressed: boolean, ) => { const { assets } = useAssets((a) => a.images.common); const LEFT_WIDTH = assets.buttonLeft.rect.width; const RIGHT_WIDTH = assets.buttonRight.rect.width; const BODY_WIDTH = assets.buttonBody.rect.width; const bodySpace = width - LEFT_WIDTH - RIGHT_WIDTH; const bodyCount = Math.ceil(bodySpace / BODY_WIDTH); ctx.save(); ctx.translate(x, y); (pressed ? assets.buttonLeftPressed : assets.buttonLeft).draw(ctx, 0, 0); for (let i = 0; i < bodyCount; i++) { (pressed ? assets.buttonBodyPressed : assets.buttonBody).draw( ctx, LEFT_WIDTH + i * BODY_WIDTH, 0, ); } (pressed ? assets.buttonRightPressed : assets.buttonRight).draw( ctx, width - RIGHT_WIDTH, 0, ); ctx.textBaseline = "top"; ctx.font = "10px NDS10"; ctx.fillStyle = "#494118"; fillTextHCentered(ctx, text, 1, 4, width); ctx.restore(); };