feat:2.6.3 破十法、平十法、借十法
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* 破十法练习绘制服务
|
||||
*/
|
||||
|
||||
import { BaseDrawService } from '../../../service/baseDraw';
|
||||
|
||||
/**
|
||||
* 破十法数据
|
||||
*/
|
||||
export interface BreakTenData {
|
||||
problems: Array<{
|
||||
minuend: number; // 被减数
|
||||
subtrahend: number; // 减数
|
||||
result: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 破十法练习绘制服务
|
||||
*/
|
||||
class BreakTenDraw extends BaseDrawService {
|
||||
breakTenData: BreakTenData | null = null;
|
||||
|
||||
constructor(
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) {
|
||||
super(canvas, ctx, options);
|
||||
this.breakTenData = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制破十法练习内容
|
||||
*/
|
||||
async draw(breakTenData: BreakTenData) {
|
||||
if (!breakTenData || !breakTenData.problems) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.breakTenData = breakTenData;
|
||||
this.prepareDraw();
|
||||
await this.drawHeaderAndDivider();
|
||||
|
||||
// 绘制口诀区域
|
||||
this.drawMnemonic();
|
||||
|
||||
// 绘制虚线分割
|
||||
this.drawDashedDivider(this.currentY + 5, 40, '#999', 1);
|
||||
this.currentY += 20;
|
||||
|
||||
// 绘制题目区域(3行3列)
|
||||
this.drawProblems(breakTenData.problems);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制口诀
|
||||
*/
|
||||
private drawMnemonic() {
|
||||
const { ctx, canvasWidth } = this;
|
||||
// 支持通过 options 自定义口诀,默认为破十法口诀
|
||||
const mnemonic =
|
||||
this.options.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);
|
||||
|
||||
// 第二层:被减数拆分成二叉树结构(两个空方框)
|
||||
// 被减数拆分成 10 和 (被减数 - 10)
|
||||
const secondLayerY = firstLayerY + boxSize + verticalSpacing;
|
||||
const minuendBoxCenterX = minuendBoxX + boxSize / 2;
|
||||
|
||||
// 绘制二叉树的两个子节点(空方框)
|
||||
const leftNodeX = minuendBoxCenterX - nodeSpacing / 2 - boxSize / 2;
|
||||
const rightNodeX = minuendBoxCenterX + nodeSpacing / 2 - boxSize / 2;
|
||||
|
||||
// 绘制左子节点(10,空方框)
|
||||
this.drawSquareBox(ctx, leftNodeX, secondLayerY, boxSize, blueColor);
|
||||
|
||||
// 绘制右子节点(被减数 - 10,空方框)
|
||||
this.drawSquareBox(ctx, rightNodeX, secondLayerY, boxSize, pinkColor);
|
||||
|
||||
// 绘制连接线(从被减数到两个子节点)
|
||||
ctx.strokeStyle = lineColor;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(minuendBoxCenterX, firstLayerY + boxSize);
|
||||
ctx.lineTo(leftNodeX + boxSize / 2, secondLayerY);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(minuendBoxCenterX, firstLayerY + boxSize);
|
||||
ctx.lineTo(rightNodeX + boxSize / 2, secondLayerY);
|
||||
ctx.stroke();
|
||||
|
||||
// 第三层:在减数正下方绘制一个方框(10)
|
||||
const thirdLayerY = secondLayerY + boxSize + verticalSpacing + 15;
|
||||
const subtrahendBoxCenterX = subtrahendBoxX + boxSize / 2;
|
||||
const thirdBoxX = subtrahendBoxX - boxSize / 2;
|
||||
const thirdBoxCenterX = thirdBoxX + boxSize / 2;
|
||||
const minusX2 = subtrahendBoxX;
|
||||
|
||||
// 绘制第三层方框(空方框,应该是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(subtrahendBoxCenterX, firstLayerY + boxSize);
|
||||
ctx.lineTo(subtrahendBoxCenterX, layer2_5Y);
|
||||
ctx.stroke();
|
||||
|
||||
// 计算相交点(第2.5层位置,在subtrahendBoxCenterX处)
|
||||
const intersectionY = layer2_5Y;
|
||||
|
||||
// 从第二层右边的方框(右子节点)下绘制直角折线到相交点
|
||||
const rightNodeCenterX = rightNodeX + boxSize / 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(rightNodeCenterX, secondLayerY + boxSize);
|
||||
ctx.lineTo(rightNodeCenterX, intersectionY); // 垂直向下
|
||||
ctx.lineTo(subtrahendBoxCenterX, intersectionY); // 水平到相交点
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制相交点到第三层方框的直线
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(thirdBoxCenterX, intersectionY);
|
||||
ctx.lineTo(thirdBoxCenterX, thirdLayerY); // 垂直向下到第三层
|
||||
ctx.stroke();
|
||||
|
||||
// 在相交位置上方绘制小减号(使用drawSymbol方法)
|
||||
this.drawSymbol(minusX2, layer2_5Y - 10, '-', 12);
|
||||
|
||||
// 从第二层左侧的方框(左子节点)下绘制直角折线连接第三层方框
|
||||
const leftNodeCenterX = leftNodeX + boxSize / 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(leftNodeCenterX, secondLayerY + boxSize);
|
||||
ctx.lineTo(leftNodeCenterX, thirdLayerY + boxSize / 2); // 垂直向下到第三层
|
||||
ctx.lineTo(thirdBoxX, thirdLayerY + boxSize / 2); // 水平连接到第三层方框
|
||||
ctx.stroke();
|
||||
|
||||
// 在第二层左侧方框和第三层方框之间的折线中间绘制加号
|
||||
const plusBetweenX =
|
||||
leftNodeCenterX + (thirdBoxX - leftNodeCenterX) / 2;
|
||||
this.drawSymbol(plusBetweenX, 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 BreakTenDraw;
|
||||
Reference in New Issue
Block a user