367 lines
11 KiB
TypeScript
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;
|