feat:2.6.2 凑十法

This commit is contained in:
R524809
2025-12-24 18:15:01 +08:00
parent 58423eb31a
commit ec3e79ad29
10 changed files with 620 additions and 3 deletions
@@ -0,0 +1,372 @@
/**
* 凑十法练习绘制服务
*/
import { BaseDrawService } from '../../../service/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<string, any>,
) {
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 = 28; // 适合小朋友的字体大小
// 使用适合小朋友的字体
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 + 10;
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 + 3, '+', 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;