2025-01-06 00:12:39 -05:00
|
|
|
import { TYPE_ACTION } from "./types.ts";
|
2024-12-29 23:00:38 -05:00
|
|
|
import { DominionCard } from "./types.ts";
|
2023-12-27 11:37:37 -08:00
|
|
|
|
2024-12-31 11:53:45 -05:00
|
|
|
const imageCache: Record<string, HTMLImageElement> = {};
|
|
|
|
export const loadImage = (
|
|
|
|
key: string,
|
|
|
|
src: string
|
|
|
|
): Promise<HTMLImageElement> => {
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
if (key in imageCache && imageCache[key]) {
|
|
|
|
resolve(imageCache[key]);
|
|
|
|
}
|
|
|
|
const img = new Image();
|
|
|
|
img.onload = () => {
|
|
|
|
imageCache[key] = img;
|
|
|
|
resolve(img);
|
|
|
|
};
|
2025-01-04 22:32:17 -05:00
|
|
|
img.onerror = (e) => {
|
|
|
|
console.log("err", e);
|
|
|
|
};
|
2024-12-31 11:53:45 -05:00
|
|
|
img.src = src;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const imageList = [
|
|
|
|
{
|
2025-01-04 22:32:17 -05:00
|
|
|
key: "card-color-1",
|
|
|
|
src: "/static/assets/CardColorOne.png",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "card-brown",
|
|
|
|
src: "/static/assets/CardBrown.png",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "card-gray",
|
|
|
|
src: "/static/assets/CardGray.png",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "card-description-focus",
|
|
|
|
src: "/static/assets/DescriptionFocus.png",
|
2024-12-31 11:53:45 -05:00
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2025-01-04 22:32:17 -05:00
|
|
|
export const loadImages = async () => {
|
|
|
|
for (const imageInfo of imageList) {
|
|
|
|
const { key, src } = imageInfo;
|
|
|
|
await loadImage(key, src);
|
|
|
|
}
|
|
|
|
};
|
2024-12-31 11:53:45 -05:00
|
|
|
|
|
|
|
export const getImage = (key: string) => {
|
|
|
|
const image = imageCache[key];
|
|
|
|
if (!image) {
|
|
|
|
throw Error(`Tried to get an invalid image ${key}`);
|
|
|
|
}
|
|
|
|
return image;
|
|
|
|
};
|
|
|
|
|
2025-01-04 22:32:17 -05:00
|
|
|
export const colorImage = (
|
|
|
|
image: HTMLImageElement,
|
2025-01-06 00:12:39 -05:00
|
|
|
color?: string
|
2025-01-04 22:32:17 -05:00
|
|
|
): HTMLCanvasElement => {
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
canvas.width = image.width;
|
|
|
|
canvas.height = image.height;
|
|
|
|
const context = canvas.getContext("2d")!;
|
|
|
|
context.save();
|
|
|
|
context.drawImage(image, 0, 0);
|
|
|
|
context.globalCompositeOperation = "multiply";
|
2025-01-06 00:12:39 -05:00
|
|
|
context.fillStyle = color ?? "white";
|
2025-01-04 22:32:17 -05:00
|
|
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
context.globalCompositeOperation = "destination-atop"; // restore transparency
|
|
|
|
context.drawImage(image, 0, 0);
|
|
|
|
context.restore();
|
|
|
|
return canvas;
|
|
|
|
};
|
|
|
|
|
2024-12-29 23:00:38 -05:00
|
|
|
export const drawCard = (
|
|
|
|
context: CanvasRenderingContext2D,
|
|
|
|
card: DominionCard
|
|
|
|
): Promise<void> => {
|
2023-12-27 11:37:37 -08:00
|
|
|
if (card.orientation === "card") {
|
2024-12-29 23:00:38 -05:00
|
|
|
return drawStandardCard(context, card);
|
2023-12-27 11:37:37 -08:00
|
|
|
} else {
|
2024-12-29 23:00:38 -05:00
|
|
|
return drawLandscapeCard(context, card);
|
2023-12-27 11:37:37 -08:00
|
|
|
}
|
2024-12-29 23:00:38 -05:00
|
|
|
};
|
2023-12-27 11:37:37 -08:00
|
|
|
|
2025-01-05 23:55:22 -05:00
|
|
|
const wrapText = (
|
|
|
|
context: CanvasRenderingContext2D,
|
|
|
|
text: string,
|
|
|
|
w: number
|
|
|
|
) => {
|
|
|
|
return text.split("\n").flatMap((paragraph) => {
|
|
|
|
const lines: string[] = [];
|
|
|
|
let words = 0;
|
|
|
|
let remainingText = paragraph.trim().replace(/\s+/g, " ");
|
|
|
|
let oldLine = "";
|
|
|
|
let countdown = 100;
|
|
|
|
while (remainingText.length > 0) {
|
|
|
|
countdown--;
|
|
|
|
if (countdown <= 0) {
|
|
|
|
console.log("CUT SHORT");
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
words++;
|
|
|
|
const newLine = remainingText.split(" ").slice(0, words).join(" ");
|
|
|
|
const metrics = context.measureText(newLine);
|
|
|
|
if (metrics.width > w) {
|
|
|
|
words = 0;
|
|
|
|
lines.push(oldLine);
|
|
|
|
remainingText = remainingText.slice(oldLine.length).trim();
|
|
|
|
} else if (newLine.length >= remainingText.length) {
|
|
|
|
words = 0;
|
|
|
|
lines.push(newLine);
|
|
|
|
remainingText = "";
|
|
|
|
}
|
|
|
|
oldLine = newLine;
|
|
|
|
}
|
|
|
|
if (!lines.length) {
|
|
|
|
return [""];
|
|
|
|
}
|
|
|
|
return lines;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const measureText = (
|
|
|
|
context: CanvasRenderingContext2D,
|
|
|
|
text: string,
|
|
|
|
maxWidth: number | undefined,
|
|
|
|
allowWrap: boolean | undefined
|
|
|
|
) => {
|
|
|
|
const measure = context.measureText(text);
|
|
|
|
const lineHeight = measure.emHeightAscent + measure.emHeightDescent;
|
|
|
|
if (!allowWrap || !maxWidth) {
|
|
|
|
return {
|
|
|
|
width: measure.width,
|
|
|
|
height: lineHeight,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
const lines = wrapText(context, text, maxWidth);
|
|
|
|
const width = Math.max(
|
|
|
|
...lines.map((line) => context.measureText(line).width)
|
|
|
|
);
|
|
|
|
const height = lines.length * lineHeight;
|
|
|
|
return { width, height };
|
|
|
|
};
|
|
|
|
|
|
|
|
const drawTextCentered = (
|
2025-01-05 10:12:27 -05:00
|
|
|
context: CanvasRenderingContext2D,
|
|
|
|
text: string,
|
|
|
|
x: number,
|
|
|
|
y: number,
|
|
|
|
options?: {
|
2025-01-05 23:55:22 -05:00
|
|
|
defaultSize?: number;
|
2025-01-05 10:12:27 -05:00
|
|
|
maxWidth?: number;
|
|
|
|
maxHeight?: number;
|
|
|
|
allowWrap?: boolean;
|
|
|
|
font?: string;
|
2025-01-05 23:55:22 -05:00
|
|
|
fontWeight?: "normal" | "bold";
|
2025-01-05 10:12:27 -05:00
|
|
|
color?: string;
|
|
|
|
}
|
|
|
|
) => {
|
2025-01-05 23:55:22 -05:00
|
|
|
const {
|
|
|
|
defaultSize = 75,
|
|
|
|
maxWidth,
|
|
|
|
maxHeight,
|
|
|
|
font = "DominionText",
|
|
|
|
fontWeight = "normal",
|
|
|
|
color,
|
|
|
|
allowWrap = false,
|
|
|
|
} = options ?? {};
|
|
|
|
context.save();
|
|
|
|
if (color) {
|
|
|
|
context.fillStyle = color;
|
|
|
|
}
|
|
|
|
let size = defaultSize;
|
|
|
|
context.font = `${fontWeight} ${size}pt ${font}`;
|
|
|
|
if (maxWidth) {
|
|
|
|
while (
|
|
|
|
measureText(context, text, maxWidth, allowWrap).width > maxWidth
|
|
|
|
) {
|
|
|
|
size -= 2;
|
|
|
|
context.font = `${fontWeight} ${size}pt ${font}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (maxHeight) {
|
|
|
|
while (
|
|
|
|
measureText(context, text, maxWidth, allowWrap).height > maxHeight
|
|
|
|
) {
|
|
|
|
size -= 1;
|
|
|
|
context.font = `${fontWeight} ${size}pt ${font}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const measure = context.measureText(text);
|
|
|
|
const lineHeight = measure.emHeightAscent + measure.emHeightDescent;
|
|
|
|
context.textAlign = "center";
|
|
|
|
context.textBaseline = "middle";
|
|
|
|
if (allowWrap && maxWidth) {
|
|
|
|
const lines = wrapText(context, text, maxWidth);
|
|
|
|
lines.forEach((line, i) => {
|
|
|
|
context.fillText(
|
|
|
|
line,
|
|
|
|
x,
|
|
|
|
y - (lineHeight * lines.length) / 2 + lineHeight * i,
|
|
|
|
maxWidth
|
|
|
|
);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
context.fillText(text, x, y, maxWidth);
|
|
|
|
}
|
|
|
|
context.restore();
|
2025-01-05 10:12:27 -05:00
|
|
|
};
|
|
|
|
|
2024-12-29 23:00:38 -05:00
|
|
|
const drawStandardCard = async (
|
|
|
|
context: CanvasRenderingContext2D,
|
|
|
|
card: DominionCard
|
|
|
|
): Promise<void> => {
|
|
|
|
const w = context.canvas.width;
|
|
|
|
const h = context.canvas.height;
|
|
|
|
context.save();
|
2025-01-04 22:32:17 -05:00
|
|
|
// Draw the image
|
|
|
|
// Draw the card base
|
2025-01-06 00:12:39 -05:00
|
|
|
const color = TYPE_ACTION.color?.value; // "#ffbc55";
|
2025-01-05 10:12:27 -05:00
|
|
|
context.drawImage(colorImage(getImage("card-color-1"), color), 0, 0);
|
2025-01-04 22:32:17 -05:00
|
|
|
context.drawImage(getImage("card-gray"), 0, 0);
|
|
|
|
context.drawImage(colorImage(getImage("card-brown"), "#ff9911"), 0, 0);
|
|
|
|
context.drawImage(getImage("card-description-focus"), 44, 1094);
|
|
|
|
// Draw the name
|
2025-01-05 23:55:22 -05:00
|
|
|
drawTextCentered(context, "Moonlit Scheme", w / 2, 220, {
|
|
|
|
maxWidth: 1100,
|
|
|
|
font: "DominionTitle",
|
|
|
|
fontWeight: "bold",
|
|
|
|
});
|
2025-01-04 22:32:17 -05:00
|
|
|
// Draw the description
|
2025-01-05 23:55:22 -05:00
|
|
|
drawTextCentered(
|
|
|
|
context,
|
|
|
|
"You may play an Action card from your hand.",
|
|
|
|
w / 2,
|
|
|
|
1520,
|
|
|
|
{
|
|
|
|
maxWidth: 1100,
|
|
|
|
font: "DominionText",
|
|
|
|
allowWrap: true,
|
|
|
|
defaultSize: 60,
|
|
|
|
}
|
|
|
|
);
|
2025-01-04 22:32:17 -05:00
|
|
|
// Draw the types
|
|
|
|
// Draw the cost
|
|
|
|
// Draw the preview
|
|
|
|
// Draw the icon
|
|
|
|
// Draw the credit
|
2024-12-29 23:00:38 -05:00
|
|
|
context.restore();
|
|
|
|
};
|
2023-12-27 11:37:37 -08:00
|
|
|
|
2024-12-29 23:00:38 -05:00
|
|
|
const drawLandscapeCard = async (
|
|
|
|
context: CanvasRenderingContext2D,
|
|
|
|
card: DominionCard
|
|
|
|
): Promise<void> => {
|
2023-12-27 11:37:37 -08:00
|
|
|
// TODO: everything
|
2024-12-29 23:00:38 -05:00
|
|
|
};
|