feat:九九乘法表
This commit is contained in:
@@ -23,7 +23,10 @@
|
||||
"makeTen/makeTen",
|
||||
"breakTen/breakTen",
|
||||
"flatTen/flatTen",
|
||||
"borrowTen/borrowTen"
|
||||
"borrowTen/borrowTen",
|
||||
"oneDigitAddition/oneDigitAddition",
|
||||
"calculationPractice/calculationPractice",
|
||||
"multiplicationTable/multiplicationTable"
|
||||
],
|
||||
"independent": false
|
||||
},
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -10,7 +10,7 @@ export const defaultPrintConfig: PrintConfig = {
|
||||
// header: 'LogoImage',
|
||||
header: 'wechat',
|
||||
appName: '涂鸦丫小程序',
|
||||
appHint: '数学|专注力|练字|涂鸦',
|
||||
appHint: '数学|专注力|识字|练字',
|
||||
};
|
||||
|
||||
export const defaultShareConfig = {
|
||||
|
||||
@@ -8,6 +8,22 @@ export interface MathFunctionType {
|
||||
}
|
||||
|
||||
export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
|
||||
{
|
||||
id: 'number-sort',
|
||||
page: 'numberSort',
|
||||
title: '数字排序',
|
||||
desc: '数字排序,写出正确顺序',
|
||||
icon: '🔢',
|
||||
img: '/assets/mathEntrance/number-sort.png',
|
||||
},
|
||||
{
|
||||
id: 'one-digit-addition',
|
||||
page: 'oneDigitAddition',
|
||||
title: '一位数加法',
|
||||
desc: '通过圆点学习一位数加法运算',
|
||||
icon: '➕',
|
||||
img: '/assets/mathEntrance/one-digit-addition.png',
|
||||
},
|
||||
{
|
||||
id: 'make-ten',
|
||||
page: 'makeTen',
|
||||
@@ -140,14 +156,6 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
|
||||
icon: '❓',
|
||||
img: '/assets/mathEntrance/missing-number.png',
|
||||
},
|
||||
{
|
||||
id: 'number-sort',
|
||||
page: 'numberSort',
|
||||
title: '数字排序',
|
||||
desc: '数字排序,写出正确顺序',
|
||||
icon: '🔢',
|
||||
img: '/assets/mathEntrance/number-sort.png',
|
||||
},
|
||||
|
||||
// 第四阶段:简单运算(从易到难)
|
||||
{
|
||||
@@ -182,4 +190,38 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
|
||||
icon: '±',
|
||||
img: '/assets/mathEntrance/addition-subtraction-10.png',
|
||||
},
|
||||
// 计算练习题
|
||||
{
|
||||
id: 'practice-addition',
|
||||
page: 'calculationPractice',
|
||||
title: '加法运算',
|
||||
desc: '练习10/20/50/100以内加法运算',
|
||||
icon: '➕',
|
||||
img: '/assets/mathEntrance/practice-addition.png',
|
||||
},
|
||||
{
|
||||
id: 'practice-subtraction',
|
||||
page: 'calculationPractice',
|
||||
title: '减法运算',
|
||||
desc: '练习10/20/50/100以内减法运算',
|
||||
icon: '➖',
|
||||
img: '/assets/mathEntrance/practice-subtraction.png',
|
||||
},
|
||||
{
|
||||
id: 'practice-mixed',
|
||||
page: 'calculationPractice',
|
||||
title: '混合运算',
|
||||
desc: '练习10/20/50/100以内加减法混合运算',
|
||||
icon: '±',
|
||||
img: '/assets/mathEntrance/practice-mixed.png',
|
||||
},
|
||||
// 九九乘法表
|
||||
{
|
||||
id: 'multiplication-table',
|
||||
page: 'multiplicationTable',
|
||||
title: '九九乘法表',
|
||||
desc: '学习九九乘法口诀',
|
||||
icon: '✖️',
|
||||
img: '/assets/mathEntrance/multiplication-table.png',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
@@ -172,6 +172,58 @@ export class BaseDrawService {
|
||||
ctx.setLineDash([]); // 重置为实线
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制线条(逻辑像素,尺寸除以3)
|
||||
* @param x1 起点X坐标
|
||||
* @param y1 起点Y坐标
|
||||
* @param x2 终点X坐标
|
||||
* @param y2 终点Y坐标
|
||||
* @param options 可选参数
|
||||
* @param options.isDashed 是否为虚线,默认为true(虚线)
|
||||
* @param options.dashPattern 虚线模式,默认为[4, 4]
|
||||
* @param options.color 线条颜色,默认为'#999'
|
||||
* @param options.lineWidth 线条宽度,默认为1
|
||||
*/
|
||||
drawLine(
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
options?: {
|
||||
isDashed?: boolean;
|
||||
dashPattern?: number[];
|
||||
color?: string;
|
||||
lineWidth?: number;
|
||||
},
|
||||
): void {
|
||||
const { ctx } = this;
|
||||
const {
|
||||
isDashed = true,
|
||||
dashPattern = [4, 4],
|
||||
color = '#999',
|
||||
lineWidth = 1,
|
||||
} = options || {};
|
||||
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = lineWidth;
|
||||
|
||||
// 设置虚线或实线
|
||||
if (isDashed) {
|
||||
ctx.setLineDash(dashPattern);
|
||||
} else {
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
// 绘制线条
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
|
||||
// 重置为实线(避免影响后续绘制)
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制网格线(逻辑像素,尺寸除以3)
|
||||
* @param gridStartX 网格起始X坐标
|
||||
@@ -287,7 +339,7 @@ export class BaseDrawService {
|
||||
* 绘制符号(使用路径绘制)
|
||||
* @param x 符号中心X坐标
|
||||
* @param y 符号中心Y坐标
|
||||
* @param symbol 符号类型:'+', '-', '×', '✓'
|
||||
* @param symbol 符号类型:'+', '-', '=', '×', '✓'
|
||||
* @param size 符号大小
|
||||
*/
|
||||
drawSymbol(x: number, y: number, symbol: string, size: number): void {
|
||||
@@ -325,6 +377,19 @@ export class BaseDrawService {
|
||||
ctx.stroke();
|
||||
break;
|
||||
|
||||
case '=':
|
||||
// 等号:两条横线
|
||||
const equalsSpacing = size * 0.15; // 两条线之间的间距
|
||||
ctx.beginPath();
|
||||
// 上横线
|
||||
ctx.moveTo(-strokeLength, -equalsSpacing);
|
||||
ctx.lineTo(strokeLength, -equalsSpacing);
|
||||
// 下横线
|
||||
ctx.moveTo(-strokeLength, equalsSpacing);
|
||||
ctx.lineTo(strokeLength, equalsSpacing);
|
||||
ctx.stroke();
|
||||
break;
|
||||
|
||||
case '×':
|
||||
// 乘号:两条斜线
|
||||
ctx.beginPath();
|
||||
|
||||
@@ -23,12 +23,33 @@
|
||||
"condition": {
|
||||
"miniprogram": {
|
||||
"list": [
|
||||
{
|
||||
"name": "mathPages/multiplicationTable/multiplicationTable",
|
||||
"pathName": "mathPages/multiplicationTable/multiplicationTable",
|
||||
"query": "id=multiplication-table",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
},
|
||||
{
|
||||
"name": "mathPages/calculationPractice/calculationPractice",
|
||||
"pathName": "mathPages/calculationPractice/calculationPractice",
|
||||
"query": "id=practice-addition",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "mathPages/oneDigitAddition/oneDigitAddition",
|
||||
"pathName": "mathPages/oneDigitAddition/oneDigitAddition",
|
||||
"query": "id=one-digit-addition",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "mathPages/borrowTen/borrowTen",
|
||||
"pathName": "mathPages/borrowTen/borrowTen",
|
||||
"query": "id=borrow-ten",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "mathPages/flatTen/flatTen",
|
||||
|
||||
Reference in New Issue
Block a user