Files
doodle-mini/miniprogram/chinesePages/wordTestSheet/draw/wordTestDrawService.ts
T
2026-06-11 14:39:06 +08:00

228 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { BaseDrawService } from '../../../core/draw/baseDraw';
const COLS = 5;
const ROWS = 10;
const LAYOUT = {
topGap: 10,
leftMargin: 36,
rightMargin: 36,
bottomMargin: 40,
instructionGap: 18,
gridTopGap: 12,
instructionFont: 'bold 16px "KaiTi", "STKaiti", "Microsoft Yahei", serif',
instructionBoxSize: 12,
instructionCheckSize: 14,
instructionSymbolGap: 3,
rowGap: 10,
charBoxGap: 12,
boxSize: 14,
} as const;
export interface WordTestSheetDrawData {
words: string[];
pageNumber: number;
}
export default class WordTestDrawService extends BaseDrawService {
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, unknown>,
) {
super(canvas, ctx, {
title: '测字表',
...options,
});
}
async draw(data: WordTestSheetDrawData) {
this.prepareDraw();
await this.drawHeaderAndDivider();
this.drawInstruction();
this.drawWordGrid(data.words);
this.drawPageNumber(data.pageNumber);
this.drawPrintFooter();
}
private getContentRect() {
const contentTop = this.currentY;
const contentWidth =
this.canvasWidth - LAYOUT.leftMargin - LAYOUT.rightMargin;
const contentHeight =
this.canvasHeight - contentTop - LAYOUT.bottomMargin;
return {
top: contentTop,
left: LAYOUT.leftMargin,
width: contentWidth,
height: contentHeight,
};
}
private drawInstruction() {
const { ctx, canvasWidth } = this;
const y = this.currentY + LAYOUT.instructionGap;
const {
instructionFont,
instructionBoxSize,
instructionCheckSize,
instructionSymbolGap,
} = LAYOUT;
const prefix = '认识的字';
const middle = '里打';
ctx.save();
ctx.font = instructionFont;
ctx.fillStyle = '#322E25';
ctx.textBaseline = 'middle';
ctx.textAlign = 'left';
const prefixW = ctx.measureText(prefix).width;
const middleW = ctx.measureText(middle).width;
const totalW =
prefixW +
instructionSymbolGap +
instructionBoxSize +
instructionSymbolGap +
middleW +
instructionSymbolGap +
instructionCheckSize;
let x = (canvasWidth - totalW) / 2;
ctx.fillText(prefix, x, y);
x += prefixW + instructionSymbolGap;
this.drawBox(
ctx,
x,
y - instructionBoxSize / 2,
instructionBoxSize,
instructionBoxSize,
);
x += instructionBoxSize + instructionSymbolGap;
ctx.fillText(middle, x, y);
x += middleW + instructionSymbolGap;
this.drawCheckMark(ctx, x, y, instructionCheckSize);
const metrics = ctx.measureText(prefix);
const instructionFontSize = 16;
const halfH = Math.max(
metrics.actualBoundingBoxAscent ?? instructionFontSize * 0.5,
metrics.actualBoundingBoxDescent ?? instructionFontSize * 0.5,
);
const contentStartY = y + halfH + LAYOUT.gridTopGap;
ctx.restore();
this.currentY = contentStartY;
}
/** 绘制对勾(替代文字 V */
private drawCheckMark(
ctx: RenderingContext,
leftX: number,
centerY: number,
size: number,
) {
ctx.save();
ctx.strokeStyle = '#322E25';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(leftX + size * 0.12, centerY + size * 0.02);
ctx.lineTo(leftX + size * 0.38, centerY + size * 0.32);
ctx.lineTo(leftX + size * 0.88, centerY - size * 0.34);
ctx.stroke();
ctx.restore();
}
private getCharVerticalMetrics(
ctx: RenderingContext,
char: string,
fontSize: number,
) {
ctx.textBaseline = 'alphabetic';
const metrics = ctx.measureText(char);
const ascent = metrics.actualBoundingBoxAscent ?? fontSize * 0.82;
const descent = metrics.actualBoundingBoxDescent ?? fontSize * 0.18;
return { ascent, descent };
}
/** 以 centerY 为视觉中心,绘制汉字与右侧方框 */
private drawCharWithBox(
ctx: RenderingContext,
char: string,
startX: number,
centerY: number,
fontSize: number,
) {
const { charBoxGap, boxSize } = LAYOUT;
const { ascent, descent } = this.getCharVerticalMetrics(
ctx,
char,
fontSize,
);
ctx.textBaseline = 'alphabetic';
const textY = centerY + (ascent - descent) / 2;
ctx.fillText(char, startX, textY);
const boxX = startX + ctx.measureText(char).width + charBoxGap;
const boxY = centerY - boxSize / 2;
this.drawBox(ctx, boxX, boxY, boxSize, boxSize);
}
private drawWordGrid(words: string[]) {
const { ctx } = this;
const content = this.getContentRect();
const colWidth = content.width / COLS;
const rowHeight = content.height / ROWS;
const fontSize = 20;
ctx.save();
ctx.font = `${fontSize}px "KaiTi", "STKaiti", "Microsoft Yahei", serif`;
ctx.fillStyle = '#322E25';
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic';
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const index = row * COLS + col;
const char = words[index];
if (!char) continue;
const cellLeft = content.left + col * colWidth;
const cellCenterY =
content.top + row * rowHeight + rowHeight / 2;
const charWidth = ctx.measureText(char).width;
const groupWidth =
charWidth + LAYOUT.charBoxGap + LAYOUT.boxSize;
const startX = cellLeft + (colWidth - groupWidth) / 2;
this.drawCharWithBox(ctx, char, startX, cellCenterY, fontSize);
}
}
ctx.restore();
}
private drawPageNumber(pageNumber: number) {
const { ctx, canvasWidth, canvasHeight } = this;
ctx.save();
ctx.font = '12px "Microsoft Yahei", sans-serif';
ctx.fillStyle = '#7C766A';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(pageNumber), canvasWidth / 2, canvasHeight - 28);
ctx.restore();
}
}