/** * 凑十法练习绘制服务 */ import { BaseDrawService } from '../../../core/draw/baseDraw'; /** * 凑十法数据 */ export interface MakeTenData { problems: Array<{ left: number; right: number; result: number; }>; } /** * 凑十法练习绘制服务 */ class MakeTenDraw extends BaseDrawService { makeTenData: MakeTenData | null = null; constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { super(canvas, ctx, options); this.makeTenData = null; } /** * 绘制凑十法练习内容 */ async draw(makeTenData: MakeTenData) { if (!makeTenData || !makeTenData.problems) { return; } this.makeTenData = makeTenData; this.prepareDraw(); await this.drawHeaderAndDivider(); // 绘制口诀区域 this.drawMnemonic(); // 绘制虚线分割 this.drawDashedDivider(this.currentY + 5, 40, '#999', 1); this.currentY += 20; // 绘制题目区域(5行4列) this.drawProblems(makeTenData.problems); } /** * 绘制口诀 */ private drawMnemonic() { const { ctx, canvasWidth } = this; const mnemonic = '看大数,拆小数,凑成10,加剩数'; 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<{ left: number; right: 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: { left: number; right: number; result: number }, ) { const blueColor = '#8CBAFF'; const yellowColor = '#FFCB05'; const greenColor = '#5AE2B1'; const pinkColor = '#F2A4C0'; const lineColor = '#6CD2EA'; // 确定大数和小数 const bigNumber = Math.max(problem.left, problem.right); const smallNumber = Math.min(problem.left, problem.right); const bigFirst = problem.left >= problem.right; 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 firstNumber = bigFirst ? bigNumber : smallNumber; const secondNumber = bigFirst ? smallNumber : bigNumber; // 第一层元素位置(间距为15) const firstBoxX = centerX - boxSize - horizontalSpacing; const plusX = firstBoxX + boxSize + horizontalSpacing; const secondBoxX = plusX + horizontalSpacing; const equalsX = secondBoxX + boxSize + horizontalSpacing - 2; const answerBoxX = equalsX + 10 + horizontalSpacing; // 绘制第一个加数方框(边框1px,#999,无填充) this.drawTopLayerBox( ctx, firstBoxX, firstLayerY, boxSize, firstNumber, fontSize, blueColor, ); // 绘制加号 this.drawPlusSign(ctx, plusX, firstLayerY + boxSize / 2, 12); // 绘制第二个加数方框 this.drawTopLayerBox( ctx, secondBoxX, firstLayerY, boxSize, secondNumber, fontSize, yellowColor, ); // 绘制等号 this.drawEqualsSign(ctx, equalsX, firstLayerY + boxSize / 2, 12); // 绘制答案框(空白) this.drawSquareBox(ctx, answerBoxX, firstLayerY, boxSize, greenColor); // 第二层:在较小加数下绘制二叉树结构(两个空方框) const secondLayerY = firstLayerY + boxSize + verticalSpacing; const smallBoxX = bigFirst ? secondBoxX : firstBoxX; const smallBoxCenterX = smallBoxX + boxSize / 2; // 绘制二叉树的两个子节点(空方框) const leftNodeX = smallBoxCenterX - nodeSpacing / 2 - boxSize / 2; const rightNodeX = smallBoxCenterX + nodeSpacing / 2 - boxSize / 2; // 绘制左子节点(空方框) this.drawSquareBox(ctx, leftNodeX, secondLayerY, boxSize, blueColor); // 绘制右子节点(空方框) this.drawSquareBox(ctx, rightNodeX, secondLayerY, boxSize, pinkColor); // 确定哪个子节点与小数相连 // 如果小数在前(bigFirst=false),第二个方框(右子节点)和加数相连 // 如果小数在后(bigFirst=true),第一个方框(左子节点)和加数相连 const connectedNodeX = bigFirst ? leftNodeX : rightNodeX; const connectedNodeCenterX = connectedNodeX + boxSize / 2; const unconnectedNodeX = bigFirst ? rightNodeX : leftNodeX; const unconnectedNodeCenterX = unconnectedNodeX + boxSize / 2; // 绘制连接线(从较小加数到两个子节点) ctx.strokeStyle = lineColor; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(smallBoxCenterX, firstLayerY + boxSize); ctx.lineTo(leftNodeX + boxSize / 2, secondLayerY); ctx.stroke(); ctx.beginPath(); ctx.moveTo(smallBoxCenterX, firstLayerY + boxSize); ctx.lineTo(rightNodeX + boxSize / 2, secondLayerY); ctx.stroke(); const thirdLayerY = secondLayerY + boxSize + verticalSpacing + 15; let bigBoxX = 0; let bigBoxCenterX = 0; let thirdBoxX = 0; let thirdLineEndX = 0; let firstPlusX = 0; let secondPlusX = 0; let thirdBoxCenterX = 0; if (bigFirst) { bigBoxX = firstBoxX; bigBoxCenterX = bigBoxX + boxSize / 2; thirdBoxX = bigBoxCenterX; thirdBoxCenterX = thirdBoxX + boxSize / 2; firstPlusX = bigBoxX + boxSize; secondPlusX = secondBoxX + boxSize / 2; thirdLineEndX = thirdBoxX + boxSize; } else { bigBoxX = secondBoxX; bigBoxCenterX = bigBoxX + boxSize / 2; thirdBoxX = bigBoxX - boxSize / 2; thirdBoxCenterX = thirdBoxX + boxSize / 2; firstPlusX = secondBoxX; secondPlusX = firstBoxX + boxSize / 2; thirdLineEndX = thirdBoxX; } // 绘制第三层方框(空方框) 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(bigBoxCenterX, firstLayerY + boxSize); ctx.lineTo(bigBoxCenterX, layer2_5Y); ctx.stroke(); // 计算相交点(第2.5层位置,在bigBoxCenterX处) const intersectionY = layer2_5Y; // ctx.strokeStyle = 'red'; // 从第二层与小数相连的方框下绘制直角折线到相交点 ctx.beginPath(); ctx.moveTo(connectedNodeCenterX, secondLayerY + boxSize); ctx.lineTo(connectedNodeCenterX, intersectionY); // 垂直向下 ctx.lineTo(bigBoxCenterX, intersectionY); // 水平到相交点 ctx.stroke(); // 绘制相交点到第三层方框的直线 ctx.beginPath(); ctx.moveTo(thirdBoxCenterX, intersectionY); ctx.lineTo(thirdBoxCenterX, thirdLayerY); // 水平到相交点 ctx.stroke(); // 在相交位置绘制小加号(使用drawSymbol方法) this.drawSymbol(firstPlusX, layer2_5Y - 10, '+', 12); // ctx.strokeStyle = 'blue'; // 从第二层另一个方框(未与小数相连的)下绘制直角折线连接第三层方框 ctx.beginPath(); ctx.moveTo(unconnectedNodeCenterX, secondLayerY + boxSize); ctx.lineTo(unconnectedNodeCenterX, thirdLayerY + boxSize / 2); // 垂直向下到第三层 ctx.lineTo(thirdLineEndX, thirdLayerY + boxSize / 2); // 水平连接到第三层方框中心 ctx.stroke(); // 在第二层另一个方框和第三层方框之间的折线中间绘制加号 this.drawSymbol(secondPlusX, thirdLayerY + 2, '+', 12); } /** * 绘制第一层方框(边框1px,#999,无填充,显示数字) */ private drawTopLayerBox( ctx: RenderingContext, x: number, y: number, size: number, number: number, fontSize: number, color: string, ) { // 绘制方框边框(1px,#999,无填充) // ctx.strokeStyle = '#999'; // ctx.lineWidth = 1; // ctx.setLineDash([]); // ctx.strokeRect(x, y, size, size); 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 drawPlusSign( 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.moveTo(x, y - size / 2); ctx.lineTo(x, y + size / 2); 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 MakeTenDraw;