Files
doodle-mini/miniprogram/mathPages/shared/service/oneDigitAdditionDraw.ts
T
2026-04-09 17:43:22 +08:00

367 lines
11 KiB
TypeScript

/**
* 一位数加法绘制服务
*/
import { BaseDrawService } from '../../../core/draw/baseDraw';
import { getRandomNumberColor } from '../../../constants/colors';
/**
* 一位数加法数据
*/
export interface OneDigitAdditionData {
problems: Array<{
left: number;
right: number;
result: number;
}>;
}
/**
* 一位数加法绘制服务
*/
class OneDigitAdditionDraw extends BaseDrawService {
additionData: OneDigitAdditionData | null = null;
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
this.additionData = null;
}
/**
* 绘制一位数加法内容
*/
async draw(additionData: OneDigitAdditionData) {
if (!additionData || !additionData.problems) {
return;
}
this.additionData = additionData;
this.prepareDraw();
await this.drawHeaderAndDivider();
await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
problems: this.additionData.problems,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
/**
* 绘制内容区域
*/
private async drawContent(params: {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
problems: Array<{
left: number;
right: number;
result: number;
}>;
canvasWidth: number;
startY: number;
}): Promise<void> {
const { ctx, problems, canvasWidth, startY } = params;
const rowStartY = startY + 30; // 顶部间距
const margin = 50;
const rows = 6;
const rowSpacing = 110; // 行间距
// 计算可用宽度(单列居中)
const availableWidth = canvasWidth - margin * 2;
const colCenterX = canvasWidth / 2; // 列的中心点
// 圆点相关参数(适度调大)
const dotRadius = 5; // 圆点半径(调大)
const dotSpacing = 8; // 圆点之间的间距(调大)
const dotsPerRow = 5; // 每行最多5个圆点
const dotRowSpacing = 8; // 圆点行之间的间距(调大)
// 数字和符号相关参数
const fontSize = 26;
const numberSpacing = 14; // 数字之间的间距
const boxSize = 36; // 答案框大小
const boxBorderColor = '#555'; // 答案框边框颜色
// 计算每道题的起始位置(6行1列)
for (let row = 0; row < rows; row++) {
const problemIndex = row;
if (problemIndex >= problems.length) {
break;
}
const problem = problems[problemIndex];
const problemY = rowStartY + row * rowSpacing;
// 绘制单道题(在页面中居中)
this.drawSingleProblem(
ctx,
colCenterX - availableWidth / 2, // 列的起始X坐标
problemY,
availableWidth, // 使用可用宽度
problem,
{
dotRadius,
dotSpacing,
dotsPerRow,
dotRowSpacing,
fontSize,
numberSpacing,
boxSize,
boxBorderColor,
},
);
// 绘制行之间的虚线分割(除了最后一行)
if (row < rows - 1) {
const dividerY = rowStartY + (row + 1) * rowSpacing - 20;
this.drawDashedDivider(dividerY, margin);
}
}
}
/**
* 绘制单道题
*/
private drawSingleProblem(
ctx: RenderingContext,
x: number,
y: number,
width: number,
problem: { left: number; right: number; result: number },
options: {
dotRadius: number;
dotSpacing: number;
dotsPerRow: number;
dotRowSpacing: number;
fontSize: number;
numberSpacing: number;
boxSize: number;
boxBorderColor: string;
},
) {
const {
dotRadius,
dotSpacing,
dotsPerRow,
dotRowSpacing,
fontSize,
numberSpacing,
boxSize,
boxBorderColor,
} = options;
// 为整个运算公式生成统一的随机颜色
const problemColor = getRandomNumberColor();
// 计算5个圆点的最大宽度(统一使用,确保间距一致)
const maxDotsWidth = this.calculateDotsWidth(
dotsPerRow, // 使用5个圆点的宽度
dotsPerRow,
dotRadius,
dotSpacing,
);
// 计算每个元素的宽度(统一使用5个圆点的宽度)
const leftElementWidth = Math.max(maxDotsWidth, fontSize);
const rightElementWidth = Math.max(maxDotsWidth, fontSize);
const resultElementWidth = Math.max(maxDotsWidth, boxSize);
// 计算总宽度并居中
const symbolWidth = 24; // 符号宽度(缩小)
const totalWidth =
leftElementWidth +
numberSpacing +
symbolWidth + // 加号宽度
numberSpacing +
rightElementWidth +
numberSpacing +
symbolWidth + // 等号宽度
numberSpacing +
resultElementWidth;
const startX = x + (width - totalWidth) / 2;
let currentX = startX;
// 计算统一的圆点区域高度(始终使用两行的高度,保持每行高度一致)
const dotsAreaHeight = 2 * dotRadius * 2 + dotRowSpacing; // 2行的高度
// 圆点区域的底部Y坐标(所有圆点都从底部开始绘制)
const dotsBottomY = y + dotsAreaHeight;
// 数字/方框的Y坐标(下方,与圆点底部对齐)
const resultBoxY = dotsBottomY + 12;
const numberY = resultBoxY + (boxSize - fontSize) / 2 + 2;
// 绘制左侧圆点和数字
const leftCenterX = currentX + leftElementWidth / 2;
const leftDotsWidth = this.calculateDotsWidth(
problem.left,
dotsPerRow,
dotRadius,
dotSpacing,
);
this.drawDots(
ctx,
leftCenterX - leftDotsWidth / 2,
dotsBottomY, // 从底部开始绘制
problem.left,
problemColor,
dotRadius,
dotSpacing,
dotsPerRow,
dotRowSpacing,
);
ctx.fillStyle = '#000';
ctx.font = `bold ${fontSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(String(problem.left), leftCenterX, numberY);
currentX += leftElementWidth + numberSpacing;
// 绘制加号(与数字垂直居中对齐)
const plusX = currentX + symbolWidth / 2;
const plusY = numberY + fontSize / 2;
this.drawSymbol(plusX, plusY, '+', symbolWidth * 0.8);
currentX = plusX + symbolWidth / 2 + numberSpacing;
// 绘制右侧圆点和数字
const rightCenterX = currentX + rightElementWidth / 2;
const rightDotsWidth = this.calculateDotsWidth(
problem.right,
dotsPerRow,
dotRadius,
dotSpacing,
);
this.drawDots(
ctx,
rightCenterX - rightDotsWidth / 2,
dotsBottomY, // 从底部开始绘制
problem.right,
problemColor,
dotRadius,
dotSpacing,
dotsPerRow,
dotRowSpacing,
);
ctx.fillStyle = '#000';
ctx.fillText(String(problem.right), rightCenterX, numberY);
currentX += rightElementWidth + numberSpacing;
// 绘制等号(与数字垂直居中对齐)
const equalsX = currentX + symbolWidth / 2;
const equalsY = numberY + fontSize / 2;
this.drawSymbol(equalsX, equalsY, '=', symbolWidth * 0.8);
currentX = equalsX + symbolWidth / 2 + numberSpacing;
// 绘制结果圆点和答案框
const resultCenterX = currentX + resultElementWidth / 2;
const resultDotsWidth = this.calculateDotsWidth(
problem.result,
dotsPerRow,
dotRadius,
dotSpacing,
);
this.drawDots(
ctx,
resultCenterX - resultDotsWidth / 2,
dotsBottomY, // 从底部开始绘制
problem.result,
problemColor,
dotRadius,
dotSpacing,
dotsPerRow,
dotRowSpacing,
);
// 绘制答案框(与数字垂直对齐)
const resultBoxX = resultCenterX - boxSize / 2;
this.drawRoundedRect(resultBoxX, resultBoxY, boxSize, boxSize, {
isDashed: false,
radius: 4,
color: boxBorderColor,
lineWidth: 1,
});
}
/**
* 绘制圆点
* y 参数是圆点区域的底部Y坐标
* 先绘制下面一行(满5个后),再绘制上面一行
*/
private drawDots(
ctx: RenderingContext,
x: number,
y: number, // 圆点区域的底部Y坐标
count: number,
color: string,
radius: number,
spacing: number,
dotsPerRow: number,
rowSpacing: number,
) {
for (let i = 0; i < count; i++) {
let row: number;
let col: number;
if (count <= dotsPerRow) {
// 只有一行,在底部(row = 0 表示最下面一行)
row = 0;
col = i;
} else {
// 多行情况:先绘制下面一行(前5个),再绘制上面一行(剩余的)
if (i < dotsPerRow) {
// 下面一行(row = 0 表示最下面一行)
row = 0;
col = i;
} else {
// 上面一行(row = 1 表示上面一行)
row = 1;
col = i - dotsPerRow;
}
}
// 从底部向上计算Y坐标
// row = 0 时在最下面,row = 1 时在上面
const dotX = x + col * (radius * 2 + spacing);
const dotY = y - (row * (radius * 2 + rowSpacing) + radius * 2);
this.drawDot(ctx, dotX + radius, dotY + radius, radius, color);
}
}
/**
* 计算圆点区域的宽度
*/
private calculateDotsWidth(
count: number,
dotsPerRow: number,
radius: number,
spacing: number,
): number {
const actualCols = Math.min(count, dotsPerRow);
if (actualCols === 0) return 0;
return actualCols * radius * 2 + (actualCols - 1) * spacing;
}
/**
* 计算圆点区域的高度
*/
private calculateDotsHeight(
count: number,
dotsPerRow: number,
radius: number,
rowSpacing: number,
): number {
const rows = Math.ceil(count / dotsPerRow);
if (rows === 0) return 0;
return rows * radius * 2 + (rows - 1) * rowSpacing;
}
}
export default OneDigitAdditionDraw;