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 = {}; export const loadImage = ( src: string, key?: string ): Promise => { 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 => { if (card.orientation === "card") { return drawStandardCard(context, card); } else { return drawLandscapeCard(context, card); } }; const _rgbCache: Record = {}; 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 => { 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; size = 78; context.font = `${size}pt DominionTitle`; while ( (await measureDominionText(context, parse(card.title))).width > 1050 ) { 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 > 650 ) { 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 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 => { // TODO: everything };