/** * 平十法练习绘制服务 */ import { BaseDrawService } from '../../../service/baseDraw'; /** * 平十法数据 */ export interface FlatTenData { problems: Array<{ minuend: number; // 被减数 subtrahend: number; // 减数 result: number; }>; } /** * 平十法练习绘制服务 */ class FlatTenDraw extends BaseDrawService { flatTenData: FlatTenData | null = null; constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { super(canvas, ctx, options); this.flatTenData = null; } /** * 绘制平十法练习内容 */ async draw(flatTenData: FlatTenData) { if (!flatTenData || !flatTenData.problems) { return; } this.flatTenData = flatTenData; this.prepareDraw(); await this.drawHeaderAndDivider(); // 绘制口诀区域 this.drawMnemonic(); // 绘制虚线分割 this.drawDashedDivider(this.currentY + 5, 40, '#999', 1); this.currentY += 20; // 绘制题目区域(3行3列) this.drawProblems(flatTenData.problems); } /** * 绘制口诀 */ private drawMnemonic() { const { ctx, canvasWidth } = this; const mnemonic = '看大数的个位,拆小数,连续减,算得数'; const fontSize = 24; // 适合小朋友的字体大小 // 使用适合小朋友的字体 ctx.font = `bold ${fontSize}px "YouYuan", "PingFang SC", "Microsoft Yahei", sans-serif`; ctx.fillStyle = '#343434'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; // 居中绘制口诀 const mnemonicY = this.currentY + 30; ctx.fillText(mnemonic, canvasWidth / 2, mnemonicY); // 更新当前Y位置 this.currentY = mnemonicY + 30; } /** * 绘制题目区域(3行3列) */ private drawProblems( problems: Array<{ minuend: number; subtrahend: number; result: number; }>, ) { const { ctx, canvasWidth } = this; const rows = 3; const cols = 3; const startX = 10; const startY = this.currentY + 20; const itemWidth = (canvasWidth - startX * 2) / cols - 18; const itemHeight = 200; // 每个题目的高度 for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { const index = row * cols + col; if (index >= problems.length) { break; } const problem = problems[index]; const x = startX + col * itemWidth; const y = startY + row * itemHeight; // 绘制单个题目 this.drawSingleProblem( ctx, x, y, itemWidth, itemHeight, problem, ); } } } /** * 绘制单个题目 */ private drawSingleProblem( ctx: RenderingContext, x: number, y: number, width: number, _height: number, problem: { minuend: number; subtrahend: number; result: number }, ) { const blueColor = '#8CBAFF'; const yellowColor = '#FFCB05'; const greenColor = '#5AE2B1'; const pinkColor = '#F2A4C0'; const lineColor = '#6CD2EA'; const boxSize = 30; // 方框大小 const fontSize = 18; // 数字字体大小 const verticalSpacing = 25; // 垂直间距 const nodeSpacing = 40; // 二叉树节点之间的水平间距 const horizontalSpacing = 10; // 第一层元素之间的水平间距 // 第一层:被减数 - 减数 = 答案框 const firstLayerY = y + 15; const centerX = x + width / 2; // 第一层元素位置 const minuendBoxX = centerX - boxSize - horizontalSpacing; const minusX = minuendBoxX + boxSize + horizontalSpacing; const subtrahendBoxX = minusX + horizontalSpacing; const equalsX = subtrahendBoxX + boxSize + horizontalSpacing - 2; const answerBoxX = equalsX + 10 + horizontalSpacing; // 绘制被减数方框 this.drawTopLayerBox( ctx, minuendBoxX, firstLayerY, boxSize, problem.minuend, fontSize, blueColor, ); // 绘制减号 this.drawMinusSign(ctx, minusX, firstLayerY + boxSize / 2, 12); // 绘制减数方框 this.drawTopLayerBox( ctx, subtrahendBoxX, firstLayerY, boxSize, problem.subtrahend, fontSize, yellowColor, ); // 绘制等号 this.drawEqualsSign(ctx, equalsX, firstLayerY + boxSize / 2, 12); // 绘制答案框(空白) this.drawSquareBox(ctx, answerBoxX, firstLayerY, boxSize, greenColor); // 第二层:以减数为根节点拆分二叉树结构(两个空方框) // 减数拆分成两部分:第一部分 = 被减数的个位数,第二部分 = 减数 - 第一部分 const secondLayerY = firstLayerY + boxSize + verticalSpacing; const subtrahendBoxCenterX = subtrahendBoxX + boxSize / 2; // 计算减数的拆分:第一部分是被减数的个位数,第二部分是减数减去第一部分 const minuendOnes = problem.minuend % 10; // 被减数的个位数 const part1 = minuendOnes; // 第一部分 const part2 = problem.subtrahend - part1; // 第二部分 // 绘制二叉树的两个子节点(空方框) const leftNodeX = subtrahendBoxCenterX - nodeSpacing / 2 - boxSize / 2; const rightNodeX = subtrahendBoxCenterX + nodeSpacing / 2 - boxSize / 2; // 绘制左子节点(第一部分,空方框) this.drawSquareBox(ctx, leftNodeX, secondLayerY, boxSize, blueColor); // 绘制右子节点(第二部分,空方框) this.drawSquareBox(ctx, rightNodeX, secondLayerY, boxSize, pinkColor); // 绘制连接线(从减数到两个子节点) ctx.strokeStyle = lineColor; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(subtrahendBoxCenterX, firstLayerY + boxSize); ctx.lineTo(leftNodeX + boxSize / 2, secondLayerY); ctx.stroke(); ctx.beginPath(); ctx.moveTo(subtrahendBoxCenterX, firstLayerY + boxSize); ctx.lineTo(rightNodeX + boxSize / 2, secondLayerY); ctx.stroke(); // 第三层:在被减数正下方绘制一个方框 const thirdLayerY = secondLayerY + boxSize + verticalSpacing + 15; const minuendBoxCenterX = minuendBoxX + boxSize / 2; const thirdBoxX = minuendBoxCenterX; // 第三层方框X轴是被减数方框的中点 const minusX2 = minuendBoxX + boxSize; const minusX3 = subtrahendBoxCenterX; // 绘制第三层方框(空方框,应该是10) this.drawSquareBox(ctx, thirdBoxX, thirdLayerY, boxSize, blueColor); // 从被减数正下方绘制直线至第2.5层的位置 const layer2_5Y = secondLayerY + boxSize + (thirdLayerY - secondLayerY - boxSize) / 2 + 4; ctx.strokeStyle = '#888'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(minuendBoxCenterX, firstLayerY + boxSize); ctx.lineTo(minuendBoxCenterX, layer2_5Y); ctx.stroke(); // 计算相交点(第2.5层位置) const intersectionY = layer2_5Y; // 从第二层左侧的方框(左子节点)下绘制直角折线到相交点 const leftNodeCenterX = leftNodeX + boxSize / 2; ctx.beginPath(); ctx.moveTo(leftNodeCenterX, secondLayerY + boxSize); ctx.lineTo(leftNodeCenterX, intersectionY); // 垂直向下 ctx.lineTo(minuendBoxCenterX, intersectionY); // 水平到相交点 ctx.stroke(); // 在相交位置上方绘制小减号(使用drawSymbol方法) this.drawSymbol(minusX2, layer2_5Y - 10, '-', 12); // 以被减数框的X轴+方框size为X轴向下绘制直线和第三层的方框相连 const minuendRightX = minuendBoxX + boxSize; ctx.strokeStyle = '#888'; ctx.beginPath(); ctx.moveTo(minuendRightX, intersectionY); ctx.lineTo(minuendRightX, thirdLayerY); // 垂直向下到第三层 ctx.stroke(); // 从第二层右侧的方框(右子节点)下绘制直角折线连接第三层方框 const rightNodeCenterX = rightNodeX + boxSize / 2; // ctx.strokeStyle = 'red'; ctx.beginPath(); ctx.moveTo(rightNodeCenterX, secondLayerY + boxSize); ctx.lineTo(rightNodeCenterX, thirdLayerY + boxSize / 2); // 垂直向下到第三层 ctx.lineTo(thirdBoxX + boxSize, thirdLayerY + boxSize / 2); // 水平连接到第三层方框中心 ctx.stroke(); this.drawSymbol(minusX3, thirdLayerY + 2, '-', 12); } /** * 绘制第一层方框(边框1px,#999,无填充,显示数字) */ private drawTopLayerBox( ctx: RenderingContext, x: number, y: number, size: number, number: number, fontSize: number, color: string, ) { // 绘制方框 this.drawSquareBox(ctx, x, y, size, color); // 绘制数字 ctx.fillStyle = '#000'; ctx.font = `bold ${fontSize}px "YouYuan", "PingFang SC", "Microsoft Yahei", sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(String(number), x + size / 2, y + size / 2); } /** * 绘制减号 */ private drawMinusSign( ctx: RenderingContext, x: number, y: number, size: number, ) { ctx.strokeStyle = '#000'; ctx.lineWidth = 1.5; ctx.beginPath(); // 横线 ctx.moveTo(x - size / 2, y); ctx.lineTo(x + size / 2, y); ctx.stroke(); } /** * 绘制等号 */ private drawEqualsSign( ctx: RenderingContext, x: number, y: number, size: number, ) { ctx.strokeStyle = '#000'; ctx.lineWidth = 1.5; ctx.beginPath(); // 上横线 ctx.moveTo(x, y - size / 3); ctx.lineTo(x + size, y - size / 3); // 下横线 ctx.moveTo(x, y + size / 3); ctx.lineTo(x + size, y + size / 3); ctx.stroke(); } } export default FlatTenDraw;