diff --git a/src/client/App.tsx b/src/client/App.tsx index 65caac1..70a0aac 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -1,6 +1,8 @@ +import { useState } from "react"; import { sampleCard } from "../sampleData.ts"; import { Card } from "./Card.tsx"; export const App = () => { - return
; + const [count, setCount] = useState(0); + return
; }; diff --git a/src/client/Card.tsx b/src/client/Card.tsx index 7b22ee7..e4eee4a 100644 --- a/src/client/Card.tsx +++ b/src/client/Card.tsx @@ -19,9 +19,8 @@ export const Card = (props: {card: DominionCard}) => { if (canvasElement) { const context = canvasElement.getContext("2d"); if (context) { - console.log("loading"); - await loadImages() - console.log("done loading"); + await loadImages(); + // await loadFonts(); drawCard(context, card); } } diff --git a/src/draw.ts b/src/draw.ts index 84fa6fb..3c20c52 100644 --- a/src/draw.ts +++ b/src/draw.ts @@ -5,8 +5,6 @@ export const loadImage = ( key: string, src: string ): Promise => { - console.log("2", key); - console.log(src); return new Promise((resolve) => { if (key in imageCache && imageCache[key]) { resolve(imageCache[key]); @@ -45,7 +43,6 @@ const imageList = [ export const loadImages = async () => { for (const imageInfo of imageList) { const { key, src } = imageInfo; - console.log(key); await loadImage(key, src); } }; @@ -88,22 +85,130 @@ export const drawCard = ( } }; -const drawText = ( +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 { maxWidth = undefined } = options ?? {}; - context.font = "bold 48px serif"; - context.fillText(text, x, y, maxWidth); + 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 ( @@ -121,8 +226,24 @@ const drawStandardCard = async ( context.drawImage(colorImage(getImage("card-brown"), "#ff9911"), 0, 0); context.drawImage(getImage("card-description-focus"), 44, 1094); // Draw the name - drawText(context, card.title, 400, 500); + 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 diff --git a/src/richtext.ts b/src/richtext.ts new file mode 100644 index 0000000..ca9edcc --- /dev/null +++ b/src/richtext.ts @@ -0,0 +1,25 @@ +// type RichnessNodeDefinition = { +// type: N["type"] +// measure(context: CanvasRenderingContext2D, node: N): Promise; +// render( +// context: CanvasRenderingContext2D, +// node: N, +// x: number, +// y: number +// ): Promise; +// }; + +// type Richness = {[K in N["type"]]: RichnessNodeDefinition} + +// const drawRichText = ( +// context: CanvasRenderingContext2D, +// richness: Richness, +// richText: N[], +// x: number, +// y: number, +// maxWidth: number, +// ) => { +// context.save(); +// const +// context.restore(); +// };