/** * 一位数加法绘制服务 */ import { BaseDrawService } from '../../../core/draw/baseDraw'; import { getRandomNumberColor } from '../../../constants/colors'; /** * 一位数加法数据 */ export interface OneDigitAdditionData { problems: Array<{ left: number; right: number; result: number; }>; } /** * 一位数加法绘制服务 */ class OneDigitAdditionDraw extends BaseDrawService { additionData: OneDigitAdditionData | null = null; constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { super(canvas, ctx, options); this.additionData = null; } /** * 绘制一位数加法内容 */ async draw(additionData: OneDigitAdditionData) { if (!additionData || !additionData.problems) { return; } this.additionData = additionData; this.prepareDraw(); await this.drawHeaderAndDivider(); await this.drawContent({ canvas: this.canvas, ctx: this.ctx, problems: this.additionData.problems, canvasWidth: this.canvasWidth, startY: this.currentY, }); } /** * 绘制内容区域 */ private async drawContent(params: { canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; problems: Array<{ left: number; right: number; result: number; }>; canvasWidth: number; startY: number; }): Promise { const { ctx, problems, canvasWidth, startY } = params; const rowStartY = startY + 30; // 顶部间距 const margin = 50; const rows = 6; const rowSpacing = 110; // 行间距 // 计算可用宽度(单列居中) const availableWidth = canvasWidth - margin * 2; const colCenterX = canvasWidth / 2; // 列的中心点 // 圆点相关参数(适度调大) const dotRadius = 5; // 圆点半径(调大) const dotSpacing = 8; // 圆点之间的间距(调大) const dotsPerRow = 5; // 每行最多5个圆点 const dotRowSpacing = 8; // 圆点行之间的间距(调大) // 数字和符号相关参数 const fontSize = 26; const numberSpacing = 14; // 数字之间的间距 const boxSize = 36; // 答案框大小 const boxBorderColor = '#555'; // 答案框边框颜色 // 计算每道题的起始位置(6行1列) for (let row = 0; row < rows; row++) { const problemIndex = row; if (problemIndex >= problems.length) { break; } const problem = problems[problemIndex]; const problemY = rowStartY + row * rowSpacing; // 绘制单道题(在页面中居中) this.drawSingleProblem( ctx, colCenterX - availableWidth / 2, // 列的起始X坐标 problemY, availableWidth, // 使用可用宽度 problem, { dotRadius, dotSpacing, dotsPerRow, dotRowSpacing, fontSize, numberSpacing, boxSize, boxBorderColor, }, ); // 绘制行之间的虚线分割(除了最后一行) if (row < rows - 1) { const dividerY = rowStartY + (row + 1) * rowSpacing - 20; this.drawDashedDivider(dividerY, margin); } } } /** * 绘制单道题 */ private drawSingleProblem( ctx: RenderingContext, x: number, y: number, width: number, problem: { left: number; right: number; result: number }, options: { dotRadius: number; dotSpacing: number; dotsPerRow: number; dotRowSpacing: number; fontSize: number; numberSpacing: number; boxSize: number; boxBorderColor: string; }, ) { const { dotRadius, dotSpacing, dotsPerRow, dotRowSpacing, fontSize, numberSpacing, boxSize, boxBorderColor, } = options; // 为整个运算公式生成统一的随机颜色 const problemColor = getRandomNumberColor(); // 计算5个圆点的最大宽度(统一使用,确保间距一致) const maxDotsWidth = this.calculateDotsWidth( dotsPerRow, // 使用5个圆点的宽度 dotsPerRow, dotRadius, dotSpacing, ); // 计算每个元素的宽度(统一使用5个圆点的宽度) const leftElementWidth = Math.max(maxDotsWidth, fontSize); const rightElementWidth = Math.max(maxDotsWidth, fontSize); const resultElementWidth = Math.max(maxDotsWidth, boxSize); // 计算总宽度并居中 const symbolWidth = 24; // 符号宽度(缩小) const totalWidth = leftElementWidth + numberSpacing + symbolWidth + // 加号宽度 numberSpacing + rightElementWidth + numberSpacing + symbolWidth + // 等号宽度 numberSpacing + resultElementWidth; const startX = x + (width - totalWidth) / 2; let currentX = startX; // 计算统一的圆点区域高度(始终使用两行的高度,保持每行高度一致) const dotsAreaHeight = 2 * dotRadius * 2 + dotRowSpacing; // 2行的高度 // 圆点区域的底部Y坐标(所有圆点都从底部开始绘制) const dotsBottomY = y + dotsAreaHeight; // 数字/方框的Y坐标(下方,与圆点底部对齐) const resultBoxY = dotsBottomY + 12; const numberY = resultBoxY + (boxSize - fontSize) / 2 + 2; // 绘制左侧圆点和数字 const leftCenterX = currentX + leftElementWidth / 2; const leftDotsWidth = this.calculateDotsWidth( problem.left, dotsPerRow, dotRadius, dotSpacing, ); this.drawDots( ctx, leftCenterX - leftDotsWidth / 2, dotsBottomY, // 从底部开始绘制 problem.left, problemColor, dotRadius, dotSpacing, dotsPerRow, dotRowSpacing, ); ctx.fillStyle = '#000'; ctx.font = `bold ${fontSize}px Arial`; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText(String(problem.left), leftCenterX, numberY); currentX += leftElementWidth + numberSpacing; // 绘制加号(与数字垂直居中对齐) const plusX = currentX + symbolWidth / 2; const plusY = numberY + fontSize / 2; this.drawSymbol(plusX, plusY, '+', symbolWidth * 0.8); currentX = plusX + symbolWidth / 2 + numberSpacing; // 绘制右侧圆点和数字 const rightCenterX = currentX + rightElementWidth / 2; const rightDotsWidth = this.calculateDotsWidth( problem.right, dotsPerRow, dotRadius, dotSpacing, ); this.drawDots( ctx, rightCenterX - rightDotsWidth / 2, dotsBottomY, // 从底部开始绘制 problem.right, problemColor, dotRadius, dotSpacing, dotsPerRow, dotRowSpacing, ); ctx.fillStyle = '#000'; ctx.fillText(String(problem.right), rightCenterX, numberY); currentX += rightElementWidth + numberSpacing; // 绘制等号(与数字垂直居中对齐) const equalsX = currentX + symbolWidth / 2; const equalsY = numberY + fontSize / 2; this.drawSymbol(equalsX, equalsY, '=', symbolWidth * 0.8); currentX = equalsX + symbolWidth / 2 + numberSpacing; // 绘制结果圆点和答案框 const resultCenterX = currentX + resultElementWidth / 2; const resultDotsWidth = this.calculateDotsWidth( problem.result, dotsPerRow, dotRadius, dotSpacing, ); this.drawDots( ctx, resultCenterX - resultDotsWidth / 2, dotsBottomY, // 从底部开始绘制 problem.result, problemColor, dotRadius, dotSpacing, dotsPerRow, dotRowSpacing, ); // 绘制答案框(与数字垂直对齐) const resultBoxX = resultCenterX - boxSize / 2; this.drawRoundedRect(resultBoxX, resultBoxY, boxSize, boxSize, { isDashed: false, radius: 4, color: boxBorderColor, lineWidth: 1, }); } /** * 绘制圆点 * y 参数是圆点区域的底部Y坐标 * 先绘制下面一行(满5个后),再绘制上面一行 */ private drawDots( ctx: RenderingContext, x: number, y: number, // 圆点区域的底部Y坐标 count: number, color: string, radius: number, spacing: number, dotsPerRow: number, rowSpacing: number, ) { for (let i = 0; i < count; i++) { let row: number; let col: number; if (count <= dotsPerRow) { // 只有一行,在底部(row = 0 表示最下面一行) row = 0; col = i; } else { // 多行情况:先绘制下面一行(前5个),再绘制上面一行(剩余的) if (i < dotsPerRow) { // 下面一行(row = 0 表示最下面一行) row = 0; col = i; } else { // 上面一行(row = 1 表示上面一行) row = 1; col = i - dotsPerRow; } } // 从底部向上计算Y坐标 // row = 0 时在最下面,row = 1 时在上面 const dotX = x + col * (radius * 2 + spacing); const dotY = y - (row * (radius * 2 + rowSpacing) + radius * 2); this.drawDot(ctx, dotX + radius, dotY + radius, radius, color); } } /** * 计算圆点区域的宽度 */ private calculateDotsWidth( count: number, dotsPerRow: number, radius: number, spacing: number, ): number { const actualCols = Math.min(count, dotsPerRow); if (actualCols === 0) return 0; return actualCols * radius * 2 + (actualCols - 1) * spacing; } /** * 计算圆点区域的高度 */ private calculateDotsHeight( count: number, dotsPerRow: number, radius: number, rowSpacing: number, ): number { const rows = Math.ceil(count / dotsPerRow); if (rows === 0) return 0; return rows * radius * 2 + (rows - 1) * rowSpacing; } } export default OneDigitAdditionDraw;