feat:九九乘法表
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* 一位数加法绘制服务
|
||||
*/
|
||||
|
||||
import { BaseDrawService } from '../../../service/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;
|
||||
Reference in New Issue
Block a user