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: "text"; text: string; isBold?: boolean; isItalic?: boolean }
|
||||||
| { type: "space" }
|
| { type: "space" }
|
||||||
| { type: "break" }
|
| { type: "break" }
|
||||||
@ -13,18 +15,43 @@ type PieceMeasure = {
|
|||||||
descent: number;
|
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 PieceDef<T extends Piece["type"], M extends PieceMeasure> = {
|
||||||
type: T;
|
type: T;
|
||||||
measure(
|
measure(
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
piece: Piece & { type: T }
|
piece: Piece & { type: T },
|
||||||
|
tools: PieceTools
|
||||||
): PromiseOr<M>;
|
): PromiseOr<M>;
|
||||||
render(
|
render(
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
piece: Piece & { type: T },
|
piece: Piece & { type: T },
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
measure: NoInfer<M>
|
measure: NoInfer<M>,
|
||||||
|
tools: PieceTools
|
||||||
): PromiseOr<void>;
|
): PromiseOr<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,8 +68,8 @@ const textPiece = pieceDef({
|
|||||||
return {
|
return {
|
||||||
type: "content",
|
type: "content",
|
||||||
width: metrics.width,
|
width: metrics.width,
|
||||||
ascent: metrics.emHeightAscent,
|
ascent: metrics.fontBoundingBoxAscent,
|
||||||
descent: metrics.emHeightDescent,
|
descent: metrics.fontBoundingBoxDescent,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
render(context, piece, x, y) {
|
render(context, piece, x, y) {
|
||||||
@ -57,8 +84,8 @@ const spacePiece = pieceDef({
|
|||||||
return {
|
return {
|
||||||
type: "space",
|
type: "space",
|
||||||
width: metrics.width,
|
width: metrics.width,
|
||||||
ascent: metrics.emHeightAscent,
|
ascent: metrics.fontBoundingBoxAscent,
|
||||||
descent: metrics.emHeightDescent,
|
descent: metrics.fontBoundingBoxDescent,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
render() {},
|
render() {},
|
||||||
@ -71,8 +98,8 @@ const breakPiece = pieceDef({
|
|||||||
return {
|
return {
|
||||||
type: "break",
|
type: "break",
|
||||||
width: 0,
|
width: 0,
|
||||||
ascent: metrics.emHeightAscent,
|
ascent: metrics.fontBoundingBoxAscent,
|
||||||
descent: metrics.emHeightDescent,
|
descent: metrics.fontBoundingBoxDescent,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
render() {},
|
render() {},
|
||||||
@ -82,33 +109,42 @@ const coinPiece = pieceDef({
|
|||||||
type: "coin",
|
type: "coin",
|
||||||
measure(context, _piece) {
|
measure(context, _piece) {
|
||||||
const metrics = context.measureText(" ");
|
const metrics = context.measureText(" ");
|
||||||
|
const height =
|
||||||
|
metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
|
||||||
|
const coinImage = getImage("coin");
|
||||||
return {
|
return {
|
||||||
type: "content",
|
type: "content",
|
||||||
width: metrics.emHeightAscent + metrics.emHeightDescent,
|
width: coinImage.width * (height / coinImage.height),
|
||||||
ascent: metrics.emHeightAscent,
|
ascent: metrics.fontBoundingBoxAscent,
|
||||||
descent: metrics.emHeightDescent,
|
descent: metrics.fontBoundingBoxDescent,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
render(context, piece, x, y, measure) {
|
render(context, piece, x, y, measure) {
|
||||||
context.save();
|
context.save();
|
||||||
context.fillStyle = "yellow";
|
// context.fillStyle = "yellow";
|
||||||
context.fillRect(
|
const height = measure.ascent + measure.descent;
|
||||||
|
// context.fillRect(x, y - measure.ascent, measure.width, height);
|
||||||
|
context.drawImage(
|
||||||
|
getImage("coin"),
|
||||||
x,
|
x,
|
||||||
y - measure.ascent,
|
y - measure.ascent,
|
||||||
measure.width,
|
measure.width,
|
||||||
measure.ascent + measure.descent
|
height
|
||||||
);
|
);
|
||||||
context.fillStyle = "black";
|
context.fillStyle = "black";
|
||||||
context.fillText(piece.text, x, y);
|
context.textAlign = "center";
|
||||||
|
context.fillText(piece.text, x + measure.width / 2, y);
|
||||||
context.restore();
|
context.restore();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const pieceDefs = [textPiece, spacePiece, breakPiece, coinPiece];
|
const pieceDefs = [textPiece, spacePiece, breakPiece, coinPiece];
|
||||||
|
|
||||||
|
let tools: PieceTools = {} as any;
|
||||||
|
|
||||||
const measurePiece = (context: CanvasRenderingContext2D, piece: Piece) => {
|
const measurePiece = (context: CanvasRenderingContext2D, piece: Piece) => {
|
||||||
const def = pieceDefs.find((def) => def.type === piece.type)!;
|
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 = (
|
const renderPiece = (
|
||||||
@ -118,18 +154,12 @@ const renderPiece = (
|
|||||||
y: number
|
y: number
|
||||||
) => {
|
) => {
|
||||||
const def = pieceDefs.find((def) => def.type === piece.type)!;
|
const def = pieceDefs.find((def) => def.type === piece.type)!;
|
||||||
const measure = def.measure(context, piece as any);
|
const measure = def.measure(context, piece as any, tools);
|
||||||
return def.render(context, piece as any, x, y, measure as any);
|
return def.render(context, piece as any, x, y, measure as any, tools);
|
||||||
};
|
};
|
||||||
|
|
||||||
// export const drawDominionText = (
|
tools.measurePiece = measurePiece;
|
||||||
// context: CanvasRenderingContext2D,
|
tools.renderPiece = renderPiece;
|
||||||
// text: Piece[],
|
|
||||||
// x: number,
|
|
||||||
// y: number,
|
|
||||||
// w: number,
|
|
||||||
// h: number
|
|
||||||
// ) => {};
|
|
||||||
|
|
||||||
type DominionFont = {
|
type DominionFont = {
|
||||||
font: "text" | "title";
|
font: "text" | "title";
|
||||||
@ -138,14 +168,106 @@ type DominionFont = {
|
|||||||
isItalic: boolean;
|
isItalic: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const measureDominionText = (
|
type PieceWithInfo = {
|
||||||
|
piece: Piece;
|
||||||
|
measure: PieceMeasure;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const measureDominionText = async (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
pieces: Piece[],
|
pieces: Piece[],
|
||||||
font: DominionFont,
|
|
||||||
maxWidth: number
|
maxWidth: number
|
||||||
) => {
|
) => {
|
||||||
const data = pieces.map((piece) => ({
|
const data: PieceWithInfo[] = await Promise.all(
|
||||||
piece,
|
pieces.map(async (piece) => ({
|
||||||
measure: measurePiece(context, 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 { TYPE_ACTION } from "./types.ts";
|
||||||
import { DominionCard } from "./types.ts";
|
import { DominionCard } from "./types.ts";
|
||||||
|
|
||||||
@ -39,6 +40,10 @@ const imageList = [
|
|||||||
key: "card-description-focus",
|
key: "card-description-focus",
|
||||||
src: "/static/assets/DescriptionFocus.png",
|
src: "/static/assets/DescriptionFocus.png",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "coin",
|
||||||
|
src: "/static/assets/Coin.png",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const loadImages = async () => {
|
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 (
|
const drawStandardCard = async (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
card: DominionCard
|
card: DominionCard
|
||||||
@ -227,23 +106,22 @@ const drawStandardCard = async (
|
|||||||
context.drawImage(colorImage(getImage("card-brown"), "#ff9911"), 0, 0);
|
context.drawImage(colorImage(getImage("card-brown"), "#ff9911"), 0, 0);
|
||||||
context.drawImage(getImage("card-description-focus"), 44, 1094);
|
context.drawImage(getImage("card-description-focus"), 44, 1094);
|
||||||
// Draw the name
|
// Draw the name
|
||||||
drawTextCentered(context, "Moonlit Scheme", w / 2, 220, {
|
context.font = "75pt DominionTitle";
|
||||||
maxWidth: 1100,
|
await renderDominionText(
|
||||||
font: "DominionTitle",
|
|
||||||
fontWeight: "bold",
|
|
||||||
});
|
|
||||||
// Draw the description
|
|
||||||
drawTextCentered(
|
|
||||||
context,
|
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,
|
w / 2,
|
||||||
1520,
|
1520,
|
||||||
{
|
1100
|
||||||
maxWidth: 1100,
|
|
||||||
font: "DominionText",
|
|
||||||
allowWrap: true,
|
|
||||||
defaultSize: 60,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
// Draw the types
|
// Draw the types
|
||||||
// Draw the cost
|
// Draw the cost
|
||||||
|
Loading…
x
Reference in New Issue
Block a user