dominionator/src/draw.ts
2025-01-08 19:19:21 -08:00

401 lines
9.9 KiB
TypeScript

import { parseColor } from "./colorhelper.ts";
import {
measureDominionText,
parse,
renderDominionText,
} from "./dominiontext.ts";
import { DominionCardType, TYPE_ACTION } from "./types.ts";
import { DominionCard } from "./types.ts";
const imageCache: Record<string, HTMLImageElement> = {};
export const loadImage = (
src: string,
key?: string
): Promise<HTMLImageElement | null> => {
return new Promise((resolve) => {
if (key && key in imageCache && imageCache[key]) {
resolve(imageCache[key]);
}
const img = new Image();
img.onload = () => {
if (key) {
imageCache[key] = img;
}
resolve(img);
};
img.onerror = (e) => {
console.log("err", e);
resolve(null);
};
img.src = src;
});
};
const imageList = [
{
key: "card-color-1",
src: "/static/assets/CardColorOne.png",
},
{
key: "card-color-2",
src: "/static/assets/CardColorTwo.png",
},
{
key: "card-color-2-night",
src: "/static/assets/CardColorTwoNight.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",
},
{
key: "coin",
src: "/static/assets/Coin.png",
},
{
key: "debt",
src: "/static/assets/Debt.png",
},
{
key: "potion",
src: "/static/assets/Potion.png",
},
{
key: "vp",
src: "/static/assets/VP.png",
},
{
key: "vp-token",
src: "/static/assets/VP-Token.png",
},
{
key: "sun",
src: "/static/assets/Sun.png",
},
];
export const loadImages = async () => {
for (const imageInfo of imageList) {
const { key, src } = imageInfo;
await loadImage(src, key);
}
};
export const getImage = (key: string) => {
const image = imageCache[key];
if (!image) {
throw Error(`Tried to get an invalid image ${key}`);
}
return image;
};
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()]);
};
export const colorImage = (
image: HTMLImageElement,
color?: string
): 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";
context.fillStyle = color ?? "white";
context.fillRect(0, 0, canvas.width, canvas.height);
context.globalCompositeOperation = "destination-atop"; // restore transparency
context.drawImage(image, 0, 0);
context.restore();
return canvas;
};
export const drawCard = (
context: CanvasRenderingContext2D,
card: DominionCard
): Promise<void> => {
if (card.orientation === "card") {
return drawStandardCard(context, card);
} else {
return drawLandscapeCard(context, card);
}
};
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";
};
const getColors = (
types: DominionCardType[]
): {
primary: string;
secondary: string | null;
description: string | null;
descriptionText: string;
titleText: string;
} => {
const descriptionType =
types.find((t) => t.color?.onConflictDescriptionOnly) ?? null;
const byPriority = [...types]
.filter((type) => type.color && type !== descriptionType)
.sort((a, b) => b.color!.priority - a.color!.priority);
const priority1 = byPriority[0]!;
let primaryType: DominionCardType | null = priority1 ?? null;
let secondaryType = byPriority[1] ?? null;
if (priority1 === TYPE_ACTION) {
const overriders = byPriority.filter((t) => t.color!.overridesAction);
if (overriders.length) {
primaryType = overriders[0] ?? null;
}
if (primaryType === secondaryType) {
secondaryType = byPriority[2] ?? null;
}
}
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,
};
};
const drawStandardCard = async (
context: CanvasRenderingContext2D,
card: DominionCard & { orientation: "card" }
): Promise<void> => {
const w = context.canvas.width;
// const h = context.canvas.height;
let size;
context.save();
// Draw the image
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
);
}
// Draw the card base
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
);
} 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
);
} else {
context.drawImage(
colorImage(getImage("card-color-1"), colors.primary),
0,
0
);
context.drawImage(getImage("card-description-focus"), 44, 1094);
}
context.drawImage(getImage("card-gray"), 0, 0);
context.drawImage(colorImage(getImage("card-brown"), "#ff9911"), 0, 0);
// Draw the name
context.fillStyle = colors.titleText;
context.font = "90pt DominionText";
const previewMeasure = await measureDominionText(
context,
parse(card.preview ?? "")
);
size = 78;
context.font = `${size}pt DominionTitle`;
while (
(await measureDominionText(context, parse(card.title))).width >
1050 - previewMeasure.width * 1.5
) {
size -= 1;
context.font = `${size}pt DominionTitle`;
}
await renderDominionText(context, parse(card.title), w / 2, 220);
// Draw the description
context.fillStyle = colors.descriptionText;
size = 60;
context.font = `${size}pt DominionText`;
while (
(await measureDominionText(context, parse(card.description), 1000))
.height > 600
) {
size -= 1;
context.font = `${size}pt DominionText`;
}
await renderDominionText(
context,
parse(card.description, { isDescription: true }),
w / 2,
1490,
1000
);
// Draw the types
context.fillStyle = colors.titleText;
size = 65;
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
);
// Draw the cost
context.fillStyle = colors.titleText;
context.font = "90pt DominionText";
const costMeasure = await measureDominionText(context, parse(card.cost));
await renderDominionText(
context,
parse(card.cost),
130 + costMeasure.width / 2,
1940
);
// Draw the preview
context.fillStyle = colors.titleText;
if (card.preview) {
context.font = "90pt DominionText";
await renderDominionText(context, parse(card.preview), 200, 210);
await renderDominionText(context, parse(card.preview), w - 200, 210);
}
// Draw the expansion icon
// 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,
2035
);
// Draw the artist credit
context.fillStyle = "white";
const artistMeasure = await measureDominionText(
context,
parse(card.artist)
);
await renderDominionText(
context,
parse(card.artist),
155 + artistMeasure.width / 2,
2035
);
// Restore the context
context.restore();
};
const drawLandscapeCard = async (
_context: CanvasRenderingContext2D,
_card: DominionCard & { orientation: "landscape" }
): Promise<void> => {
// TODO: everything
};