import { DominionCard } from "./types.ts"; const imageCache: Record = {}; export const loadImage = ( key: string, src: string ): Promise => { return new Promise((resolve) => { if (key in imageCache && imageCache[key]) { resolve(imageCache[key]); } const img = new Image(); img.onload = () => { imageCache[key] = img; resolve(img); }; img.onerror = (e) => { console.log("err", e); }; img.src = src; }); }; const imageList = [ { 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", }, ]; export const loadImages = async () => { for (const imageInfo of imageList) { const { key, src } = imageInfo; await loadImage(key, src); } }; export const getImage = (key: string) => { const image = imageCache[key]; if (!image) { throw Error(`Tried to get an invalid image ${key}`); } return image; }; 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; 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 => { if (card.orientation === "card") { return drawStandardCard(context, card); } else { return drawLandscapeCard(context, card); } }; 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 = ( context: CanvasRenderingContext2D, text: string, x: number, y: number, options?: { defaultSize?: number; maxWidth?: number; maxHeight?: number; allowWrap?: boolean; font?: string; fontWeight?: "normal" | "bold"; color?: string; } ) => { 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(); }; const drawStandardCard = async ( context: CanvasRenderingContext2D, card: DominionCard ): Promise => { const w = context.canvas.width; const h = context.canvas.height; context.save(); // Draw the image // Draw the card base const color = "#ffffff"; // "#ffbc55"; context.drawImage(colorImage(getImage("card-color-1"), color), 0, 0); 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 drawTextCentered(context, "Moonlit Scheme", w / 2, 220, { maxWidth: 1100, font: "DominionTitle", fontWeight: "bold", }); // Draw the description drawTextCentered( context, "You may play an Action card from your hand.", w / 2, 1520, { maxWidth: 1100, font: "DominionText", allowWrap: true, defaultSize: 60, } ); // Draw the types // Draw the cost // Draw the preview // Draw the icon // Draw the credit context.restore(); }; const drawLandscapeCard = async ( context: CanvasRenderingContext2D, card: DominionCard ): Promise => { // TODO: everything };