type Piece = | { type: "text"; text: string; isBold?: boolean; isItalic?: boolean } | { type: "space" } | { type: "break" } | { type: "coin"; text: string }; type PromiseOr = T | Promise; type PieceMeasure = { type: "content" | "space" | "break"; width: number; ascent: number; descent: number; }; type PieceDef = { type: T; measure( context: CanvasRenderingContext2D, piece: Piece & { type: T } ): PromiseOr; render( context: CanvasRenderingContext2D, piece: Piece & { type: T }, x: number, y: number, measure: NoInfer ): PromiseOr; }; const pieceDef = ( def: PieceDef ) => { return def; }; const textPiece = pieceDef({ type: "text", measure(context, piece) { const metrics = context.measureText(piece.text); return { type: "content", width: metrics.width, ascent: metrics.emHeightAscent, descent: metrics.emHeightDescent, }; }, render(context, piece, x, y) { context.fillText(piece.text, x, y); }, }); const spacePiece = pieceDef({ type: "space", measure(context, _piece) { const metrics = context.measureText(" "); return { type: "space", width: metrics.width, ascent: metrics.emHeightAscent, descent: metrics.emHeightDescent, }; }, render() {}, }); const breakPiece = pieceDef({ type: "break", measure(context, _piece) { const metrics = context.measureText(" "); return { type: "break", width: 0, ascent: metrics.emHeightAscent, descent: metrics.emHeightDescent, }; }, render() {}, }); const coinPiece = pieceDef({ type: "coin", measure(context, _piece) { const metrics = context.measureText(" "); return { type: "content", width: metrics.emHeightAscent + metrics.emHeightDescent, ascent: metrics.emHeightAscent, descent: metrics.emHeightDescent, }; }, render(context, piece, x, y, measure) { context.save(); context.fillStyle = "yellow"; context.fillRect( x, y - measure.ascent, measure.width, measure.ascent + measure.descent ); context.fillStyle = "black"; context.fillText(piece.text, x, y); context.restore(); }, }); const pieceDefs = [textPiece, spacePiece, breakPiece, coinPiece]; const measurePiece = (context: CanvasRenderingContext2D, piece: Piece) => { const def = pieceDefs.find((def) => def.type === piece.type)!; return def.measure(context, piece as any); }; const renderPiece = ( context: CanvasRenderingContext2D, piece: Piece, x: number, 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); }; // export const drawDominionText = ( // context: CanvasRenderingContext2D, // text: Piece[], // x: number, // y: number, // w: number, // h: number // ) => {}; type DominionFont = { font: "text" | "title"; size: number; isBold: boolean; isItalic: boolean; }; export const measureDominionText = ( context: CanvasRenderingContext2D, pieces: Piece[], font: DominionFont, maxWidth: number ) => { const data = pieces.map((piece) => ({ piece, measure: measurePiece(context, piece), })); };