feat:九九乘法表

This commit is contained in:
R524809
2026-01-16 18:02:16 +08:00
parent 7dd10c7902
commit 9c13b44806
25 changed files with 1388 additions and 13 deletions
@@ -0,0 +1,14 @@
{
"navigationBarTitleText": "计算练习题",
"navigationBarBackgroundColor": "#FFD719",
"homeButton": true,
"backgroundColor": "#F6F6F6",
"enablePullDownRefresh": false,
"usingComponents": {
"toy-button": "../../ui/button/button",
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
"math-type-selector": "../../components/math-type-selector/math-type-selector",
"draw-ad": "../../components/draw-ad/draw-ad"
}
}
@@ -0,0 +1,2 @@
// 计算练习题页面样式
@import '../../base/baseDrawPage.wxss';
@@ -0,0 +1,208 @@
import CalculationPracticeDraw from '../shared/service/calculationPracticeDraw';
import {
createMathPage,
CanvasDataState,
} from '../shared/common/mathPageMixin';
type OperationType = 'addition' | 'subtraction' | 'mixed';
createMathPage({
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as CalculationPracticeDraw | null,
calculationPracticeData: null as {
problems: Array<{
left: number;
operator: '+' | '-';
right: number;
result: number;
}>;
} | null,
data: {
pageTitle: '计算练习题',
functionId: '',
hasContent: false,
showShareDialog: false,
operationType: 'addition' as OperationType, // 运算类型:加法、减法、混合
currentMode: 'within-10', // 默认10以内
currentModeName: '10以内',
typeActions: [
{ name: '10以内', value: 'within-10' },
{ name: '20以内', value: 'within-20' },
{ name: '50以内', value: 'within-50' },
{ name: '100以内', value: 'within-100' },
],
} as CanvasDataState & {
operationType: OperationType;
currentMode: string;
currentModeName: string;
typeActions: Array<{ name: string; value: string }>;
},
onLoad(options: { id?: string; type?: string }) {
const functionId = options.id || 'practice-addition';
// 根据 functionId 确定运算类型
let operationType: OperationType = 'addition';
if (functionId === 'practice-subtraction') {
operationType = 'subtraction';
} else if (functionId === 'practice-mixed') {
operationType = 'mixed';
} else if (options.type) {
operationType = options.type as OperationType;
}
let pageTitle = '计算练习题';
if (operationType === 'addition') {
pageTitle = '加法运算';
} else if (operationType === 'subtraction') {
pageTitle = '减法运算';
} else if (operationType === 'mixed') {
pageTitle = '混合运算';
}
this.setData({
operationType,
pageTitle,
});
this.initPageInfo(functionId, pageTitle);
},
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new CalculationPracticeDraw(canvas, ctx, options);
},
drawServiceOptions: {},
onCanvasReady: () => {
// 初始随机生成
this.onRandom();
},
});
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService || !this.calculationPracticeData) {
return;
}
try {
await this.drawService.draw(this.calculationPracticeData);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
// 根据当前模式确定最大值
let maxValue = 10;
if (this.data.currentMode === 'within-20') {
maxValue = 20;
} else if (this.data.currentMode === 'within-50') {
maxValue = 50;
} else if (this.data.currentMode === 'within-100') {
maxValue = 100;
}
const problems: Array<{
left: number;
operator: '+' | '-';
right: number;
result: number;
}> = [];
const usedProblems = new Set<string>();
let attempts = 0;
const maxAttempts = 2000;
const targetCount = 11 * 3; // 12行3列 = 36道题
while (problems.length < targetCount && attempts < maxAttempts) {
attempts++;
let left: number;
let right: number;
let operator: '+' | '-';
let result: number;
// 根据运算类型生成题目
if (this.data.operationType === 'addition') {
// 加法:left + right <= maxValue
left = Math.floor(Math.random() * (maxValue - 1)) + 1; // 1 到 maxValue-1
const maxRight = maxValue - left;
right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
operator = '+';
result = left + right;
} else if (this.data.operationType === 'subtraction') {
// 减法:left - right >= 0, left <= maxValue
left = Math.floor(Math.random() * maxValue) + 1; // 1 到 maxValue
right = Math.floor(Math.random() * left) + 1; // 1 到 left
operator = '-';
result = left - right;
} else {
// 混合运算:随机选择加法或减法
const isAddition = Math.random() < 0.5;
if (isAddition) {
left = Math.floor(Math.random() * (maxValue - 1)) + 1;
const maxRight = maxValue - left;
right = Math.floor(Math.random() * maxRight) + 1;
operator = '+';
result = left + right;
} else {
left = Math.floor(Math.random() * maxValue) + 1;
right = Math.floor(Math.random() * left) + 1;
operator = '-';
result = left - right;
}
}
// 使用 "left,operator,right" 作为唯一标识,避免重复
const problemKey = `${left},${operator},${right}`;
if (usedProblems.has(problemKey)) {
continue;
}
usedProblems.add(problemKey);
problems.push({
left,
operator,
right,
result,
});
}
if (problems.length < targetCount) {
console.warn(
`只生成了 ${problems.length} 道题目,可能符合条件的题目组合不足`,
);
}
this.calculationPracticeData = { problems };
this.drawCanvas();
},
/** 选择类型 */
onSelectType(event: any) {
const { name, value } = event.detail;
this.setData({
currentMode: value,
currentModeName: name,
});
// 重新生成数据
this.onRandom();
},
});
@@ -0,0 +1,6 @@
<!-- 模版不能跨包引用 -->
<import src="../shared/templates/canvas-page-template.wxml" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -0,0 +1,13 @@
{
"navigationBarTitleText": "九九乘法表",
"navigationBarBackgroundColor": "#FFD719",
"homeButton": true,
"backgroundColor": "#F6F6F6",
"enablePullDownRefresh": false,
"usingComponents": {
"toy-button": "../../ui/button/button",
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
"draw-ad": "../../components/draw-ad/draw-ad"
}
}
@@ -0,0 +1,2 @@
// 九九乘法表页面样式
@import '../../base/baseDrawPage.wxss';
@@ -0,0 +1,72 @@
import MultiplicationTableDraw from '../shared/service/multiplicationTableDraw';
import {
createMathPage,
CanvasDataState,
} from '../shared/common/mathPageMixin';
createMathPage({
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as MultiplicationTableDraw | null,
multiplicationTableData: null as {} | null,
data: {
pageTitle: '九九乘法表',
functionId: '',
hasContent: false,
showShareDialog: false,
} as CanvasDataState,
onLoad(options: { id?: string }) {
const functionId = options.id || 'multiplication-table';
this.setData({
pageTitle: '九九乘法表',
});
this.initPageInfo(functionId, '九九乘法表');
},
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new MultiplicationTableDraw(canvas, ctx, options);
},
drawServiceOptions: {
title: '九九乘法表',
},
onCanvasReady: () => {
// 初始绘制
this.drawCanvas();
},
});
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService) {
return;
}
try {
await this.drawService.draw({});
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成(重新绘制,颜色会随机变化)
*/
onRandom() {
this.drawCanvas();
},
});
@@ -0,0 +1,6 @@
<!-- 模版不能跨包引用 -->
<import src="../shared/templates/canvas-page-template.wxml" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -0,0 +1,13 @@
{
"navigationBarTitleText": "一位数加法",
"navigationBarBackgroundColor": "#FFD719",
"homeButton": true,
"backgroundColor": "#F6F6F6",
"enablePullDownRefresh": false,
"usingComponents": {
"toy-button": "../../ui/button/button",
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
"draw-ad": "../../components/draw-ad/draw-ad"
}
}
@@ -0,0 +1,3 @@
// 一位数加法练习页面样式
@import '../../base/baseDrawPage.wxss';
@@ -0,0 +1,118 @@
import OneDigitAdditionDraw from '../shared/service/oneDigitAdditionDraw';
import {
createMathPage,
CanvasDataState,
} from '../shared/common/mathPageMixin';
createMathPage({
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as OneDigitAdditionDraw | null,
oneDigitAdditionData: null as {
problems: Array<{
left: number;
right: number;
result: number;
}>;
} | null,
data: {
pageTitle: '一位数加法',
subTitle: '通过圆点学习一位数加法运算',
functionId: '',
hasContent: false,
showShareDialog: false,
} as CanvasDataState,
onLoad(options: { id?: string }) {
const functionId = options.id || 'one-digit-addition';
this.initPageInfo(functionId, '一位数加法');
},
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new OneDigitAdditionDraw(canvas, ctx, options);
},
drawServiceOptions: {
subTitle: this.data.subTitle,
},
onCanvasReady: () => {
// 初始随机生成
this.onRandom();
},
});
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService || !this.oneDigitAdditionData) {
return;
}
try {
await this.drawService.draw(this.oneDigitAdditionData);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
const problems: Array<{
left: number;
right: number;
result: number;
}> = [];
const usedProblems = new Set<string>(); // 用于记录已生成的题目,避免重复
// 生成6道不重复的题目(6行1列)
let attempts = 0;
const maxAttempts = 1000; // 最大尝试次数,避免无限循环
while (problems.length < 6 && attempts < maxAttempts) {
attempts++;
// 两个加数的和 <= 10
// left >= 1, right >= 1, left + right <= 10
const maxSum = 10;
const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9
const maxRight = maxSum - left; // 确保 left + right <= 10
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
const result = left + right;
// 使用 "left,right" 作为唯一标识,避免重复
const problemKey = `${left},${right}`;
if (usedProblems.has(problemKey)) {
continue;
}
usedProblems.add(problemKey);
problems.push({
left,
right,
result,
});
}
if (problems.length < 6) {
console.warn(
`只生成了 ${problems.length} 道题目,可能符合条件的题目组合不足`,
);
}
this.oneDigitAdditionData = { problems };
this.drawCanvas();
},
});
@@ -0,0 +1,6 @@
<!-- 模版不能跨包引用 -->
<import src="../shared/templates/canvas-page-template.wxml" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -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;