use dominiontext framework
This commit is contained in:
parent
8e7bcc185c
commit
4e79fd38a1
@ -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;
|
||||
};
|
||||
|
158
src/draw.ts
158
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user