export const drawLine = ( ctx: CanvasRenderingContext2D, x0: number, y0: number, x1: number, y1: number, width: number, ) => { const dx = Math.abs(x1 - x0); const dy = Math.abs(y1 - y0); const sx = x0 < x1 ? 1 : -1; const sy = y0 < y1 ? 1 : -1; let err = dx - dy; const drawThickPixel = (x: number, y: number) => { const isVertical = dy > dx; if (width === 1) { ctx.fillRect(x, y, 1, 1); } else if (isVertical) { const offset = Math.floor((width - 1) / 2); ctx.fillRect(x - offset, y, width, 1); } else { const offset = Math.floor((width - 1) / 2); ctx.fillRect(x, y - offset, 1, width); } }; while (true) { drawThickPixel(x0, y0); if (x0 === x1 && y0 === y1) break; const e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } }; 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 + (ctx.font === "10px NDS10" ? 9 : 7), ); }; 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; i < lines.length; i += 1, y += lineHeight) { fillTextHCentered(ctx, lines[i]!, x, 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.font = "10px NDS10"; ctx.fillStyle = "#494118"; fillTextHCentered(ctx, text, 1, 4 + 9, width); ctx.restore(); };