Files
doodle-mini/miniprogram/mathPages/shared/service/flatTenDraw.ts
T
2025-12-25 14:59:57 +08:00

340 lines
11 KiB
TypeScript

/**
* 平十法练习绘制服务
*/
import { BaseDrawService } from '../../../service/baseDraw';
/**
* 平十法数据
*/
export interface FlatTenData {
problems: Array<{
minuend: number; // 被减数
subtrahend: number; // 减数
result: number;
}>;
}
/**
* 平十法练习绘制服务
*/
class FlatTenDraw extends BaseDrawService {
flatTenData: FlatTenData | null = null;
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
this.flatTenData = null;
}
/**
* 绘制平十法练习内容
*/
async draw(flatTenData: FlatTenData) {
if (!flatTenData || !flatTenData.problems) {
return;
}
this.flatTenData = flatTenData;
this.prepareDraw();
await this.drawHeaderAndDivider();
// 绘制口诀区域
this.drawMnemonic();
// 绘制虚线分割
this.drawDashedDivider(this.currentY + 5, 40, '#999', 1);
this.currentY += 20;
// 绘制题目区域(3行3列)
this.drawProblems(flatTenData.problems);
}
/**
* 绘制口诀
*/
private drawMnemonic() {
const { ctx, canvasWidth } = this;
const 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);
// 第二层:以减数为根节点拆分二叉树结构(两个空方框)
// 减数拆分成两部分:第一部分 = 被减数的个位数,第二部分 = 减数 - 第一部分
const secondLayerY = firstLayerY + boxSize + verticalSpacing;
const subtrahendBoxCenterX = subtrahendBoxX + boxSize / 2;
// 计算减数的拆分:第一部分是被减数的个位数,第二部分是减数减去第一部分
const minuendOnes = problem.minuend % 10; // 被减数的个位数
const part1 = minuendOnes; // 第一部分
const part2 = problem.subtrahend - part1; // 第二部分
// 绘制二叉树的两个子节点(空方框)
const leftNodeX = subtrahendBoxCenterX - nodeSpacing / 2 - boxSize / 2;
const rightNodeX = subtrahendBoxCenterX + nodeSpacing / 2 - boxSize / 2;
// 绘制左子节点(第一部分,空方框)
this.drawSquareBox(ctx, leftNodeX, secondLayerY, boxSize, blueColor);
// 绘制右子节点(第二部分,空方框)
this.drawSquareBox(ctx, rightNodeX, secondLayerY, boxSize, pinkColor);
// 绘制连接线(从减数到两个子节点)
ctx.strokeStyle = lineColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(subtrahendBoxCenterX, firstLayerY + boxSize);
ctx.lineTo(leftNodeX + boxSize / 2, secondLayerY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(subtrahendBoxCenterX, firstLayerY + boxSize);
ctx.lineTo(rightNodeX + boxSize / 2, secondLayerY);
ctx.stroke();
// 第三层:在被减数正下方绘制一个方框
const thirdLayerY = secondLayerY + boxSize + verticalSpacing + 15;
const minuendBoxCenterX = minuendBoxX + boxSize / 2;
const thirdBoxX = minuendBoxCenterX; // 第三层方框X轴是被减数方框的中点
const minusX2 = minuendBoxX + boxSize;
const minusX3 = subtrahendBoxCenterX;
// 绘制第三层方框(空方框,应该是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(minuendBoxCenterX, firstLayerY + boxSize);
ctx.lineTo(minuendBoxCenterX, layer2_5Y);
ctx.stroke();
// 计算相交点(第2.5层位置)
const intersectionY = layer2_5Y;
// 从第二层左侧的方框(左子节点)下绘制直角折线到相交点
const leftNodeCenterX = leftNodeX + boxSize / 2;
ctx.beginPath();
ctx.moveTo(leftNodeCenterX, secondLayerY + boxSize);
ctx.lineTo(leftNodeCenterX, intersectionY); // 垂直向下
ctx.lineTo(minuendBoxCenterX, intersectionY); // 水平到相交点
ctx.stroke();
// 在相交位置上方绘制小减号(使用drawSymbol方法)
this.drawSymbol(minusX2, layer2_5Y - 10, '-', 12);
// 以被减数框的X轴+方框size为X轴向下绘制直线和第三层的方框相连
const minuendRightX = minuendBoxX + boxSize;
ctx.strokeStyle = '#888';
ctx.beginPath();
ctx.moveTo(minuendRightX, intersectionY);
ctx.lineTo(minuendRightX, thirdLayerY); // 垂直向下到第三层
ctx.stroke();
// 从第二层右侧的方框(右子节点)下绘制直角折线连接第三层方框
const rightNodeCenterX = rightNodeX + boxSize / 2;
// ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(rightNodeCenterX, secondLayerY + boxSize);
ctx.lineTo(rightNodeCenterX, thirdLayerY + boxSize / 2); // 垂直向下到第三层
ctx.lineTo(thirdBoxX + boxSize, thirdLayerY + boxSize / 2); // 水平连接到第三层方框中心
ctx.stroke();
this.drawSymbol(minusX3, 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 FlatTenDraw;