use dominiontext framework

This commit is contained in:
Dylan Pizzo 2025-01-06 21:12:28 -05:00
parent 8e7bcc185c
commit 4e79fd38a1
2 changed files with 173 additions and 173 deletions

View File

@ -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<PieceMeasure>;
renderPiece: (
context: CanvasRenderingContext2D,
piece: Piece,
x: number,
y: number
) => PromiseOr<void>;
};
type PieceDef<T extends Piece["type"], M extends PieceMeasure> = {
type: T;
measure(
context: CanvasRenderingContext2D,
piece: Piece & { type: T }
piece: Piece & { type: T },
tools: PieceTools
): PromiseOr<M>;
render(
context: CanvasRenderingContext2D,
piece: Piece & { type: T },
x: number,
y: number,
measure: NoInfer<M>
measure: NoInfer<M>,
tools: PieceTools
): PromiseOr<void>;
};
@ -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;
};

View File

@ -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