Files
2026-04-09 17:43:22 +08:00

373 lines
12 KiB
TypeScript
Raw Permalink 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';
/**
* 凑十法数据
*/
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 = 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;