From 4e79fd38a117e7fbdb40593d12cc2d159e6ae07d Mon Sep 17 00:00:00 2001 From: Dylan Pizzo Date: Mon, 6 Jan 2025 21:12:28 -0500 Subject: [PATCH] use dominiontext framework --- src/dominiontext.ts | 188 ++++++++++++++++++++++++++++++++++++-------- src/draw.ts | 158 +++++-------------------------------- 2 files changed, 173 insertions(+), 173 deletions(-) diff --git a/src/dominiontext.ts b/src/dominiontext.ts index 2836cc2..a4d8f2b 100644 --- a/src/dominiontext.ts +++ b/src/dominiontext.ts @@ -1,4 +1,6 @@ -type Piece = +import { getImage } from "./draw.ts"; + +export type Piece = | { type: "text"; text: string; isBold?: boolean; isItalic?: boolean } | { type: "space" } | { type: "break" } @@ -13,18 +15,43 @@ type PieceMeasure = { descent: number; }; +type Line = { + pieces: { + piece: Piece; + xOffset: number; + }[]; + width: number; + ascent: number; + descent: number; +}; + +type PieceTools = { + measurePiece: ( + context: CanvasRenderingContext2D, + piece: Piece + ) => PromiseOr; + renderPiece: ( + context: CanvasRenderingContext2D, + piece: Piece, + x: number, + y: number + ) => PromiseOr; +}; + type PieceDef = { type: T; measure( context: CanvasRenderingContext2D, - piece: Piece & { type: T } + piece: Piece & { type: T }, + tools: PieceTools ): PromiseOr; render( context: CanvasRenderingContext2D, piece: Piece & { type: T }, x: number, y: number, - measure: NoInfer + measure: NoInfer, + tools: PieceTools ): PromiseOr; }; @@ -41,8 +68,8 @@ const textPiece = pieceDef({ return { type: "content", width: metrics.width, - ascent: metrics.emHeightAscent, - descent: metrics.emHeightDescent, + ascent: metrics.fontBoundingBoxAscent, + descent: metrics.fontBoundingBoxDescent, }; }, render(context, piece, x, y) { @@ -57,8 +84,8 @@ const spacePiece = pieceDef({ return { type: "space", width: metrics.width, - ascent: metrics.emHeightAscent, - descent: metrics.emHeightDescent, + ascent: metrics.fontBoundingBoxAscent, + descent: metrics.fontBoundingBoxDescent, }; }, render() {}, @@ -71,8 +98,8 @@ const breakPiece = pieceDef({ return { type: "break", width: 0, - ascent: metrics.emHeightAscent, - descent: metrics.emHeightDescent, + ascent: metrics.fontBoundingBoxAscent, + descent: metrics.fontBoundingBoxDescent, }; }, render() {}, @@ -82,33 +109,42 @@ const coinPiece = pieceDef({ type: "coin", measure(context, _piece) { const metrics = context.measureText(" "); + const height = + metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent; + const coinImage = getImage("coin"); return { type: "content", - width: metrics.emHeightAscent + metrics.emHeightDescent, - ascent: metrics.emHeightAscent, - descent: metrics.emHeightDescent, + width: coinImage.width * (height / coinImage.height), + ascent: metrics.fontBoundingBoxAscent, + descent: metrics.fontBoundingBoxDescent, }; }, render(context, piece, x, y, measure) { context.save(); - context.fillStyle = "yellow"; - context.fillRect( + // context.fillStyle = "yellow"; + const height = measure.ascent + measure.descent; + // context.fillRect(x, y - measure.ascent, measure.width, height); + context.drawImage( + getImage("coin"), x, y - measure.ascent, measure.width, - measure.ascent + measure.descent + height ); context.fillStyle = "black"; - context.fillText(piece.text, x, y); + context.textAlign = "center"; + context.fillText(piece.text, x + measure.width / 2, y); context.restore(); }, }); const pieceDefs = [textPiece, spacePiece, breakPiece, coinPiece]; +let tools: PieceTools = {} as any; + const measurePiece = (context: CanvasRenderingContext2D, piece: Piece) => { const def = pieceDefs.find((def) => def.type === piece.type)!; - return def.measure(context, piece as any); + return def.measure(context, piece as any, tools); }; const renderPiece = ( @@ -118,18 +154,12 @@ const renderPiece = ( y: number ) => { const def = pieceDefs.find((def) => def.type === piece.type)!; - const measure = def.measure(context, piece as any); - return def.render(context, piece as any, x, y, measure as any); + const measure = def.measure(context, piece as any, tools); + return def.render(context, piece as any, x, y, measure as any, tools); }; -// export const drawDominionText = ( -// context: CanvasRenderingContext2D, -// text: Piece[], -// x: number, -// y: number, -// w: number, -// h: number -// ) => {}; +tools.measurePiece = measurePiece; +tools.renderPiece = renderPiece; type DominionFont = { font: "text" | "title"; @@ -138,14 +168,106 @@ type DominionFont = { isItalic: boolean; }; -export const measureDominionText = ( +type PieceWithInfo = { + piece: Piece; + measure: PieceMeasure; +}; + +export const measureDominionText = async ( context: CanvasRenderingContext2D, pieces: Piece[], - font: DominionFont, maxWidth: number ) => { - const data = pieces.map((piece) => ({ - piece, - measure: measurePiece(context, piece), - })); + const data: PieceWithInfo[] = await Promise.all( + pieces.map(async (piece) => ({ + piece, + measure: await measurePiece(context, piece), + })) + ); + const lines: Line[] = [{ pieces: [], width: 0, ascent: 0, descent: 0 }]; + for (const pieceInfo of data) { + const line = lines[lines.length - 1]!; + if (pieceInfo.measure.type === "break") { + line.pieces.push({ piece: pieceInfo.piece, xOffset: line.width }); + line.width += pieceInfo.measure.width; + line.ascent = Math.max(line.ascent, pieceInfo.measure.ascent); + line.descent = Math.max(line.descent, pieceInfo.measure.descent); + lines.push({ pieces: [], width: 0, ascent: 0, descent: 0 }); + } else { + if (line.width + pieceInfo.measure.width > maxWidth) { + lines.push({ + pieces: [{ piece: pieceInfo.piece, xOffset: 0 }], + width: pieceInfo.measure.width, + ascent: pieceInfo.measure.ascent, + descent: pieceInfo.measure.descent, + }); + } else { + line.pieces.push({ + piece: pieceInfo.piece, + xOffset: line.width, + }); + line.width += pieceInfo.measure.width; + line.ascent = Math.max(line.ascent, pieceInfo.measure.ascent); + line.descent = Math.max( + line.descent, + pieceInfo.measure.descent + ); + } + } + } + return { + lines, + width: Math.max(...lines.map((line) => line.width)), + height: lines + .map((line) => line.ascent + line.descent) + .reduce((a, b) => a + b), + }; +}; + +export const renderDominionText = async ( + context: CanvasRenderingContext2D, + pieces: Piece[], + x: number, + y: number, + maxWidth: number +) => { + const { lines, height } = await measureDominionText( + context, + pieces, + maxWidth + ); + let yOffset = 0; + for (const line of lines) { + yOffset += line.ascent; + for (const { piece, xOffset } of line.pieces) { + await renderPiece( + context, + piece, + x - line.width / 2 + xOffset, + y - height / 2 + yOffset + ); + } + yOffset += line.descent; + } +}; + +export const parse = (text: string): Piece[] => { + const pieces: Piece[] = []; + for (let i = 0; i < text.length; i++) { + const char = text[i]; + if (char === " ") { + pieces.push({ type: "space" }); + } else if (char === "\n") { + pieces.push({ type: "break" }); + } else if (char === "$") { + const end = text.slice(i).match(/\$\d*/)![0].length; + pieces.push({ type: "coin", text: text.slice(i + 1, i + end) }); + i += end - 1; + } else { + const end = text.slice(i).match(/\S+/)![0].length; + pieces.push({ type: "text", text: text.slice(i, i + end) }); + i += end - 1; + } + } + return pieces; }; diff --git a/src/draw.ts b/src/draw.ts index 8088c0e..7fac90c 100644 --- a/src/draw.ts +++ b/src/draw.ts @@ -1,3 +1,4 @@ +import { parse, renderDominionText } from "./dominiontext.ts"; import { TYPE_ACTION } from "./types.ts"; import { DominionCard } from "./types.ts"; @@ -39,6 +40,10 @@ const imageList = [ key: "card-description-focus", src: "/static/assets/DescriptionFocus.png", }, + { + key: "coin", + src: "/static/assets/Coin.png", + }, ]; export const loadImages = async () => { @@ -86,132 +91,6 @@ export const drawCard = ( } }; -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(/ +/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 = 1.2 * (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 = 1.2 * (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 @@ -227,23 +106,22 @@ const drawStandardCard = async ( 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.font = "75pt DominionTitle"; + await renderDominionText( context, - "You may play an Action card from your hand costing up to \u202f◯\u202f.", + parse("Moonlit Scheme"), + w / 2, + 220, + 1100 + ); + // Draw the description + context.font = "60pt DominionText"; + await renderDominionText( + context, + parse("You may play an Action card from your hand costing up to $4."), w / 2, 1520, - { - maxWidth: 1100, - font: "DominionText", - allowWrap: true, - defaultSize: 60, - } + 1100 ); // Draw the types // Draw the cost