2025-01-08 19:08:40 -08:00
|
|
|
import { parseColor } from "./colorhelper.ts";
|
2025-01-06 23:01:01 -05:00
|
|
|
import {
|
|
|
|
measureDominionText,
|
|
|
|
parse,
|
|
|
|
renderDominionText,
|
|
|
|
} from "./dominiontext.ts";
|
2025-01-06 23:34:41 -05:00
|
|
|
import { DominionCardType, 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 = (
|
2025-01-06 23:34:41 -05:00
|
|
|
src: string,
|
|
|
|
key?: string
|
|
|
|
): Promise<HTMLImageElement | null> => {
|
2024-12-31 11:53:45 -05:00
|
|
|
return new Promise((resolve) => {
|
2025-01-06 23:34:41 -05:00
|
|
|
if (key && key in imageCache && imageCache[key]) {
|
2024-12-31 11:53:45 -05:00
|
|
|
resolve(imageCache[key]);
|
|
|
|
}
|
|
|
|
const img = new Image();
|
|
|
|
img.onload = () => {
|
2025-01-06 23:34:41 -05:00
|
|
|
if (key) {
|
|
|
|
imageCache[key] = img;
|
|
|
|
}
|
2024-12-31 11:53:45 -05:00
|
|
|
resolve(img);
|
|
|
|
};
|
2025-01-04 22:32:17 -05:00
|
|
|
img.onerror = (e) => {
|
|
|
|
console.log("err", e);
|
2025-01-06 23:34:41 -05:00
|
|
|
resolve(null);
|
2025-01-04 22:32:17 -05:00
|
|
|
};
|
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",
|
|
|
|
},
|
2025-01-07 20:18:42 -08:00
|
|
|
{
|
|
|
|
key: "card-color-2",
|
|
|
|
src: "/static/assets/CardColorTwo.png",
|
|
|
|
},
|
2025-01-08 19:08:40 -08:00
|
|
|
{
|
|
|
|
key: "card-color-2-night",
|
|
|
|
src: "/static/assets/CardColorTwoNight.png",
|
|
|
|
},
|
2025-01-04 22:32:17 -05:00
|
|
|
{
|
|
|
|
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-06 21:12:28 -05:00
|
|
|
{
|
|
|
|
key: "coin",
|
|
|
|
src: "/static/assets/Coin.png",
|
|
|
|
},
|
2025-01-06 21:06:13 -08:00
|
|
|
{
|
|
|
|
key: "debt",
|
|
|
|
src: "/static/assets/Debt.png",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "potion",
|
|
|
|
src: "/static/assets/Potion.png",
|
|
|
|
},
|
2025-01-07 08:10:47 -08:00
|
|
|
{
|
|
|
|
key: "vp",
|
|
|
|
src: "/static/assets/VP.png",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "vp-token",
|
|
|
|
src: "/static/assets/VP-Token.png",
|
|
|
|
},
|
2025-01-07 22:17:05 -08:00
|
|
|
{
|
|
|
|
key: "sun",
|
|
|
|
src: "/static/assets/Sun.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;
|
2025-01-06 23:34:41 -05:00
|
|
|
await loadImage(src, key);
|
2025-01-04 22:32:17 -05:00
|
|
|
}
|
|
|
|
};
|
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-07 23:07:20 -08:00
|
|
|
export const loadFonts = async () => {
|
|
|
|
const titleFont = new FontFace(
|
|
|
|
"DominionTitle",
|
|
|
|
`local("Trajan Pro Bold"), local("TrajanPro-Bold"), local('Trajan Pro'),
|
|
|
|
url('https://fonts.cdnfonts.com/s/14928/TrajanPro-Bold.woff') format('woff'),
|
|
|
|
url('https://shemitz.net/static/dominion3/Trajan%20Pro%20Bold.ttf') format('truetype'),
|
|
|
|
url('https://dominion.games/fonts/TrajanPro-Bold.otf') format('opentype'),
|
|
|
|
local("Trajan"),
|
|
|
|
local("Optimus Princeps"),
|
|
|
|
url(https://fonts.gstatic.com/s/cinzel/v8/8vIJ7ww63mVu7gt79mT7PkRXMw.woff2) format('woff2')`
|
|
|
|
);
|
|
|
|
|
|
|
|
const specialFont = new FontFace(
|
|
|
|
"DominionSpecial",
|
|
|
|
`local("Minion Std Black"), local("MinionStd-Black"), local("Minion Std"), local('Minion Pro'),
|
|
|
|
url('https://fonts.cdnfonts.com/s/13260/MinionPro-Regular.woff') format('woff'),
|
|
|
|
url('https://shemitz.net/static/dominion3/MinionStd-Black.otf') format('opentype'),
|
|
|
|
local("Optimus Princeps"),
|
|
|
|
url(https://fonts.gstatic.com/s/cinzel/v8/8vIJ7ww63mVu7gt79mT7PkRXMw.woff2) format('woff2')`
|
|
|
|
);
|
|
|
|
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
(document.fonts as any).add(titleFont);
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
(document.fonts as any).add(specialFont);
|
|
|
|
|
|
|
|
await Promise.all([titleFont.load(), specialFont.load()]);
|
|
|
|
};
|
|
|
|
|
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-08 19:08:40 -08:00
|
|
|
const _rgbCache: Record<string, { r: number; g: number; b: number }> = {};
|
|
|
|
const getColorRgb = (c: string): { r: number; g: number; b: number } => {
|
|
|
|
const { rgb } = parseColor(c);
|
|
|
|
const [r, g, b] = rgb;
|
|
|
|
return { r, g, b };
|
|
|
|
// if (c in _rgbCache) {
|
|
|
|
// return _rgbCache[c]!;
|
|
|
|
// }
|
|
|
|
// const canvas = document.createElement("canvas");
|
|
|
|
// canvas.width = 10;
|
|
|
|
// canvas.height = 10;
|
|
|
|
// const context = canvas.getContext("2d")!;
|
|
|
|
// context.fillRect(0, 0, 10, 10);
|
|
|
|
// const data = context.getImageData(5, 5, 1, 1).data;
|
|
|
|
// console.log(data);
|
|
|
|
// const [r, g, b] = data;
|
|
|
|
// const rgb = { r: r!, g: g!, b: b! };
|
|
|
|
// _rgbCache[c] = rgb;
|
|
|
|
// return rgb;
|
|
|
|
};
|
|
|
|
|
|
|
|
const getTextColorForBackground = (c: string): string => {
|
|
|
|
// return "black";
|
|
|
|
const { r, g, b } = getColorRgb(c);
|
|
|
|
const avg = (r + g + b) / 3 / 255;
|
|
|
|
console.log([r, g, b], avg);
|
|
|
|
return avg > 0.5 ? "black" : "white";
|
|
|
|
};
|
|
|
|
|
2025-01-07 20:18:42 -08:00
|
|
|
const getColors = (
|
|
|
|
types: DominionCardType[]
|
2025-01-08 19:08:40 -08:00
|
|
|
): {
|
|
|
|
primary: string;
|
|
|
|
secondary: string | null;
|
|
|
|
description: string | null;
|
|
|
|
descriptionText: string;
|
|
|
|
titleText: string;
|
|
|
|
} => {
|
|
|
|
const descriptionType =
|
|
|
|
types.find((t) => t.color?.onConflictDescriptionOnly) ?? null;
|
2025-01-06 23:01:01 -05:00
|
|
|
const byPriority = [...types]
|
2025-01-08 19:08:40 -08:00
|
|
|
.filter((type) => type.color && type !== descriptionType)
|
2025-01-06 23:01:01 -05:00
|
|
|
.sort((a, b) => b.color!.priority - a.color!.priority);
|
2025-01-07 20:18:42 -08:00
|
|
|
const priority1 = byPriority[0]!;
|
2025-01-08 19:08:40 -08:00
|
|
|
let primaryType: DominionCardType | null = priority1 ?? null;
|
|
|
|
let secondaryType = byPriority[1] ?? null;
|
2025-01-07 20:18:42 -08:00
|
|
|
if (priority1 === TYPE_ACTION) {
|
2025-01-06 23:01:01 -05:00
|
|
|
const overriders = byPriority.filter((t) => t.color!.overridesAction);
|
|
|
|
if (overriders.length) {
|
2025-01-08 19:08:40 -08:00
|
|
|
primaryType = overriders[0] ?? null;
|
2025-01-07 20:18:42 -08:00
|
|
|
}
|
2025-01-08 19:08:40 -08:00
|
|
|
if (primaryType === secondaryType) {
|
|
|
|
secondaryType = byPriority[2] ?? null;
|
2025-01-06 23:01:01 -05:00
|
|
|
}
|
|
|
|
}
|
2025-01-08 19:08:40 -08:00
|
|
|
primaryType = primaryType ?? descriptionType;
|
|
|
|
const primary = primaryType?.color?.value ?? "white";
|
|
|
|
const secondary = secondaryType?.color?.value ?? null;
|
|
|
|
const description = descriptionType?.color?.value ?? null;
|
|
|
|
const descriptionText = getTextColorForBackground(description ?? primary);
|
|
|
|
const titleText = getTextColorForBackground(primary);
|
|
|
|
return {
|
|
|
|
primary,
|
|
|
|
secondary,
|
|
|
|
description,
|
|
|
|
descriptionText,
|
|
|
|
titleText,
|
|
|
|
};
|
2025-01-06 23:01:01 -05:00
|
|
|
};
|
|
|
|
|
2024-12-29 23:00:38 -05:00
|
|
|
const drawStandardCard = async (
|
|
|
|
context: CanvasRenderingContext2D,
|
2025-01-06 22:28:57 -05:00
|
|
|
card: DominionCard & { orientation: "card" }
|
2024-12-29 23:00:38 -05:00
|
|
|
): Promise<void> => {
|
|
|
|
const w = context.canvas.width;
|
2025-01-07 23:07:20 -08:00
|
|
|
// const h = context.canvas.height;
|
2025-01-06 21:06:13 -08:00
|
|
|
let size;
|
2024-12-29 23:00:38 -05:00
|
|
|
context.save();
|
2025-01-04 22:32:17 -05:00
|
|
|
// Draw the image
|
2025-01-06 23:34:41 -05:00
|
|
|
const image = await loadImage(card.image);
|
|
|
|
if (image) {
|
|
|
|
const cx = w / 2;
|
|
|
|
const cy = 704;
|
|
|
|
const windowHeight = 830;
|
|
|
|
const windowWidth = 1194;
|
|
|
|
const scale = Math.max(
|
|
|
|
windowHeight / image.height,
|
|
|
|
windowWidth / image.width
|
|
|
|
);
|
|
|
|
context.drawImage(
|
|
|
|
image,
|
|
|
|
cx - (scale * image.width) / 2,
|
|
|
|
cy - (scale * image.height) / 2,
|
|
|
|
scale * image.width,
|
|
|
|
scale * image.height
|
|
|
|
);
|
|
|
|
}
|
2025-01-04 22:32:17 -05:00
|
|
|
// Draw the card base
|
2025-01-07 20:18:42 -08:00
|
|
|
const colors = getColors(card.types); // "#ffbc55";
|
|
|
|
|
|
|
|
if (colors.secondary) {
|
|
|
|
context.drawImage(
|
|
|
|
colorImage(getImage("card-color-1"), colors.secondary),
|
|
|
|
0,
|
|
|
|
0
|
|
|
|
);
|
|
|
|
context.drawImage(
|
|
|
|
colorImage(getImage("card-color-2"), colors.primary),
|
|
|
|
0,
|
|
|
|
0
|
|
|
|
);
|
2025-01-08 19:08:40 -08:00
|
|
|
} else if (colors.description) {
|
|
|
|
context.drawImage(
|
|
|
|
colorImage(getImage("card-color-1"), colors.description),
|
|
|
|
0,
|
|
|
|
0
|
|
|
|
);
|
|
|
|
context.drawImage(
|
|
|
|
colorImage(getImage("card-color-2-night"), colors.primary),
|
|
|
|
0,
|
|
|
|
0
|
|
|
|
);
|
2025-01-07 20:18:42 -08:00
|
|
|
} else {
|
|
|
|
context.drawImage(
|
|
|
|
colorImage(getImage("card-color-1"), colors.primary),
|
|
|
|
0,
|
|
|
|
0
|
|
|
|
);
|
|
|
|
context.drawImage(getImage("card-description-focus"), 44, 1094);
|
|
|
|
}
|
2025-01-04 22:32:17 -05:00
|
|
|
context.drawImage(getImage("card-gray"), 0, 0);
|
|
|
|
context.drawImage(colorImage(getImage("card-brown"), "#ff9911"), 0, 0);
|
|
|
|
// Draw the name
|
2025-01-08 19:08:40 -08:00
|
|
|
context.fillStyle = colors.titleText;
|
2025-01-08 19:19:21 -08:00
|
|
|
context.font = "90pt DominionText";
|
|
|
|
const previewMeasure = await measureDominionText(
|
|
|
|
context,
|
|
|
|
parse(card.preview ?? "")
|
|
|
|
);
|
2025-01-06 21:06:13 -08:00
|
|
|
size = 78;
|
|
|
|
context.font = `${size}pt DominionTitle`;
|
|
|
|
while (
|
2025-01-08 19:19:21 -08:00
|
|
|
(await measureDominionText(context, parse(card.title))).width >
|
|
|
|
1050 - previewMeasure.width * 1.5
|
2025-01-06 21:06:13 -08:00
|
|
|
) {
|
|
|
|
size -= 1;
|
|
|
|
context.font = `${size}pt DominionTitle`;
|
|
|
|
}
|
|
|
|
await renderDominionText(context, parse(card.title), w / 2, 220);
|
2025-01-04 22:32:17 -05:00
|
|
|
// Draw the description
|
2025-01-08 19:08:40 -08:00
|
|
|
context.fillStyle = colors.descriptionText;
|
|
|
|
size = 60;
|
|
|
|
context.font = `${size}pt DominionText`;
|
|
|
|
while (
|
|
|
|
(await measureDominionText(context, parse(card.description), 1000))
|
2025-01-08 19:14:01 -08:00
|
|
|
.height > 600
|
2025-01-08 19:08:40 -08:00
|
|
|
) {
|
|
|
|
size -= 1;
|
|
|
|
context.font = `${size}pt DominionText`;
|
|
|
|
}
|
2025-01-06 21:12:28 -05:00
|
|
|
await renderDominionText(
|
2025-01-05 23:55:22 -05:00
|
|
|
context,
|
2025-01-07 20:02:50 -08:00
|
|
|
parse(card.description, { isDescription: true }),
|
2025-01-05 23:55:22 -05:00
|
|
|
w / 2,
|
2025-01-06 23:51:51 -05:00
|
|
|
1490,
|
|
|
|
1000
|
2025-01-05 23:55:22 -05:00
|
|
|
);
|
2025-01-04 22:32:17 -05:00
|
|
|
// Draw the types
|
2025-01-08 19:08:40 -08:00
|
|
|
context.fillStyle = colors.titleText;
|
2025-01-06 21:06:13 -08:00
|
|
|
size = 65;
|
2025-01-06 23:01:01 -05:00
|
|
|
context.font = `${size}pt DominionTitle`;
|
|
|
|
while (
|
|
|
|
(
|
|
|
|
await measureDominionText(
|
|
|
|
context,
|
|
|
|
parse(card.types.map((t) => t.name).join(" - "))
|
|
|
|
)
|
|
|
|
).width > 800
|
|
|
|
) {
|
|
|
|
size -= 1;
|
|
|
|
context.font = `${size}pt DominionTitle`;
|
|
|
|
}
|
|
|
|
await renderDominionText(
|
|
|
|
context,
|
|
|
|
parse(card.types.map((t) => t.name).join(" - ")),
|
|
|
|
w / 2,
|
|
|
|
1930,
|
|
|
|
800
|
|
|
|
);
|
2025-01-04 22:32:17 -05:00
|
|
|
// Draw the cost
|
2025-01-08 19:08:40 -08:00
|
|
|
context.fillStyle = colors.titleText;
|
2025-01-06 22:13:53 -05:00
|
|
|
context.font = "90pt DominionText";
|
2025-01-06 21:06:13 -08:00
|
|
|
const costMeasure = await measureDominionText(context, parse(card.cost));
|
|
|
|
await renderDominionText(
|
|
|
|
context,
|
|
|
|
parse(card.cost),
|
|
|
|
130 + costMeasure.width / 2,
|
|
|
|
1940
|
|
|
|
);
|
2025-01-04 22:32:17 -05:00
|
|
|
// Draw the preview
|
2025-01-08 19:08:40 -08:00
|
|
|
context.fillStyle = colors.titleText;
|
2025-01-06 22:28:57 -05:00
|
|
|
if (card.preview) {
|
|
|
|
context.font = "90pt DominionText";
|
2025-01-06 21:06:13 -08:00
|
|
|
await renderDominionText(context, parse(card.preview), 200, 210);
|
|
|
|
await renderDominionText(context, parse(card.preview), w - 200, 210);
|
2025-01-06 22:28:57 -05:00
|
|
|
}
|
2025-01-08 19:19:21 -08:00
|
|
|
// Draw the expansion icon
|
2025-01-06 23:34:41 -05:00
|
|
|
// Draw the author credit
|
|
|
|
context.fillStyle = "white";
|
|
|
|
context.font = "31pt DominionText";
|
|
|
|
const authorMeasure = await measureDominionText(
|
|
|
|
context,
|
|
|
|
parse(card.author)
|
|
|
|
);
|
|
|
|
await renderDominionText(
|
|
|
|
context,
|
|
|
|
parse(card.author),
|
|
|
|
w - 150 - authorMeasure.width / 2,
|
2025-01-06 21:06:13 -08:00
|
|
|
2035
|
2025-01-06 23:34:41 -05:00
|
|
|
);
|
|
|
|
// Draw the artist credit
|
2025-01-08 19:08:40 -08:00
|
|
|
context.fillStyle = "white";
|
2025-01-06 23:34:41 -05:00
|
|
|
const artistMeasure = await measureDominionText(
|
|
|
|
context,
|
|
|
|
parse(card.artist)
|
|
|
|
);
|
|
|
|
await renderDominionText(
|
|
|
|
context,
|
|
|
|
parse(card.artist),
|
|
|
|
155 + artistMeasure.width / 2,
|
2025-01-06 21:06:13 -08:00
|
|
|
2035
|
2025-01-06 23:34:41 -05:00
|
|
|
);
|
|
|
|
// Restore the context
|
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 (
|
2025-01-07 23:07:20 -08:00
|
|
|
_context: CanvasRenderingContext2D,
|
|
|
|
_card: DominionCard & { orientation: "landscape" }
|
2024-12-29 23:00:38 -05:00
|
|
|
): Promise<void> => {
|
2023-12-27 11:37:37 -08:00
|
|
|
// TODO: everything
|
2024-12-29 23:00:38 -05:00
|
|
|
};
|