feat:九九乘法表
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* 计算练习题绘制服务
|
||||
*/
|
||||
|
||||
import { BaseDrawService } from '../../../service/baseDraw';
|
||||
|
||||
/**
|
||||
* 计算练习题数据
|
||||
*/
|
||||
export interface CalculationPracticeData {
|
||||
problems: Array<{
|
||||
left: number;
|
||||
operator: '+' | '-';
|
||||
right: number;
|
||||
result: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算练习题绘制服务
|
||||
*/
|
||||
class CalculationPracticeDraw extends BaseDrawService {
|
||||
practiceData: CalculationPracticeData | null = null;
|
||||
|
||||
constructor(
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) {
|
||||
super(canvas, ctx, options);
|
||||
this.practiceData = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制计算练习题内容
|
||||
*/
|
||||
async draw(practiceData: CalculationPracticeData) {
|
||||
if (!practiceData || !practiceData.problems) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.practiceData = practiceData;
|
||||
this.prepareDraw();
|
||||
await this.drawHeaderAndDivider();
|
||||
await this.drawContent({
|
||||
canvas: this.canvas,
|
||||
ctx: this.ctx,
|
||||
problems: this.practiceData.problems,
|
||||
canvasWidth: this.canvasWidth,
|
||||
startY: this.currentY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制内容区域
|
||||
*/
|
||||
private async drawContent(params: {
|
||||
canvas: WechatMiniprogram.Canvas;
|
||||
ctx: RenderingContext;
|
||||
problems: Array<{
|
||||
left: number;
|
||||
operator: '+' | '-';
|
||||
right: number;
|
||||
result: number;
|
||||
}>;
|
||||
canvasWidth: number;
|
||||
startY: number;
|
||||
}): Promise<void> {
|
||||
const { ctx, problems, canvasWidth, startY } = params;
|
||||
const rowStartY = startY + 30; // 顶部间距
|
||||
const margin = 30;
|
||||
const rows = 12;
|
||||
const cols = 3;
|
||||
const rowSpacing = 60; // 行间距
|
||||
const colSpacing = 15; // 列间距
|
||||
|
||||
// 计算可用宽度
|
||||
const availableWidth = canvasWidth - margin * 2;
|
||||
const colWidth = (availableWidth - colSpacing * (cols - 1)) / cols;
|
||||
|
||||
// 数字和符号相关参数
|
||||
const fontSize = 18;
|
||||
const numberSpacing = 5; // 数字之间的间距
|
||||
const boxSize = 28; // 答案框大小
|
||||
const boxBorderColor = '#555'; // 答案框边框颜色
|
||||
const symbolWidth = 16; // 符号宽度
|
||||
|
||||
// 计算每道题的起始位置(10行3列)
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const problemIndex = row * cols + col;
|
||||
if (problemIndex >= problems.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
const problem = problems[problemIndex];
|
||||
const problemX = margin + col * (colWidth + colSpacing);
|
||||
const problemY = rowStartY + row * rowSpacing;
|
||||
|
||||
// 绘制单道题
|
||||
this.drawSingleProblem(
|
||||
ctx,
|
||||
problemX,
|
||||
problemY,
|
||||
colWidth,
|
||||
problem,
|
||||
{
|
||||
fontSize,
|
||||
numberSpacing,
|
||||
boxSize,
|
||||
boxBorderColor,
|
||||
symbolWidth,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制单道题
|
||||
*/
|
||||
private drawSingleProblem(
|
||||
ctx: RenderingContext,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
problem: {
|
||||
left: number;
|
||||
operator: '+' | '-';
|
||||
right: number;
|
||||
result: number;
|
||||
},
|
||||
options: {
|
||||
fontSize: number;
|
||||
numberSpacing: number;
|
||||
boxSize: number;
|
||||
boxBorderColor: string;
|
||||
symbolWidth: number;
|
||||
},
|
||||
) {
|
||||
const {
|
||||
fontSize,
|
||||
numberSpacing,
|
||||
boxSize,
|
||||
boxBorderColor,
|
||||
symbolWidth,
|
||||
} = options;
|
||||
|
||||
// 计算每个元素的宽度(统一使用,确保对齐)
|
||||
const numberWidth = fontSize * 1.5; // 数字宽度(考虑两位数)
|
||||
// const elementWidth = Math.max(numberWidth, boxSize);
|
||||
const elementWidth = numberWidth;
|
||||
|
||||
// 计算总宽度并居中
|
||||
const totalWidth =
|
||||
elementWidth +
|
||||
numberSpacing +
|
||||
symbolWidth +
|
||||
numberSpacing +
|
||||
elementWidth +
|
||||
numberSpacing +
|
||||
symbolWidth +
|
||||
numberSpacing +
|
||||
elementWidth;
|
||||
|
||||
const startX = x + (width - totalWidth) / 2;
|
||||
let currentX = startX;
|
||||
|
||||
// 答案框的Y坐标
|
||||
const resultBoxY = y;
|
||||
// 数字和符号的Y坐标(与答案框垂直居中对齐)
|
||||
const centerY = resultBoxY + boxSize / 2;
|
||||
|
||||
// 绘制左侧数字
|
||||
const leftCenterX = currentX + elementWidth / 2;
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = `${fontSize}px Arial`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(String(problem.left), leftCenterX, centerY);
|
||||
|
||||
currentX += elementWidth + numberSpacing;
|
||||
|
||||
// 绘制运算符(与数字垂直居中对齐)
|
||||
const operatorX = currentX + symbolWidth / 2;
|
||||
this.drawSymbol(
|
||||
operatorX,
|
||||
centerY,
|
||||
problem.operator,
|
||||
symbolWidth * 0.8,
|
||||
);
|
||||
currentX = operatorX + symbolWidth / 2 + numberSpacing;
|
||||
|
||||
// 绘制右侧数字
|
||||
const rightCenterX = currentX + elementWidth / 2;
|
||||
ctx.fillText(String(problem.right), rightCenterX, centerY);
|
||||
|
||||
currentX += elementWidth + numberSpacing;
|
||||
|
||||
// 绘制等号(与数字垂直居中对齐)
|
||||
const equalsX = currentX + symbolWidth / 2;
|
||||
this.drawSymbol(equalsX, centerY, '=', symbolWidth * 0.8);
|
||||
currentX = equalsX + symbolWidth / 2 + numberSpacing;
|
||||
|
||||
// 绘制答案框
|
||||
const resultBoxX = currentX + (elementWidth - boxSize) / 2;
|
||||
this.drawRoundedRect(resultBoxX, resultBoxY, boxSize, boxSize, {
|
||||
isDashed: false,
|
||||
radius: 4,
|
||||
color: boxBorderColor,
|
||||
lineWidth: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default CalculationPracticeDraw;
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 九九乘法表绘制服务
|
||||
*/
|
||||
|
||||
import { BaseDrawService } from '../../../service/baseDraw';
|
||||
import { getRandomUniqueNumberColors } from '../../../constants/colors';
|
||||
|
||||
/**
|
||||
* 数字转中文(1-99)
|
||||
*/
|
||||
function numberToChinese(num: number): string {
|
||||
const digits = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
|
||||
const tens = [
|
||||
'',
|
||||
'十',
|
||||
'二十',
|
||||
'三十',
|
||||
'四十',
|
||||
'五十',
|
||||
'六十',
|
||||
'七十',
|
||||
'八十',
|
||||
'九十',
|
||||
];
|
||||
|
||||
if (num === 0) return '零';
|
||||
if (num < 10) return digits[num];
|
||||
if (num === 10) return '十';
|
||||
if (num < 20) return '十' + digits[num % 10];
|
||||
if (num % 10 === 0) return tens[Math.floor(num / 10)];
|
||||
return tens[Math.floor(num / 10)] + digits[num % 10];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成九九乘法表汉字口诀
|
||||
* @param a 第一个乘数
|
||||
* @param b 第二个乘数
|
||||
* @param result 结果
|
||||
*/
|
||||
function getChineseFormula(a: number, b: number, result: number): string {
|
||||
const aChinese = numberToChinese(a);
|
||||
const bChinese = numberToChinese(b);
|
||||
const resultChinese = numberToChinese(result);
|
||||
return `${aChinese}${bChinese}得${resultChinese}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 九九乘法表数据
|
||||
*/
|
||||
export interface MultiplicationTableData {
|
||||
// 不需要额外数据,九九乘法表是固定的
|
||||
}
|
||||
|
||||
/**
|
||||
* 九九乘法表绘制服务
|
||||
*/
|
||||
class MultiplicationTableDraw extends BaseDrawService {
|
||||
tableData: MultiplicationTableData | null = null;
|
||||
|
||||
constructor(
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) {
|
||||
super(canvas, ctx, options);
|
||||
this.tableData = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制九九乘法表内容
|
||||
*/
|
||||
async draw(tableData?: MultiplicationTableData) {
|
||||
this.tableData = tableData || {};
|
||||
this.prepareDraw();
|
||||
await this.drawHeaderAndDivider();
|
||||
await this.drawContent({
|
||||
canvas: this.canvas,
|
||||
ctx: this.ctx,
|
||||
canvasWidth: this.canvasWidth,
|
||||
startY: this.currentY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制内容区域
|
||||
*/
|
||||
private async drawContent(params: {
|
||||
canvas: WechatMiniprogram.Canvas;
|
||||
ctx: RenderingContext;
|
||||
canvasWidth: number;
|
||||
startY: number;
|
||||
}): Promise<void> {
|
||||
const { ctx, canvasWidth, startY } = params;
|
||||
|
||||
// 获取9种颜色(1-9列,每列一种颜色)
|
||||
const colors = getRandomUniqueNumberColors(9);
|
||||
|
||||
// 布局参数
|
||||
const margin = 30; // 左右边距
|
||||
const topMargin = 40; // 顶部间距
|
||||
const sectionSpacing = 40; // 上下两部分之间的间距
|
||||
const dividerMargin = 20; // 分割线左右边距
|
||||
|
||||
// 计算可用宽度
|
||||
const availableWidth = canvasWidth - margin * 2;
|
||||
const cellWidth = availableWidth / 9; // 每列宽度
|
||||
const rowHeight = 26; // 每行高度(减小行高)
|
||||
const fontSize = 12; // 字体大小(减小字体)
|
||||
|
||||
let currentY = startY + topMargin;
|
||||
|
||||
// ========== 绘制数字版九九乘法表 ==========
|
||||
const numberTableStartY = currentY;
|
||||
|
||||
// 绘制表格边框和内容
|
||||
for (let row = 1; row <= 9; row++) {
|
||||
const rowY = numberTableStartY + (row - 1) * rowHeight;
|
||||
for (let col = 1; col <= row; col++) {
|
||||
const colX = margin + (col - 1) * cellWidth;
|
||||
const color = colors[col - 1];
|
||||
|
||||
// 绘制单元格边框
|
||||
ctx.strokeStyle = '#333';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(colX, rowY, cellWidth, rowHeight);
|
||||
|
||||
// 生成公式:col x row = result
|
||||
const result = col * row;
|
||||
const formula = `${col}×${row}=${result}`;
|
||||
|
||||
// 绘制公式(带颜色)
|
||||
ctx.fillStyle = color;
|
||||
ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(
|
||||
formula,
|
||||
colX + cellWidth / 2,
|
||||
rowY + rowHeight / 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 数字版结束位置
|
||||
const numberTableEndY = numberTableStartY + 9 * rowHeight;
|
||||
currentY = numberTableEndY + sectionSpacing;
|
||||
|
||||
// ========== 绘制虚线分割线 ==========
|
||||
this.drawLine(
|
||||
dividerMargin,
|
||||
currentY,
|
||||
canvasWidth - dividerMargin,
|
||||
currentY,
|
||||
{
|
||||
isDashed: true,
|
||||
dashPattern: [4, 4],
|
||||
color: '#999',
|
||||
lineWidth: 1,
|
||||
},
|
||||
);
|
||||
|
||||
currentY += sectionSpacing;
|
||||
|
||||
// ========== 绘制汉字版九九乘法表 ==========
|
||||
const chineseTableStartY = currentY;
|
||||
const hzFontSize = 8;
|
||||
|
||||
// 绘制表格边框和内容
|
||||
for (let row = 1; row <= 9; row++) {
|
||||
const rowY = chineseTableStartY + (row - 1) * rowHeight;
|
||||
for (let col = 1; col <= row; col++) {
|
||||
const colX = margin + (col - 1) * cellWidth;
|
||||
const color = colors[col - 1]; // 使用和数字版相同的颜色
|
||||
|
||||
// 绘制单元格边框
|
||||
ctx.strokeStyle = '#333';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(colX, rowY, cellWidth, rowHeight);
|
||||
|
||||
// 生成汉字口诀
|
||||
const result = col * row;
|
||||
const chineseFormula = getChineseFormula(col, row, result);
|
||||
|
||||
// 绘制汉字口诀(带颜色)
|
||||
ctx.fillStyle = color;
|
||||
ctx.font = `bold ${hzFontSize}px "Microsoft Yahei"`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(
|
||||
chineseFormula,
|
||||
colX + cellWidth / 2,
|
||||
rowY + rowHeight / 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MultiplicationTableDraw;
|
||||
@@ -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