335 lines
10 KiB
TypeScript
335 lines
10 KiB
TypeScript
/**
|
|
* 破十法练习绘制服务
|
|
*/
|
|
|
|
import { BaseDrawService } from '../../../core/draw/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;
|