Files
pihkaal-me/app/utils/canvas.ts

248 lines
5.7 KiB
TypeScript

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