diff --git a/miniprogram/app.json b/miniprogram/app.json
index 3f28489..019b315 100644
--- a/miniprogram/app.json
+++ b/miniprogram/app.json
@@ -17,7 +17,8 @@
"missingNumber/missingNumber",
"compare/compare",
"countingSelect/countingSelect",
- "numberDecompose/numberDecompose"
+ "numberDecompose/numberDecompose",
+ "numberSort/numberSort"
],
"independent": false
},
diff --git a/miniprogram/constants/mathFunctions.ts b/miniprogram/constants/mathFunctions.ts
index 9525ea2..dad8145 100644
--- a/miniprogram/constants/mathFunctions.ts
+++ b/miniprogram/constants/mathFunctions.ts
@@ -75,6 +75,13 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
icon: '❓',
img: '/assets/mathEntrance/missing-number.png',
},
+ {
+ id: 'number-sort',
+ page: 'numberSort',
+ title: '数字排序',
+ desc: '数字牌被打乱了,把正确的数字按顺序写下来吧~',
+ icon: '🔢',
+ },
{
id: 'number-decompose',
page: 'numberDecompose',
diff --git a/miniprogram/focusPages/codeConnect/codeConnect.wxml b/miniprogram/focusPages/codeConnect/codeConnect.wxml
index b475771..3b70e13 100644
--- a/miniprogram/focusPages/codeConnect/codeConnect.wxml
+++ b/miniprogram/focusPages/codeConnect/codeConnect.wxml
@@ -1,38 +1,6 @@
-
-
- 预览打印效果
+
+
-
-
-
-
-
-
-
-
- 随机生成
-
-
-
-
-
-
-
-
+
diff --git a/miniprogram/focusPages/colorPattern/colorPattern.wxml b/miniprogram/focusPages/colorPattern/colorPattern.wxml
index b475771..3b70e13 100644
--- a/miniprogram/focusPages/colorPattern/colorPattern.wxml
+++ b/miniprogram/focusPages/colorPattern/colorPattern.wxml
@@ -1,38 +1,6 @@
-
-
- 预览打印效果
+
+
-
-
-
-
-
-
-
-
- 随机生成
-
-
-
-
-
-
-
-
+
diff --git a/miniprogram/focusPages/gridDrawing/gridDrawing.wxml b/miniprogram/focusPages/gridDrawing/gridDrawing.wxml
index 17d4f8a..aa6a456 100644
--- a/miniprogram/focusPages/gridDrawing/gridDrawing.wxml
+++ b/miniprogram/focusPages/gridDrawing/gridDrawing.wxml
@@ -1,32 +1,15 @@
-
-
- 预览打印效果
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/miniprogram/focusPages/gridReasoning/gridReasoning.wxml b/miniprogram/focusPages/gridReasoning/gridReasoning.wxml
index 092df36..da2d7d5 100644
--- a/miniprogram/focusPages/gridReasoning/gridReasoning.wxml
+++ b/miniprogram/focusPages/gridReasoning/gridReasoning.wxml
@@ -1,31 +1,15 @@
-
-
- 预览打印效果
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/miniprogram/focusPages/lineRecognition/lineRecognition.wxml b/miniprogram/focusPages/lineRecognition/lineRecognition.wxml
index b475771..3b70e13 100644
--- a/miniprogram/focusPages/lineRecognition/lineRecognition.wxml
+++ b/miniprogram/focusPages/lineRecognition/lineRecognition.wxml
@@ -1,38 +1,6 @@
-
-
- 预览打印效果
+
+
-
-
-
-
-
-
-
-
- 随机生成
-
-
-
-
-
-
-
-
+
diff --git a/miniprogram/focusPages/matchConnect/matchConnect.wxml b/miniprogram/focusPages/matchConnect/matchConnect.wxml
index b475771..3b70e13 100644
--- a/miniprogram/focusPages/matchConnect/matchConnect.wxml
+++ b/miniprogram/focusPages/matchConnect/matchConnect.wxml
@@ -1,38 +1,6 @@
-
-
- 预览打印效果
+
+
-
-
-
-
-
-
-
-
- 随机生成
-
-
-
-
-
-
-
-
+
diff --git a/miniprogram/focusPages/positionColoring/positionColoring.wxml b/miniprogram/focusPages/positionColoring/positionColoring.wxml
index b475771..3b70e13 100644
--- a/miniprogram/focusPages/positionColoring/positionColoring.wxml
+++ b/miniprogram/focusPages/positionColoring/positionColoring.wxml
@@ -1,38 +1,6 @@
-
-
- 预览打印效果
+
+
-
-
-
-
-
-
-
-
- 随机生成
-
-
-
-
-
-
-
-
+
diff --git a/miniprogram/focusPages/shared/service/codeConnectDraw.ts b/miniprogram/focusPages/shared/service/codeConnectDraw.ts
index 870be13..5ce4059 100644
--- a/miniprogram/focusPages/shared/service/codeConnectDraw.ts
+++ b/miniprogram/focusPages/shared/service/codeConnectDraw.ts
@@ -50,20 +50,9 @@ class CodeConnectDraw extends BaseDrawService {
return;
}
- this.setPrintConfig();
this.codeData = codeData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域
- this.drawDivider();
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
diff --git a/miniprogram/focusPages/shared/service/colorPatternDraw.ts b/miniprogram/focusPages/shared/service/colorPatternDraw.ts
index 270a346..2438ddb 100644
--- a/miniprogram/focusPages/shared/service/colorPatternDraw.ts
+++ b/miniprogram/focusPages/shared/service/colorPatternDraw.ts
@@ -41,20 +41,9 @@ class ColorPatternDraw extends BaseDrawService {
return;
}
- this.setPrintConfig();
this.gridData = gridData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域
- this.drawDivider();
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
this.drawContent({
ctx: this.ctx,
gridData: this.gridData,
diff --git a/miniprogram/focusPages/shared/service/gridDraw.ts b/miniprogram/focusPages/shared/service/gridDraw.ts
index d7a273a..60b5fe8 100644
--- a/miniprogram/focusPages/shared/service/gridDraw.ts
+++ b/miniprogram/focusPages/shared/service/gridDraw.ts
@@ -51,20 +51,9 @@ class GridDraw extends BaseDrawService {
return;
}
- this.setPrintConfig();
this.gridData = gridData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域(网格和填充)
- this.drawDivider();
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
await this.drawGridContent({
canvas: this.canvas,
ctx: this.ctx,
diff --git a/miniprogram/focusPages/shared/service/gridReasoningDraw.ts b/miniprogram/focusPages/shared/service/gridReasoningDraw.ts
index d9fea61..631cd1b 100644
--- a/miniprogram/focusPages/shared/service/gridReasoningDraw.ts
+++ b/miniprogram/focusPages/shared/service/gridReasoningDraw.ts
@@ -60,20 +60,9 @@ class GridReasoningDraw extends BaseDrawService {
return;
}
- this.setPrintConfig();
this.gridData = gridData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域
- this.drawDivider();
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
diff --git a/miniprogram/focusPages/shared/service/lineRecognitionDraw.ts b/miniprogram/focusPages/shared/service/lineRecognitionDraw.ts
index e399fa9..675761c 100644
--- a/miniprogram/focusPages/shared/service/lineRecognitionDraw.ts
+++ b/miniprogram/focusPages/shared/service/lineRecognitionDraw.ts
@@ -83,20 +83,9 @@ class LineRecognitionDraw extends BaseDrawService {
await this.loadSpiralLineImage();
}
- this.setPrintConfig();
this.lineData = lineData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域
- this.drawDivider();
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
diff --git a/miniprogram/focusPages/shared/service/matchConnectDraw.ts b/miniprogram/focusPages/shared/service/matchConnectDraw.ts
index 6746759..86af9b0 100644
--- a/miniprogram/focusPages/shared/service/matchConnectDraw.ts
+++ b/miniprogram/focusPages/shared/service/matchConnectDraw.ts
@@ -39,20 +39,9 @@ class MatchConnectDraw extends BaseDrawService {
return;
}
- this.setPrintConfig();
this.matchData = matchData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域
- this.drawDivider();
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
diff --git a/miniprogram/focusPages/shared/service/positionColoringDraw.ts b/miniprogram/focusPages/shared/service/positionColoringDraw.ts
index e0f50f3..c069f66 100644
--- a/miniprogram/focusPages/shared/service/positionColoringDraw.ts
+++ b/miniprogram/focusPages/shared/service/positionColoringDraw.ts
@@ -40,20 +40,9 @@ class PositionColoringDraw extends BaseDrawService {
return;
}
- this.setPrintConfig();
this.gridData = gridData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域
- this.drawDivider();
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
diff --git a/miniprogram/focusPages/shared/service/shapeSymbolDraw.ts b/miniprogram/focusPages/shared/service/shapeSymbolDraw.ts
index f6a828d..955e6c4 100644
--- a/miniprogram/focusPages/shared/service/shapeSymbolDraw.ts
+++ b/miniprogram/focusPages/shared/service/shapeSymbolDraw.ts
@@ -45,20 +45,9 @@ class ShapeSymbolDraw extends BaseDrawService {
return;
}
- this.setPrintConfig();
this.gridData = gridData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域
- this.drawDivider();
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
this.drawContent({
ctx: this.ctx,
gridData: this.gridData,
diff --git a/miniprogram/focusPages/shared/templates/canvas-page-template.wxml b/miniprogram/focusPages/shared/templates/canvas-page-template.wxml
new file mode 100644
index 0000000..7d0d932
--- /dev/null
+++ b/miniprogram/focusPages/shared/templates/canvas-page-template.wxml
@@ -0,0 +1,78 @@
+
+
+
+
+ 预览打印效果
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 随机生成
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/mathPages/numberSort/numberSort.json b/miniprogram/mathPages/numberSort/numberSort.json
new file mode 100644
index 0000000..2e3e057
--- /dev/null
+++ b/miniprogram/mathPages/numberSort/numberSort.json
@@ -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"
+ }
+}
diff --git a/miniprogram/mathPages/numberSort/numberSort.less b/miniprogram/mathPages/numberSort/numberSort.less
new file mode 100644
index 0000000..e2b2a7b
--- /dev/null
+++ b/miniprogram/mathPages/numberSort/numberSort.less
@@ -0,0 +1 @@
+@import '../../base/baseDrawPage.wxss';
\ No newline at end of file
diff --git a/miniprogram/mathPages/numberSort/numberSort.ts b/miniprogram/mathPages/numberSort/numberSort.ts
new file mode 100644
index 0000000..9ceeb2e
--- /dev/null
+++ b/miniprogram/mathPages/numberSort/numberSort.ts
@@ -0,0 +1,132 @@
+import NumberSortDraw from '../shared/service/numberSortDraw';
+import {
+ createMathPage,
+ CanvasDataState,
+} from '../shared/common/mathPageMixin';
+import { NumberSortData } from '../shared/service/numberSortDraw';
+import { NUMBER_COLORS, getNumberColors } from '../../constants/colors';
+
+createMathPage({
+ canvas: null as Canvas | null,
+ ctx: null as RenderingContext | null,
+ boxHeight: 0,
+ boxWidth: 0,
+ drawService: null as NumberSortDraw | null,
+ sortData: null as NumberSortData | null,
+
+ data: {
+ pageTitle: '数字排序',
+ functionId: '',
+ hasContent: false,
+ showShareDialog: false,
+ } as CanvasDataState,
+
+ onLoad(options: { id?: string }) {
+ const functionId = options.id || 'number-sort';
+ this.setData({
+ functionId,
+ });
+ this.initPageInfo(functionId, '数字排序');
+ },
+
+ onReady() {
+ this.initCanvas({
+ createDrawService: (
+ canvas: Canvas,
+ ctx: RenderingContext,
+ options?: Record,
+ ) => {
+ return new NumberSortDraw(canvas, ctx, options);
+ },
+ drawServiceOptions: {
+ title: this.data.pageTitle,
+ subTitle: '数字牌被打乱了,把正确的数字按顺序写下来吧~',
+ functionId: this.data.functionId,
+ },
+ onCanvasReady: () => {
+ // 初始随机生成
+ this.onRandom();
+ },
+ });
+ },
+
+ /**
+ * 绘制Canvas内容
+ */
+ async drawCanvas() {
+ if (!this.ctx || !this.drawService || !this.sortData) {
+ return;
+ }
+
+ try {
+ await this.drawService.draw(this.sortData);
+ this.setData({ hasContent: true });
+ } catch (error) {
+ console.error('绘制失败:', error);
+ this.setData({ hasContent: false });
+ }
+ },
+
+ /**
+ * 随机生成
+ */
+ onRandom() {
+ const groups: NumberSortData['groups'] = [];
+ const availableColors = getNumberColors();
+
+ // 生成6组题目
+ for (let i = 0; i < 6; i++) {
+ // 随机选择4个不同的数字(范围1-9)
+ const availableNumbers = Array.from({ length: 9 }, (_, i) => i + 1);
+ const selectedNumbers: number[] = [];
+
+ // 随机选择4个数字
+ for (let j = 0; j < 4; j++) {
+ const randomIndex = Math.floor(
+ Math.random() * availableNumbers.length,
+ );
+ selectedNumbers.push(
+ availableNumbers.splice(randomIndex, 1)[0],
+ );
+ }
+
+ // 打乱顺序
+ const shuffledNumbers = [...selectedNumbers];
+ for (let j = shuffledNumbers.length - 1; j > 0; j--) {
+ const k = Math.floor(Math.random() * (j + 1));
+ [shuffledNumbers[j], shuffledNumbers[k]] = [
+ shuffledNumbers[k],
+ shuffledNumbers[j],
+ ];
+ }
+
+ // 为每个数字分配颜色和倾斜角度
+ const numbersWithColors = shuffledNumbers.map((number) => {
+ // 使用数字对应的颜色,如果数字超出范围则随机选择
+ const color =
+ NUMBER_COLORS[number as keyof typeof NUMBER_COLORS] ||
+ availableColors[
+ Math.floor(Math.random() * availableColors.length)
+ ];
+ // 生成随机倾斜角度(-30度到+30度,转换为弧度)
+ const angleDegrees = (Math.random() - 0.5) * 60; // -30到+30度
+ const angle = (angleDegrees * Math.PI) / 180; // 转换为弧度
+ return {
+ number,
+ color,
+ angle,
+ };
+ });
+
+ groups.push({
+ numbers: numbersWithColors,
+ });
+ }
+
+ this.sortData = {
+ groups,
+ };
+
+ this.drawCanvas();
+ },
+});
diff --git a/miniprogram/mathPages/numberSort/numberSort.wxml b/miniprogram/mathPages/numberSort/numberSort.wxml
new file mode 100644
index 0000000..9ee45f2
--- /dev/null
+++ b/miniprogram/mathPages/numberSort/numberSort.wxml
@@ -0,0 +1,38 @@
+
+
+ 预览打印效果
+
+
+
+
+
+
+
+
+
+ 随机生成
+
+
+
+
+
+
+
+
diff --git a/miniprogram/mathPages/assets/animal-number/animal-number-1.png b/miniprogram/mathPages/shared/assets/animal-number/animal-number-1.png
similarity index 100%
rename from miniprogram/mathPages/assets/animal-number/animal-number-1.png
rename to miniprogram/mathPages/shared/assets/animal-number/animal-number-1.png
diff --git a/miniprogram/mathPages/assets/animal-number/animal-number-10.png b/miniprogram/mathPages/shared/assets/animal-number/animal-number-10.png
similarity index 100%
rename from miniprogram/mathPages/assets/animal-number/animal-number-10.png
rename to miniprogram/mathPages/shared/assets/animal-number/animal-number-10.png
diff --git a/miniprogram/mathPages/assets/animal-number/animal-number-2.png b/miniprogram/mathPages/shared/assets/animal-number/animal-number-2.png
similarity index 100%
rename from miniprogram/mathPages/assets/animal-number/animal-number-2.png
rename to miniprogram/mathPages/shared/assets/animal-number/animal-number-2.png
diff --git a/miniprogram/mathPages/assets/animal-number/animal-number-3.png b/miniprogram/mathPages/shared/assets/animal-number/animal-number-3.png
similarity index 100%
rename from miniprogram/mathPages/assets/animal-number/animal-number-3.png
rename to miniprogram/mathPages/shared/assets/animal-number/animal-number-3.png
diff --git a/miniprogram/mathPages/assets/animal-number/animal-number-4.png b/miniprogram/mathPages/shared/assets/animal-number/animal-number-4.png
similarity index 100%
rename from miniprogram/mathPages/assets/animal-number/animal-number-4.png
rename to miniprogram/mathPages/shared/assets/animal-number/animal-number-4.png
diff --git a/miniprogram/mathPages/assets/animal-number/animal-number-5.png b/miniprogram/mathPages/shared/assets/animal-number/animal-number-5.png
similarity index 100%
rename from miniprogram/mathPages/assets/animal-number/animal-number-5.png
rename to miniprogram/mathPages/shared/assets/animal-number/animal-number-5.png
diff --git a/miniprogram/mathPages/assets/animal-number/animal-number-6.png b/miniprogram/mathPages/shared/assets/animal-number/animal-number-6.png
similarity index 100%
rename from miniprogram/mathPages/assets/animal-number/animal-number-6.png
rename to miniprogram/mathPages/shared/assets/animal-number/animal-number-6.png
diff --git a/miniprogram/mathPages/assets/animal-number/animal-number-7.png b/miniprogram/mathPages/shared/assets/animal-number/animal-number-7.png
similarity index 100%
rename from miniprogram/mathPages/assets/animal-number/animal-number-7.png
rename to miniprogram/mathPages/shared/assets/animal-number/animal-number-7.png
diff --git a/miniprogram/mathPages/assets/animal-number/animal-number-8.png b/miniprogram/mathPages/shared/assets/animal-number/animal-number-8.png
similarity index 100%
rename from miniprogram/mathPages/assets/animal-number/animal-number-8.png
rename to miniprogram/mathPages/shared/assets/animal-number/animal-number-8.png
diff --git a/miniprogram/mathPages/assets/animal-number/animal-number-9.png b/miniprogram/mathPages/shared/assets/animal-number/animal-number-9.png
similarity index 100%
rename from miniprogram/mathPages/assets/animal-number/animal-number-9.png
rename to miniprogram/mathPages/shared/assets/animal-number/animal-number-9.png
diff --git a/miniprogram/mathPages/assets/finger/finger-1.png b/miniprogram/mathPages/shared/assets/finger/finger-1.png
similarity index 100%
rename from miniprogram/mathPages/assets/finger/finger-1.png
rename to miniprogram/mathPages/shared/assets/finger/finger-1.png
diff --git a/miniprogram/mathPages/assets/finger/finger-10.png b/miniprogram/mathPages/shared/assets/finger/finger-10.png
similarity index 100%
rename from miniprogram/mathPages/assets/finger/finger-10.png
rename to miniprogram/mathPages/shared/assets/finger/finger-10.png
diff --git a/miniprogram/mathPages/assets/finger/finger-2.png b/miniprogram/mathPages/shared/assets/finger/finger-2.png
similarity index 100%
rename from miniprogram/mathPages/assets/finger/finger-2.png
rename to miniprogram/mathPages/shared/assets/finger/finger-2.png
diff --git a/miniprogram/mathPages/assets/finger/finger-3.png b/miniprogram/mathPages/shared/assets/finger/finger-3.png
similarity index 100%
rename from miniprogram/mathPages/assets/finger/finger-3.png
rename to miniprogram/mathPages/shared/assets/finger/finger-3.png
diff --git a/miniprogram/mathPages/assets/finger/finger-4.png b/miniprogram/mathPages/shared/assets/finger/finger-4.png
similarity index 100%
rename from miniprogram/mathPages/assets/finger/finger-4.png
rename to miniprogram/mathPages/shared/assets/finger/finger-4.png
diff --git a/miniprogram/mathPages/assets/finger/finger-5.png b/miniprogram/mathPages/shared/assets/finger/finger-5.png
similarity index 100%
rename from miniprogram/mathPages/assets/finger/finger-5.png
rename to miniprogram/mathPages/shared/assets/finger/finger-5.png
diff --git a/miniprogram/mathPages/assets/finger/finger-6.png b/miniprogram/mathPages/shared/assets/finger/finger-6.png
similarity index 100%
rename from miniprogram/mathPages/assets/finger/finger-6.png
rename to miniprogram/mathPages/shared/assets/finger/finger-6.png
diff --git a/miniprogram/mathPages/assets/finger/finger-7.png b/miniprogram/mathPages/shared/assets/finger/finger-7.png
similarity index 100%
rename from miniprogram/mathPages/assets/finger/finger-7.png
rename to miniprogram/mathPages/shared/assets/finger/finger-7.png
diff --git a/miniprogram/mathPages/assets/finger/finger-8.png b/miniprogram/mathPages/shared/assets/finger/finger-8.png
similarity index 100%
rename from miniprogram/mathPages/assets/finger/finger-8.png
rename to miniprogram/mathPages/shared/assets/finger/finger-8.png
diff --git a/miniprogram/mathPages/assets/finger/finger-9.png b/miniprogram/mathPages/shared/assets/finger/finger-9.png
similarity index 100%
rename from miniprogram/mathPages/assets/finger/finger-9.png
rename to miniprogram/mathPages/shared/assets/finger/finger-9.png
diff --git a/miniprogram/mathPages/assets/fruits/1.png b/miniprogram/mathPages/shared/assets/fruits/1.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/1.png
rename to miniprogram/mathPages/shared/assets/fruits/1.png
diff --git a/miniprogram/mathPages/assets/fruits/10.png b/miniprogram/mathPages/shared/assets/fruits/10.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/10.png
rename to miniprogram/mathPages/shared/assets/fruits/10.png
diff --git a/miniprogram/mathPages/assets/fruits/11.png b/miniprogram/mathPages/shared/assets/fruits/11.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/11.png
rename to miniprogram/mathPages/shared/assets/fruits/11.png
diff --git a/miniprogram/mathPages/assets/fruits/12.png b/miniprogram/mathPages/shared/assets/fruits/12.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/12.png
rename to miniprogram/mathPages/shared/assets/fruits/12.png
diff --git a/miniprogram/mathPages/assets/fruits/13.png b/miniprogram/mathPages/shared/assets/fruits/13.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/13.png
rename to miniprogram/mathPages/shared/assets/fruits/13.png
diff --git a/miniprogram/mathPages/assets/fruits/14.png b/miniprogram/mathPages/shared/assets/fruits/14.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/14.png
rename to miniprogram/mathPages/shared/assets/fruits/14.png
diff --git a/miniprogram/mathPages/assets/fruits/15.png b/miniprogram/mathPages/shared/assets/fruits/15.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/15.png
rename to miniprogram/mathPages/shared/assets/fruits/15.png
diff --git a/miniprogram/mathPages/assets/fruits/16.png b/miniprogram/mathPages/shared/assets/fruits/16.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/16.png
rename to miniprogram/mathPages/shared/assets/fruits/16.png
diff --git a/miniprogram/mathPages/assets/fruits/17.png b/miniprogram/mathPages/shared/assets/fruits/17.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/17.png
rename to miniprogram/mathPages/shared/assets/fruits/17.png
diff --git a/miniprogram/mathPages/assets/fruits/18.png b/miniprogram/mathPages/shared/assets/fruits/18.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/18.png
rename to miniprogram/mathPages/shared/assets/fruits/18.png
diff --git a/miniprogram/mathPages/assets/fruits/19.png b/miniprogram/mathPages/shared/assets/fruits/19.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/19.png
rename to miniprogram/mathPages/shared/assets/fruits/19.png
diff --git a/miniprogram/mathPages/assets/fruits/2.png b/miniprogram/mathPages/shared/assets/fruits/2.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/2.png
rename to miniprogram/mathPages/shared/assets/fruits/2.png
diff --git a/miniprogram/mathPages/assets/fruits/20.png b/miniprogram/mathPages/shared/assets/fruits/20.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/20.png
rename to miniprogram/mathPages/shared/assets/fruits/20.png
diff --git a/miniprogram/mathPages/assets/fruits/21.png b/miniprogram/mathPages/shared/assets/fruits/21.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/21.png
rename to miniprogram/mathPages/shared/assets/fruits/21.png
diff --git a/miniprogram/mathPages/assets/fruits/22.png b/miniprogram/mathPages/shared/assets/fruits/22.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/22.png
rename to miniprogram/mathPages/shared/assets/fruits/22.png
diff --git a/miniprogram/mathPages/assets/fruits/3.png b/miniprogram/mathPages/shared/assets/fruits/3.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/3.png
rename to miniprogram/mathPages/shared/assets/fruits/3.png
diff --git a/miniprogram/mathPages/assets/fruits/4.png b/miniprogram/mathPages/shared/assets/fruits/4.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/4.png
rename to miniprogram/mathPages/shared/assets/fruits/4.png
diff --git a/miniprogram/mathPages/assets/fruits/5.png b/miniprogram/mathPages/shared/assets/fruits/5.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/5.png
rename to miniprogram/mathPages/shared/assets/fruits/5.png
diff --git a/miniprogram/mathPages/assets/fruits/6.png b/miniprogram/mathPages/shared/assets/fruits/6.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/6.png
rename to miniprogram/mathPages/shared/assets/fruits/6.png
diff --git a/miniprogram/mathPages/assets/fruits/7.png b/miniprogram/mathPages/shared/assets/fruits/7.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/7.png
rename to miniprogram/mathPages/shared/assets/fruits/7.png
diff --git a/miniprogram/mathPages/assets/fruits/8.png b/miniprogram/mathPages/shared/assets/fruits/8.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/8.png
rename to miniprogram/mathPages/shared/assets/fruits/8.png
diff --git a/miniprogram/mathPages/assets/fruits/9.png b/miniprogram/mathPages/shared/assets/fruits/9.png
similarity index 100%
rename from miniprogram/mathPages/assets/fruits/9.png
rename to miniprogram/mathPages/shared/assets/fruits/9.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/1.png b/miniprogram/mathPages/shared/assets/twelve-animals/1.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/1.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/1.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/10.png b/miniprogram/mathPages/shared/assets/twelve-animals/10.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/10.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/10.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/11.png b/miniprogram/mathPages/shared/assets/twelve-animals/11.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/11.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/11.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/12.png b/miniprogram/mathPages/shared/assets/twelve-animals/12.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/12.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/12.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/2.png b/miniprogram/mathPages/shared/assets/twelve-animals/2.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/2.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/2.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/3.png b/miniprogram/mathPages/shared/assets/twelve-animals/3.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/3.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/3.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/4.png b/miniprogram/mathPages/shared/assets/twelve-animals/4.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/4.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/4.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/5.png b/miniprogram/mathPages/shared/assets/twelve-animals/5.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/5.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/5.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/6.png b/miniprogram/mathPages/shared/assets/twelve-animals/6.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/6.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/6.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/7.png b/miniprogram/mathPages/shared/assets/twelve-animals/7.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/7.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/7.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/8.png b/miniprogram/mathPages/shared/assets/twelve-animals/8.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/8.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/8.png
diff --git a/miniprogram/mathPages/assets/twelve-animals/9.png b/miniprogram/mathPages/shared/assets/twelve-animals/9.png
similarity index 100%
rename from miniprogram/mathPages/assets/twelve-animals/9.png
rename to miniprogram/mathPages/shared/assets/twelve-animals/9.png
diff --git a/miniprogram/mathPages/shared/service/additionContentDraw.ts b/miniprogram/mathPages/shared/service/additionContentDraw.ts
deleted file mode 100644
index 1a298d7..0000000
--- a/miniprogram/mathPages/shared/service/additionContentDraw.ts
+++ /dev/null
@@ -1,576 +0,0 @@
-import { getImage } from '../../../utils/index';
-
-interface DrawAdditionContentParams {
- canvas: WechatMiniprogram.Canvas;
- ctx: RenderingContext;
- problems: Array<{
- type: 'addition' | 'subtraction';
- left: number;
- right: number;
- result: number;
- }>;
- canvasWidth: number;
- startY: number;
- imageType?: string; // 'twelve-animals' 或 'fruits'
-}
-
-/**
- * 绘制圆角矩形(虚线或实线边框)
- */
-function drawRoundedRect(
- ctx: RenderingContext,
- x: number,
- y: number,
- width: number,
- height: number,
- radius: number,
- isDashed: boolean = true,
-) {
- ctx.beginPath();
- ctx.moveTo(x + radius, y);
- ctx.lineTo(x + width - radius, y);
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
- ctx.lineTo(x + width, y + height - radius);
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
- ctx.lineTo(x + radius, y + height);
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
- ctx.lineTo(x, y + radius);
- ctx.quadraticCurveTo(x, y, x + radius, y);
- ctx.closePath();
-
- if (isDashed) {
- ctx.setLineDash([2, 2]); // 虚线
- } else {
- ctx.setLineDash([]); // 实线
- }
- ctx.stroke();
- ctx.setLineDash([]); // 重置为实线
-}
-
-/**
- * 绘制实线圆角方框(用于填写答案)
- */
-function drawAnswerBox(
- ctx: RenderingContext,
- x: number,
- y: number,
- width: number,
- height: number,
-) {
- const radius = 6; // 圆角大小
- ctx.strokeStyle = '#000';
- ctx.lineWidth = 1;
- ctx.setLineDash([]); // 实线
- ctx.beginPath();
- ctx.moveTo(x + radius, y);
- ctx.lineTo(x + width - radius, y);
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
- ctx.lineTo(x + width, y + height - radius);
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
- ctx.lineTo(x + radius, y + height);
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
- ctx.lineTo(x, y + radius);
- ctx.quadraticCurveTo(x, y, x + radius, y);
- ctx.closePath();
- ctx.stroke();
-}
-
-/**
- * 绘制右侧答案框
- */
-function drawAnswerBoxArea(
- ctx: RenderingContext,
- params: {
- rightBoxX: number;
- boxY: number;
- rightBoxWidth: number;
- borderRadius: number;
- boxHeight: number;
- padding: number;
- problemType: 'addition' | 'subtraction';
- },
-) {
- const {
- rightBoxX,
- boxY,
- rightBoxWidth,
- borderRadius,
- boxHeight,
- padding,
- problemType,
- } = params;
-
- const plusSignSize = 18; // 加号大小(缩小)
- const minusSignSize = 18; // 减号大小(缩小)
- const equalsSignSize = 18; // 等号大小(缩小)
- const spacing = 8; // 加号左右间距(方便修改和调试)
- const answerBoxWidth = 34; // 答案框宽度
- const answerBoxHeight = 34; // 答案框高度
-
- // 绘制右侧答案框边框
- ctx.strokeStyle = '#999';
- ctx.lineWidth = 1;
- drawRoundedRect(
- ctx,
- rightBoxX,
- boxY,
- rightBoxWidth,
- boxHeight,
- borderRadius,
- true,
- );
-
- const rightBoxCenterY = boxY + boxHeight / 2;
- const rightBoxAvailableWidth = rightBoxWidth - padding * 2;
- const answerStartY = rightBoxCenterY - answerBoxHeight / 2;
-
- // 根据问题类型确定运算符号大小
- const operatorSignSize =
- problemType === 'addition' ? plusSignSize : minusSignSize;
-
- // 计算总宽度并水平居中(加法和减法逻辑相同)
- const totalAnswerWidth =
- answerBoxWidth +
- spacing +
- operatorSignSize +
- spacing +
- answerBoxWidth +
- spacing +
- equalsSignSize +
- spacing +
- answerBoxWidth;
- const answerStartX =
- rightBoxX + padding + (rightBoxAvailableWidth - totalAnswerWidth) / 2;
-
- // 计算各个元素的位置
- const box1X = answerStartX;
- const operatorX = box1X + answerBoxWidth + spacing;
- const box2X = operatorX + plusSignSize + spacing;
- const equalsX = box2X + answerBoxWidth + spacing;
- const box3X = equalsX + equalsSignSize + spacing;
-
- // 绘制第一个方框(圆角)
- drawAnswerBox(ctx, box1X, answerStartY, answerBoxWidth, answerBoxHeight);
-
- // 绘制运算符号(加号或减号)
- ctx.strokeStyle = '#000';
- ctx.lineWidth = 2;
- ctx.beginPath();
- if (problemType === 'addition') {
- // 加号:横线和竖线
- ctx.moveTo(operatorX, rightBoxCenterY);
- ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY);
- ctx.moveTo(
- operatorX + operatorSignSize / 2,
- rightBoxCenterY - operatorSignSize / 2,
- );
- ctx.lineTo(
- operatorX + operatorSignSize / 2,
- rightBoxCenterY + operatorSignSize / 2,
- );
- } else {
- // 减号:只有横线
- ctx.moveTo(operatorX, rightBoxCenterY);
- ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY);
- }
- ctx.stroke();
-
- // 绘制第二个方框(圆角)
- drawAnswerBox(ctx, box2X, answerStartY, answerBoxWidth, answerBoxHeight);
-
- // 绘制等号(缩短)
- ctx.strokeStyle = '#000';
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(equalsX, rightBoxCenterY - 3);
- ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY - 3);
- ctx.moveTo(equalsX, rightBoxCenterY + 3);
- ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY + 3);
- ctx.stroke();
-
- // 绘制第三个方框(答案,圆角)
- drawAnswerBox(ctx, box3X, answerStartY, answerBoxWidth, answerBoxHeight);
-}
-
-// 根据图片类型设置图片大小
-function getImageSize(imageType: 'twelve-animals' | 'fruits'): number {
- return imageType === 'twelve-animals' ? 40 : 36;
-}
-
-/**
- * 绘制加减法计算内容区域
- * 左侧框:展示图片(加法:加号左右展示图片;减法:总数图片,后几个加划线)
- * 右侧框:填写运算方式(实线方框 + 实线方框 = 实线方框 或 实线方框 - 实线方框 = 实线方框)
- */
-export async function drawAdditionContent({
- canvas,
- ctx,
- problems,
- canvasWidth,
- startY,
- imageType: _imageType = '', // 不再使用,每个运算会随机选择图片类型
-}: DrawAdditionContentParams): Promise {
- // 根据图片类型确定图片目录和最大索引
- const imageConfig = {
- 'twelve-animals': {
- folder: 'twelve-animals',
- maxIndex: 12,
- },
- fruits: {
- folder: 'fruits',
- maxIndex: 22,
- },
- };
-
- const leftMargin = 24;
- const rightMargin = 24;
- const itemSpacing = 140;
- // 调整左右框宽度比例:6:4 (约420:280)
- const leftBoxWidth = 334; // 左侧图片框固定宽度
- const rightBoxWidth = 196; // 右侧答案框固定宽度
- const boxHeight = 110;
- const borderRadius = 12;
-
- const imageSpacing = 0; // 图片之间的间距(水平方向)
- const rowSpacing = 4; // 第一行和第二行之间的间距(垂直方向)
- const padding = 15; // 框内边距
- const startYPos = startY + 20;
- const plusSignSize = 20; // 加号大小(缩小)
- const plusSignSpacing = 15; // 加号左右间距(方便修改和调试)
-
- // 计算左侧和右侧的起始X位置
- const leftBoxX = leftMargin;
- const rightBoxX = canvasWidth - rightMargin - rightBoxWidth;
-
- // 为每个题目随机选择图片类型和图片索引
- const problemImageConfigs: Array<{
- type: 'twelve-animals' | 'fruits';
- imageIndex: number;
- }> = [];
-
- for (let i = 0; i < problems.length; i++) {
- // 随机选择图片类型
- const imageTypes: ('twelve-animals' | 'fruits')[] = [
- 'twelve-animals',
- 'fruits',
- ];
- const randomType =
- imageTypes[Math.floor(Math.random() * imageTypes.length)];
- const config = imageConfig[randomType];
-
- // 为当前题目生成唯一的图片索引
- const availableImageIndices = Array.from(
- { length: config.maxIndex },
- (_, i) => i + 1,
- );
- const randomIndex = Math.floor(
- Math.random() * availableImageIndices.length,
- );
- const imageIndex = availableImageIndices[randomIndex];
-
- problemImageConfigs.push({
- type: randomType,
- imageIndex,
- });
- }
-
- // 绘制每一行的内容
- for (let i = 0; i < problems.length; i++) {
- const problem = problems[i];
- const boxY = startYPos + i * itemSpacing;
- const imageConfigItem = problemImageConfigs[i];
- const config = imageConfig[imageConfigItem.type];
-
- // 根据图片类型获取图片大小
- const imageSize = getImageSize(imageConfigItem.type);
-
- // 加载图片
- let image: any = null;
- try {
- const imagePath = `/mathPages/assets/${config.folder}/${imageConfigItem.imageIndex}.png`;
- image = await getImage(canvas, imagePath);
- } catch (error) {
- console.error('图片加载失败:', error);
- }
-
- // ========== 绘制左侧图片框 ==========
- ctx.strokeStyle = '#999';
- ctx.lineWidth = 1;
- drawRoundedRect(
- ctx,
- leftBoxX,
- boxY,
- leftBoxWidth,
- boxHeight,
- borderRadius,
- true,
- );
-
- const leftBoxCenterY = boxY + boxHeight / 2;
- const leftBoxContentX = leftBoxX + padding;
-
- if (problem.type === 'addition') {
- // 加法:左侧图片 + 加号 + 右侧图片
- const leftImageCount = problem.left;
- const rightImageCount = problem.right;
-
- /**
- * 根据数量计算每行最多展示的图片数
- */
- const getMaxImagesPerRow = (count: number): number => {
- if (count <= 6) return 3;
- if (count <= 8) return 4;
- return 5; // 8-10
- };
-
- const leftMaxPerRow = getMaxImagesPerRow(leftImageCount);
- const rightMaxPerRow = getMaxImagesPerRow(rightImageCount);
- const leftRows = Math.min(
- 2,
- Math.ceil(leftImageCount / leftMaxPerRow),
- );
- const rightRows = Math.min(
- 2,
- Math.ceil(rightImageCount / rightMaxPerRow),
- );
-
- // 计算左侧图片区域的宽度和高度
- // 需要根据实际行数计算每行的最大宽度
- const leftFirstRowCount = Math.min(leftImageCount, leftMaxPerRow);
- const leftSecondRowCount = Math.max(
- 0,
- Math.min(leftMaxPerRow, leftImageCount - leftMaxPerRow),
- );
- const leftImagesWidth =
- Math.max(
- leftFirstRowCount * imageSize +
- (leftFirstRowCount - 1) * imageSpacing,
- leftSecondRowCount * imageSize +
- (leftSecondRowCount - 1) * imageSpacing,
- ) || imageSize; // 至少为 imageSize
-
- // 计算右侧图片区域的宽度和高度
- const rightFirstRowCount = Math.min(
- rightImageCount,
- rightMaxPerRow,
- );
- const rightSecondRowCount = Math.max(
- 0,
- Math.min(rightMaxPerRow, rightImageCount - rightMaxPerRow),
- );
- const rightImagesWidth =
- Math.max(
- rightFirstRowCount * imageSize +
- (rightFirstRowCount - 1) * imageSpacing,
- rightSecondRowCount * imageSize +
- (rightSecondRowCount - 1) * imageSpacing,
- ) || imageSize; // 至少为 imageSize
-
- // 获取图片实际高度(用于垂直居中计算)
- let imageActualHeight = imageSize;
- if (image) {
- // @ts-ignore
- imageActualHeight = (image.height / image.width) * imageSize;
- }
-
- // 计算左侧图片区域的实际高度(考虑图片的实际高度和行间距)
- const leftImagesHeight =
- leftRows * imageActualHeight + (leftRows - 1) * rowSpacing;
- // 计算右侧图片区域的实际高度
- const rightImagesHeight =
- rightRows * imageActualHeight + (rightRows - 1) * rowSpacing;
-
- // 计算加号位置(居中,包含左右间距)
- const totalContentWidth =
- leftImagesWidth +
- plusSignSpacing +
- plusSignSize +
- plusSignSpacing +
- rightImagesWidth;
- const availableWidth = leftBoxWidth - padding * 2;
- const contentStartX =
- leftBoxContentX + (availableWidth - totalContentWidth) / 2;
-
- const leftImagesStartX = contentStartX;
- // 垂直居中:使用图片实际高度计算
- const leftImagesStartY = leftBoxCenterY - leftImagesHeight / 2;
-
- // 绘制左侧图片(最多两行)
- for (let j = 0; j < Math.min(leftImageCount, 10); j++) {
- const row = Math.floor(j / leftMaxPerRow);
- const col = j % leftMaxPerRow;
-
- // 如果超过两行,停止绘制
- if (row >= 2) break;
-
- const imagesInThisRow = Math.min(
- leftMaxPerRow,
- leftImageCount - row * leftMaxPerRow,
- );
- const rowWidth =
- imagesInThisRow * imageSize +
- (imagesInThisRow - 1) * imageSpacing;
- const rowStartX =
- leftImagesStartX + (leftImagesWidth - rowWidth) / 2;
-
- const imageX = rowStartX + col * (imageSize + imageSpacing);
- // 使用图片实际高度和行间距计算垂直位置
- const imageY =
- leftImagesStartY + row * (imageActualHeight + rowSpacing);
-
- if (image) {
- // @ts-ignore
- const scaledHeight =
- (image.height / image.width) * imageSize;
- ctx.drawImage(
- image,
- imageX,
- imageY,
- imageSize,
- scaledHeight,
- );
- }
- }
-
- // 绘制加号(左右有间距)
- const plusX =
- leftImagesStartX +
- leftImagesWidth +
- plusSignSpacing +
- plusSignSize / 2;
- const plusY = leftBoxCenterY;
- ctx.strokeStyle = '#000';
- ctx.lineWidth = 2;
- ctx.beginPath();
- // 横线
- ctx.moveTo(plusX - plusSignSize / 2, plusY);
- ctx.lineTo(plusX + plusSignSize / 2, plusY);
- // 竖线
- ctx.moveTo(plusX, plusY - plusSignSize / 2);
- ctx.lineTo(plusX, plusY + plusSignSize / 2);
- ctx.stroke();
-
- const rightImagesStartX =
- plusX + plusSignSize / 2 + plusSignSpacing;
- // 垂直居中:使用图片实际高度计算
- const rightImagesStartY = leftBoxCenterY - rightImagesHeight / 2;
-
- // 绘制右侧图片(最多两行)
- for (let j = 0; j < Math.min(rightImageCount, 10); j++) {
- const row = Math.floor(j / rightMaxPerRow);
- const col = j % rightMaxPerRow;
-
- // 如果超过两行,停止绘制
- if (row >= 2) break;
-
- const imagesInThisRow = Math.min(
- rightMaxPerRow,
- rightImageCount - row * rightMaxPerRow,
- );
- const rowWidth =
- imagesInThisRow * imageSize +
- (imagesInThisRow - 1) * imageSpacing;
- const rowStartX =
- rightImagesStartX + (rightImagesWidth - rowWidth) / 2;
-
- const imageX = rowStartX + col * (imageSize + imageSpacing);
- // 使用图片实际高度和行间距计算垂直位置
- const imageY =
- rightImagesStartY + row * (imageActualHeight + rowSpacing);
-
- if (image) {
- // @ts-ignore
- const scaledHeight =
- (image.height / image.width) * imageSize;
- ctx.drawImage(
- image,
- imageX,
- imageY,
- imageSize,
- scaledHeight,
- );
- }
- }
- } else {
- // 减法:展示总数量的图片,后几个加划线
- const totalCount = problem.left; // 总数
- const subtractCount = problem.right; // 要减去的数量
-
- // 获取图片实际高度(用于垂直居中计算)
- let imageActualHeight = imageSize;
- if (image) {
- // @ts-ignore
- imageActualHeight = (image.height / image.width) * imageSize;
- }
-
- // 计算图片布局
- const imagesPerRow = 5;
- const totalRows = Math.ceil(totalCount / imagesPerRow);
- // 使用图片实际高度和行间距计算总高度
- const totalHeight =
- totalRows * imageActualHeight + (totalRows - 1) * rowSpacing;
- const imagesStartY = leftBoxCenterY - totalHeight / 2;
-
- // 绘制所有图片
- for (let j = 0; j < totalCount; j++) {
- const row = Math.floor(j / imagesPerRow);
- const col = j % imagesPerRow;
- const imagesInThisRow = Math.min(
- imagesPerRow,
- totalCount - row * imagesPerRow,
- );
- const rowWidth =
- imagesInThisRow * imageSize +
- (imagesInThisRow - 1) * imageSpacing;
- const rowStartX =
- leftBoxContentX +
- (leftBoxWidth - padding * 2 - rowWidth) / 2;
-
- const imageX = rowStartX + col * (imageSize + imageSpacing);
- // 使用图片实际高度和行间距计算垂直位置
- const imageY =
- imagesStartY + row * (imageActualHeight + rowSpacing);
-
- if (image) {
- // @ts-ignore
- const scaledHeight =
- (image.height / image.width) * imageSize;
- ctx.drawImage(
- image,
- imageX,
- imageY,
- imageSize,
- scaledHeight,
- );
-
- // 如果是后几个图片,绘制虚线删除线(表示减去)
- if (j >= totalCount - subtractCount) {
- ctx.strokeStyle = '#000';
- ctx.lineWidth = 2;
- ctx.setLineDash([4, 3]); // 设置虚线样式
- ctx.beginPath();
- // 绘制第一条虚线对角线(从左上到右下)
- ctx.moveTo(imageX, imageY);
- ctx.lineTo(imageX + imageSize, imageY + scaledHeight);
- // // 绘制第二条虚线对角线(从右上到左下)
- // ctx.moveTo(imageX + imageSize, imageY);
- // ctx.lineTo(imageX, imageY + scaledHeight);
- ctx.stroke();
- ctx.setLineDash([]); // 重置为实线,防止影响后续绘制
- }
- }
- }
- }
-
- // ========== 绘制右侧答案框 ==========
- drawAnswerBoxArea(ctx, {
- rightBoxX,
- boxY,
- rightBoxWidth,
- borderRadius,
- boxHeight,
- padding,
- problemType: problem.type,
- });
- }
-}
diff --git a/miniprogram/mathPages/shared/service/additionDraw.ts b/miniprogram/mathPages/shared/service/additionDraw.ts
index 41feefd..cdfbc1d 100644
--- a/miniprogram/mathPages/shared/service/additionDraw.ts
+++ b/miniprogram/mathPages/shared/service/additionDraw.ts
@@ -1,19 +1,27 @@
+/**
+ * 加减法计算绘制服务
+ */
+
import { BaseDrawService } from '../../../service/baseDraw';
-import { drawAdditionContent } from './additionContentDraw';
+import { getImage } from '../../../utils/index';
+
+/**
+ * 加减法计算数据
+ */
+export interface AdditionData {
+ problems: Array<{
+ type: 'addition' | 'subtraction';
+ left: number;
+ right: number;
+ result: number;
+ }>;
+}
/**
* 加减法计算绘制服务
- * 组合使用基础绘制服务和内容区域绘制服务
*/
class AdditionDraw extends BaseDrawService {
- calculationData: {
- problems: Array<{
- type: 'addition' | 'subtraction';
- left: number;
- right: number;
- result: number;
- }>;
- } | null;
+ calculationData: AdditionData | null = null;
constructor(
canvas: Canvas,
@@ -24,15 +32,11 @@ class AdditionDraw extends BaseDrawService {
this.calculationData = null;
}
+ /**
+ * 绘制加减法计算内容
+ */
async draw(
- calculationData: {
- problems: Array<{
- type: 'addition' | 'subtraction';
- left: number;
- right: number;
- result: number;
- }>;
- },
+ calculationData: AdditionData,
_calculationType: string = 'addition-5', // 保留参数以保持接口兼容性
_imageType?: string, // 可选,不再使用,每个运算会随机选择图片类型
) {
@@ -40,21 +44,10 @@ class AdditionDraw extends BaseDrawService {
return;
}
- this.setPrintConfig();
this.calculationData = calculationData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域(加减法计算)
- this.drawDivider();
- await drawAdditionContent({
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
+ await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
problems: this.calculationData.problems,
@@ -62,6 +55,613 @@ class AdditionDraw extends BaseDrawService {
startY: this.currentY,
});
}
+
+ /**
+ * 绘制内容区域
+ */
+ private async drawContent(params: {
+ canvas: WechatMiniprogram.Canvas;
+ ctx: RenderingContext;
+ problems: Array<{
+ type: 'addition' | 'subtraction';
+ left: number;
+ right: number;
+ result: number;
+ }>;
+ canvasWidth: number;
+ startY: number;
+ }): Promise {
+ const { canvas, ctx, problems, canvasWidth, startY } = params;
+
+ // 根据图片类型确定图片目录和最大索引
+ const imageConfig = {
+ 'twelve-animals': {
+ folder: 'twelve-animals',
+ maxIndex: 12,
+ },
+ fruits: {
+ folder: 'fruits',
+ maxIndex: 22,
+ },
+ };
+
+ const leftMargin = 24;
+ const rightMargin = 24;
+ const itemSpacing = 140;
+ // 调整左右框宽度比例:6:4 (约420:280)
+ const leftBoxWidth = 334; // 左侧图片框固定宽度
+ const rightBoxWidth = 196; // 右侧答案框固定宽度
+ const boxHeight = 110;
+ const borderRadius = 12;
+
+ const imageSpacing = 0; // 图片之间的间距(水平方向)
+ const rowSpacing = 4; // 第一行和第二行之间的间距(垂直方向)
+ const padding = 15; // 框内边距
+ const startYPos = startY + 20;
+ const plusSignSize = 20; // 加号大小(缩小)
+ const plusSignSpacing = 15; // 加号左右间距(方便修改和调试)
+
+ // 计算左侧和右侧的起始X位置
+ const leftBoxX = leftMargin;
+ const rightBoxX = canvasWidth - rightMargin - rightBoxWidth;
+
+ // 为每个题目随机选择图片类型和图片索引
+ const problemImageConfigs: Array<{
+ type: 'twelve-animals' | 'fruits';
+ imageIndex: number;
+ }> = [];
+
+ for (let i = 0; i < problems.length; i++) {
+ // 随机选择图片类型
+ const imageTypes: ('twelve-animals' | 'fruits')[] = [
+ 'twelve-animals',
+ 'fruits',
+ ];
+ const randomType =
+ imageTypes[Math.floor(Math.random() * imageTypes.length)];
+ const config = imageConfig[randomType];
+
+ // 为当前题目生成唯一的图片索引
+ const availableImageIndices = Array.from(
+ { length: config.maxIndex },
+ (_, i) => i + 1,
+ );
+ const randomIndex = Math.floor(
+ Math.random() * availableImageIndices.length,
+ );
+ const imageIndex = availableImageIndices[randomIndex];
+
+ problemImageConfigs.push({
+ type: randomType,
+ imageIndex,
+ });
+ }
+
+ // 绘制每一行的内容
+ for (let i = 0; i < problems.length; i++) {
+ const problem = problems[i];
+ const boxY = startYPos + i * itemSpacing;
+ const imageConfigItem = problemImageConfigs[i];
+ const config = imageConfig[imageConfigItem.type];
+
+ // 根据图片类型获取图片大小
+ const imageSize = this.getImageSize(imageConfigItem.type);
+
+ // 加载图片
+ let image: any = null;
+ try {
+ const imagePath = `/mathPages/shared/assets/${config.folder}/${imageConfigItem.imageIndex}.png`;
+ image = await getImage(canvas, imagePath);
+ } catch (error) {
+ console.error('图片加载失败:', error);
+ }
+
+ // ========== 绘制左侧图片框 ==========
+ ctx.strokeStyle = '#999';
+ ctx.lineWidth = 1;
+ this.drawRoundedRectInternal(
+ ctx,
+ leftBoxX,
+ boxY,
+ leftBoxWidth,
+ boxHeight,
+ borderRadius,
+ true,
+ );
+
+ const leftBoxCenterY = boxY + boxHeight / 2;
+ const leftBoxContentX = leftBoxX + padding;
+
+ if (problem.type === 'addition') {
+ // 加法:左侧图片 + 加号 + 右侧图片
+ const leftImageCount = problem.left;
+ const rightImageCount = problem.right;
+
+ /**
+ * 根据数量计算每行最多展示的图片数
+ */
+ const getMaxImagesPerRow = (count: number): number => {
+ if (count <= 6) return 3;
+ if (count <= 8) return 4;
+ return 5; // 8-10
+ };
+
+ const leftMaxPerRow = getMaxImagesPerRow(leftImageCount);
+ const rightMaxPerRow = getMaxImagesPerRow(rightImageCount);
+ const leftRows = Math.min(
+ 2,
+ Math.ceil(leftImageCount / leftMaxPerRow),
+ );
+ const rightRows = Math.min(
+ 2,
+ Math.ceil(rightImageCount / rightMaxPerRow),
+ );
+
+ // 计算左侧图片区域的宽度和高度
+ // 需要根据实际行数计算每行的最大宽度
+ const leftFirstRowCount = Math.min(
+ leftImageCount,
+ leftMaxPerRow,
+ );
+ const leftSecondRowCount = Math.max(
+ 0,
+ Math.min(leftMaxPerRow, leftImageCount - leftMaxPerRow),
+ );
+ const leftImagesWidth =
+ Math.max(
+ leftFirstRowCount * imageSize +
+ (leftFirstRowCount - 1) * imageSpacing,
+ leftSecondRowCount * imageSize +
+ (leftSecondRowCount - 1) * imageSpacing,
+ ) || imageSize; // 至少为 imageSize
+
+ // 计算右侧图片区域的宽度和高度
+ const rightFirstRowCount = Math.min(
+ rightImageCount,
+ rightMaxPerRow,
+ );
+ const rightSecondRowCount = Math.max(
+ 0,
+ Math.min(rightMaxPerRow, rightImageCount - rightMaxPerRow),
+ );
+ const rightImagesWidth =
+ Math.max(
+ rightFirstRowCount * imageSize +
+ (rightFirstRowCount - 1) * imageSpacing,
+ rightSecondRowCount * imageSize +
+ (rightSecondRowCount - 1) * imageSpacing,
+ ) || imageSize; // 至少为 imageSize
+
+ // 获取图片实际高度(用于垂直居中计算)
+ let imageActualHeight = imageSize;
+ if (image) {
+ // @ts-ignore
+ imageActualHeight =
+ (image.height / image.width) * imageSize;
+ }
+
+ // 计算左侧图片区域的实际高度(考虑图片的实际高度和行间距)
+ const leftImagesHeight =
+ leftRows * imageActualHeight + (leftRows - 1) * rowSpacing;
+ // 计算右侧图片区域的实际高度
+ const rightImagesHeight =
+ rightRows * imageActualHeight +
+ (rightRows - 1) * rowSpacing;
+
+ // 计算加号位置(居中,包含左右间距)
+ const totalContentWidth =
+ leftImagesWidth +
+ plusSignSpacing +
+ plusSignSize +
+ plusSignSpacing +
+ rightImagesWidth;
+ const availableWidth = leftBoxWidth - padding * 2;
+ const contentStartX =
+ leftBoxContentX + (availableWidth - totalContentWidth) / 2;
+
+ const leftImagesStartX = contentStartX;
+ // 垂直居中:使用图片实际高度计算
+ const leftImagesStartY = leftBoxCenterY - leftImagesHeight / 2;
+
+ // 绘制左侧图片(最多两行)
+ for (let j = 0; j < Math.min(leftImageCount, 10); j++) {
+ const row = Math.floor(j / leftMaxPerRow);
+ const col = j % leftMaxPerRow;
+
+ // 如果超过两行,停止绘制
+ if (row >= 2) break;
+
+ const imagesInThisRow = Math.min(
+ leftMaxPerRow,
+ leftImageCount - row * leftMaxPerRow,
+ );
+ const rowWidth =
+ imagesInThisRow * imageSize +
+ (imagesInThisRow - 1) * imageSpacing;
+ const rowStartX =
+ leftImagesStartX + (leftImagesWidth - rowWidth) / 2;
+
+ const imageX = rowStartX + col * (imageSize + imageSpacing);
+ // 使用图片实际高度和行间距计算垂直位置
+ const imageY =
+ leftImagesStartY +
+ row * (imageActualHeight + rowSpacing);
+
+ if (image) {
+ // @ts-ignore
+ const scaledHeight =
+ (image.height / image.width) * imageSize;
+ ctx.drawImage(
+ image,
+ imageX,
+ imageY,
+ imageSize,
+ scaledHeight,
+ );
+ }
+ }
+
+ // 绘制加号(左右有间距)
+ const plusX =
+ leftImagesStartX +
+ leftImagesWidth +
+ plusSignSpacing +
+ plusSignSize / 2;
+ const plusY = leftBoxCenterY;
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ // 横线
+ ctx.moveTo(plusX - plusSignSize / 2, plusY);
+ ctx.lineTo(plusX + plusSignSize / 2, plusY);
+ // 竖线
+ ctx.moveTo(plusX, plusY - plusSignSize / 2);
+ ctx.lineTo(plusX, plusY + plusSignSize / 2);
+ ctx.stroke();
+
+ const rightImagesStartX =
+ plusX + plusSignSize / 2 + plusSignSpacing;
+ // 垂直居中:使用图片实际高度计算
+ const rightImagesStartY =
+ leftBoxCenterY - rightImagesHeight / 2;
+
+ // 绘制右侧图片(最多两行)
+ for (let j = 0; j < Math.min(rightImageCount, 10); j++) {
+ const row = Math.floor(j / rightMaxPerRow);
+ const col = j % rightMaxPerRow;
+
+ // 如果超过两行,停止绘制
+ if (row >= 2) break;
+
+ const imagesInThisRow = Math.min(
+ rightMaxPerRow,
+ rightImageCount - row * rightMaxPerRow,
+ );
+ const rowWidth =
+ imagesInThisRow * imageSize +
+ (imagesInThisRow - 1) * imageSpacing;
+ const rowStartX =
+ rightImagesStartX + (rightImagesWidth - rowWidth) / 2;
+
+ const imageX = rowStartX + col * (imageSize + imageSpacing);
+ // 使用图片实际高度和行间距计算垂直位置
+ const imageY =
+ rightImagesStartY +
+ row * (imageActualHeight + rowSpacing);
+
+ if (image) {
+ // @ts-ignore
+ const scaledHeight =
+ (image.height / image.width) * imageSize;
+ ctx.drawImage(
+ image,
+ imageX,
+ imageY,
+ imageSize,
+ scaledHeight,
+ );
+ }
+ }
+ } else {
+ // 减法:展示总数量的图片,后几个加划线
+ const totalCount = problem.left; // 总数
+ const subtractCount = problem.right; // 要减去的数量
+
+ // 获取图片实际高度(用于垂直居中计算)
+ let imageActualHeight = imageSize;
+ if (image) {
+ // @ts-ignore
+ imageActualHeight =
+ (image.height / image.width) * imageSize;
+ }
+
+ // 计算图片布局
+ const imagesPerRow = 5;
+ const totalRows = Math.ceil(totalCount / imagesPerRow);
+ // 使用图片实际高度和行间距计算总高度
+ const totalHeight =
+ totalRows * imageActualHeight +
+ (totalRows - 1) * rowSpacing;
+ const imagesStartY = leftBoxCenterY - totalHeight / 2;
+
+ // 绘制所有图片
+ for (let j = 0; j < totalCount; j++) {
+ const row = Math.floor(j / imagesPerRow);
+ const col = j % imagesPerRow;
+ const imagesInThisRow = Math.min(
+ imagesPerRow,
+ totalCount - row * imagesPerRow,
+ );
+ const rowWidth =
+ imagesInThisRow * imageSize +
+ (imagesInThisRow - 1) * imageSpacing;
+ const rowStartX =
+ leftBoxContentX +
+ (leftBoxWidth - padding * 2 - rowWidth) / 2;
+
+ const imageX = rowStartX + col * (imageSize + imageSpacing);
+ // 使用图片实际高度和行间距计算垂直位置
+ const imageY =
+ imagesStartY + row * (imageActualHeight + rowSpacing);
+
+ if (image) {
+ // @ts-ignore
+ const scaledHeight =
+ (image.height / image.width) * imageSize;
+ ctx.drawImage(
+ image,
+ imageX,
+ imageY,
+ imageSize,
+ scaledHeight,
+ );
+
+ // 如果是后几个图片,绘制虚线删除线(表示减去)
+ if (j >= totalCount - subtractCount) {
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 2;
+ ctx.setLineDash([4, 3]); // 设置虚线样式
+ ctx.beginPath();
+ // 绘制第一条虚线对角线(从左上到右下)
+ ctx.moveTo(imageX, imageY);
+ ctx.lineTo(
+ imageX + imageSize,
+ imageY + scaledHeight,
+ );
+ ctx.stroke();
+ ctx.setLineDash([]); // 重置为实线,防止影响后续绘制
+ }
+ }
+ }
+ }
+
+ // ========== 绘制右侧答案框 ==========
+ this.drawAnswerBoxArea(ctx, {
+ rightBoxX,
+ boxY,
+ rightBoxWidth,
+ borderRadius,
+ boxHeight,
+ padding,
+ problemType: problem.type,
+ });
+ }
+ }
+
+ /**
+ * 绘制圆角矩形(虚线或实线边框)
+ */
+ private drawRoundedRectInternal(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ radius: number,
+ isDashed: boolean = true,
+ ) {
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.quadraticCurveTo(
+ x + width,
+ y + height,
+ x + width - radius,
+ y + height,
+ );
+ ctx.lineTo(x + radius, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+
+ if (isDashed) {
+ ctx.setLineDash([2, 2]); // 虚线
+ } else {
+ ctx.setLineDash([]); // 实线
+ }
+ ctx.stroke();
+ ctx.setLineDash([]); // 重置为实线
+ }
+
+ /**
+ * 绘制实线圆角方框(用于填写答案)
+ */
+ private drawAnswerBox(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ ) {
+ const radius = 6; // 圆角大小
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 1;
+ ctx.setLineDash([]); // 实线
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.quadraticCurveTo(
+ x + width,
+ y + height,
+ x + width - radius,
+ y + height,
+ );
+ ctx.lineTo(x + radius, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+ ctx.stroke();
+ }
+
+ /**
+ * 绘制右侧答案框
+ */
+ private drawAnswerBoxArea(
+ ctx: RenderingContext,
+ params: {
+ rightBoxX: number;
+ boxY: number;
+ rightBoxWidth: number;
+ borderRadius: number;
+ boxHeight: number;
+ padding: number;
+ problemType: 'addition' | 'subtraction';
+ },
+ ) {
+ const {
+ rightBoxX,
+ boxY,
+ rightBoxWidth,
+ borderRadius,
+ boxHeight,
+ padding,
+ problemType,
+ } = params;
+
+ const plusSignSize = 18; // 加号大小(缩小)
+ const minusSignSize = 18; // 减号大小(缩小)
+ const equalsSignSize = 18; // 等号大小(缩小)
+ const spacing = 8; // 加号左右间距(方便修改和调试)
+ const answerBoxWidth = 34; // 答案框宽度
+ const answerBoxHeight = 34; // 答案框高度
+
+ // 绘制右侧答案框边框
+ ctx.strokeStyle = '#999';
+ ctx.lineWidth = 1;
+ this.drawRoundedRectInternal(
+ ctx,
+ rightBoxX,
+ boxY,
+ rightBoxWidth,
+ boxHeight,
+ borderRadius,
+ true,
+ );
+
+ const rightBoxCenterY = boxY + boxHeight / 2;
+ const rightBoxAvailableWidth = rightBoxWidth - padding * 2;
+ const answerStartY = rightBoxCenterY - answerBoxHeight / 2;
+
+ // 根据问题类型确定运算符号大小
+ const operatorSignSize =
+ problemType === 'addition' ? plusSignSize : minusSignSize;
+
+ // 计算总宽度并水平居中(加法和减法逻辑相同)
+ const totalAnswerWidth =
+ answerBoxWidth +
+ spacing +
+ operatorSignSize +
+ spacing +
+ answerBoxWidth +
+ spacing +
+ equalsSignSize +
+ spacing +
+ answerBoxWidth;
+ const answerStartX =
+ rightBoxX +
+ padding +
+ (rightBoxAvailableWidth - totalAnswerWidth) / 2;
+
+ // 计算各个元素的位置
+ const box1X = answerStartX;
+ const operatorX = box1X + answerBoxWidth + spacing;
+ const box2X = operatorX + plusSignSize + spacing;
+ const equalsX = box2X + answerBoxWidth + spacing;
+ const box3X = equalsX + equalsSignSize + spacing;
+
+ // 绘制第一个方框(圆角)
+ this.drawAnswerBox(
+ ctx,
+ box1X,
+ answerStartY,
+ answerBoxWidth,
+ answerBoxHeight,
+ );
+
+ // 绘制运算符号(加号或减号)
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ if (problemType === 'addition') {
+ // 加号:横线和竖线
+ ctx.moveTo(operatorX, rightBoxCenterY);
+ ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY);
+ ctx.moveTo(
+ operatorX + operatorSignSize / 2,
+ rightBoxCenterY - operatorSignSize / 2,
+ );
+ ctx.lineTo(
+ operatorX + operatorSignSize / 2,
+ rightBoxCenterY + operatorSignSize / 2,
+ );
+ } else {
+ // 减号:只有横线
+ ctx.moveTo(operatorX, rightBoxCenterY);
+ ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY);
+ }
+ ctx.stroke();
+
+ // 绘制第二个方框(圆角)
+ this.drawAnswerBox(
+ ctx,
+ box2X,
+ answerStartY,
+ answerBoxWidth,
+ answerBoxHeight,
+ );
+
+ // 绘制等号(缩短)
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(equalsX, rightBoxCenterY - 3);
+ ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY - 3);
+ ctx.moveTo(equalsX, rightBoxCenterY + 3);
+ ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY + 3);
+ ctx.stroke();
+
+ // 绘制第三个方框(答案,圆角)
+ this.drawAnswerBox(
+ ctx,
+ box3X,
+ answerStartY,
+ answerBoxWidth,
+ answerBoxHeight,
+ );
+ }
+
+ /**
+ * 根据图片类型设置图片大小
+ */
+ private getImageSize(imageType: 'twelve-animals' | 'fruits'): number {
+ return imageType === 'twelve-animals' ? 40 : 36;
+ }
}
export default AdditionDraw;
diff --git a/miniprogram/mathPages/shared/service/compareContentDraw.ts b/miniprogram/mathPages/shared/service/compareContentDraw.ts
deleted file mode 100644
index 80883b4..0000000
--- a/miniprogram/mathPages/shared/service/compareContentDraw.ts
+++ /dev/null
@@ -1,344 +0,0 @@
-import { getImage } from '../../../utils/index';
-
-interface DrawCompareContentParams {
- canvas: WechatMiniprogram.Canvas;
- ctx: RenderingContext;
- compareData: {
- problems: Array<{
- leftCount: number;
- rightCount: number;
- leftImageIndex: number;
- rightImageIndex: number;
- imageType: 'fruits' | 'twelve-animals';
- }>;
- };
- canvasWidth: number;
- startY: number;
-}
-
-/**
- * 绘制圆角矩形(虚线边框)
- */
-function drawRoundedRect(
- ctx: RenderingContext,
- x: number,
- y: number,
- width: number,
- height: number,
- radius: number,
- isDashed: boolean = true,
-) {
- ctx.beginPath();
- ctx.moveTo(x + radius, y);
- ctx.lineTo(x + width - radius, y);
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
- ctx.lineTo(x + width, y + height - radius);
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
- ctx.lineTo(x + radius, y + height);
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
- ctx.lineTo(x, y + radius);
- ctx.quadraticCurveTo(x, y, x + radius, y);
- ctx.closePath();
-
- if (isDashed) {
- ctx.setLineDash([6 / 3, 6 / 3]); // 约2px的虚线
- } else {
- ctx.setLineDash([]);
- }
- ctx.stroke();
- ctx.setLineDash([]); // 重置为实线
-}
-
-/**
- * 绘制数一数比大小内容区域
- * 两列布局,每列左右两个虚线框,中间圆圈
- */
-export async function drawCompareContent({
- canvas,
- ctx,
- compareData,
- canvasWidth,
- startY,
-}: DrawCompareContentParams): Promise {
- const { problems } = compareData;
-
- // 布局参数
- const leftMargin = 30;
- const rightMargin = 30;
- const topMargin = 12;
- const rowSpacing = 116; // 行之间的间距
- const columnSpacing = 40; // 两列之间的间距(可调节)
-
- // 计算每列的宽度(减去边距和列间距)
- const availableWidth =
- canvasWidth - leftMargin - rightMargin - columnSpacing;
- const columnWidth = availableWidth / 2;
-
- // 框的尺寸
- const boxWidth = 100;
- const boxHeight = 100;
- const borderRadius = 12;
- const circleRadius = 18; // 圆圈半径
-
- // 框与圆圈之间的间距
- const boxToCircleSpacing = 12;
-
- // 计算左侧列和右侧列的起始X位置
- const leftColumnStartX = leftMargin;
- const rightColumnStartX = leftMargin + columnWidth + columnSpacing;
-
- // 计算每列内部的布局(左框 | 圆圈 | 右框,居中)
- const totalWidthPerColumn =
- boxWidth +
- boxToCircleSpacing +
- circleRadius * 2 +
- boxToCircleSpacing +
- boxWidth;
- const columnPadding = (columnWidth - totalWidthPerColumn) / 2;
-
- let currentY = startY + topMargin;
-
- // 计算分割线位置(两列中间)
- // const dividerX = leftMargin + columnWidth + columnSpacing / 2;
-
- // 固定绘制6行
- const totalRows = 6;
- for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
- const rowY = currentY + rowIndex * rowSpacing;
- // 绘制左侧列
- const leftProblemIndex = rowIndex * 2;
- if (leftProblemIndex < problems.length) {
- await drawCompareProblem(
- ctx,
- canvas,
- problems[leftProblemIndex],
- leftColumnStartX + columnPadding,
- rowY,
- boxWidth,
- boxHeight,
- borderRadius,
- circleRadius,
- boxToCircleSpacing,
- );
- }
-
- // 绘制右侧列
- const rightProblemIndex = rowIndex * 2 + 1;
- if (rightProblemIndex < problems.length) {
- await drawCompareProblem(
- ctx,
- canvas,
- problems[rightProblemIndex],
- rightColumnStartX + columnPadding,
- rowY,
- boxWidth,
- boxHeight,
- borderRadius,
- circleRadius,
- boxToCircleSpacing,
- );
- }
- }
-
- // 绘制中间分割线(贯穿所有行)
- // if (totalRows > 0) {
- // ctx.strokeStyle = '#999';
- // ctx.lineWidth = 1;
- // ctx.beginPath();
- // const dividerStartY = currentY;
- // const dividerEndY = currentY + (totalRows - 1) * rowSpacing + boxHeight;
- // ctx.moveTo(dividerX, dividerStartY);
- // ctx.lineTo(dividerX, dividerEndY);
- // ctx.stroke();
- // }
-}
-
-/**
- * 绘制一个比较问题(左框 | 圆圈 | 右框)
- */
-async function drawCompareProblem(
- ctx: RenderingContext,
- canvas: WechatMiniprogram.Canvas,
- problem: {
- leftCount: number;
- rightCount: number;
- leftImageIndex: number;
- rightImageIndex: number;
- imageType: 'fruits' | 'twelve-animals';
- },
- startX: number,
- startY: number,
- boxWidth: number,
- boxHeight: number,
- borderRadius: number,
- circleRadius: number,
- boxToCircleSpacing: number,
-) {
- const {
- leftCount,
- rightCount,
- leftImageIndex,
- rightImageIndex,
- imageType,
- } = problem;
-
- // 图片配置
- const imageConfig = {
- 'twelve-animals': {
- folder: 'twelve-animals',
- maxIndex: 12,
- },
- fruits: {
- folder: 'fruits',
- maxIndex: 22,
- },
- };
-
- const config = imageConfig[imageType] || imageConfig['twelve-animals'];
-
- // 计算左框位置
- const leftBoxX = startX;
- const leftBoxY = startY;
-
- // 计算圆圈位置
- const circleX = leftBoxX + boxWidth + boxToCircleSpacing + circleRadius;
- const circleY = startY + boxHeight / 2;
-
- // 计算右框位置
- const rightBoxX = circleX + circleRadius + boxToCircleSpacing;
- const rightBoxY = startY;
-
- // 绘制左框
- ctx.strokeStyle = '#999';
- ctx.lineWidth = 1;
- drawRoundedRect(
- ctx,
- leftBoxX,
- leftBoxY,
- boxWidth,
- boxHeight,
- borderRadius,
- true,
- );
-
- // 绘制左框内的图片
- await drawImagesInBox(
- ctx,
- canvas,
- leftCount,
- leftImageIndex,
- config.folder,
- leftBoxX,
- leftBoxY,
- boxWidth,
- boxHeight,
- );
-
- // 绘制实心圆圈
- ctx.fillStyle = '#fff';
- ctx.strokeStyle = '#333';
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.arc(circleX, circleY, circleRadius, 0, Math.PI * 2);
- ctx.fill(); // 先填充白色
- ctx.stroke(); // 再绘制边框
-
- // 绘制右框
- ctx.strokeStyle = '#999';
- ctx.lineWidth = 1;
- drawRoundedRect(
- ctx,
- rightBoxX,
- rightBoxY,
- boxWidth,
- boxHeight,
- borderRadius,
- true,
- );
-
- // 绘制右框内的图片
- await drawImagesInBox(
- ctx,
- canvas,
- rightCount,
- rightImageIndex,
- config.folder,
- rightBoxX,
- rightBoxY,
- boxWidth,
- boxHeight,
- );
-}
-
-/**
- * 在框内绘制多张图片
- */
-async function drawImagesInBox(
- ctx: RenderingContext,
- canvas: WechatMiniprogram.Canvas,
- count: number,
- imageIndex: number,
- folder: string,
- boxX: number,
- boxY: number,
- boxWidth: number,
- boxHeight: number,
-) {
- const padding = 8; // 框内边距
- const availableWidth = boxWidth - padding * 2;
- const availableHeight = boxHeight - padding * 2;
-
- // 根据数量确定每行的图片数和图片大小
- let imagesPerRow: number;
- let imageSize: number;
-
- if (count <= 4) {
- imagesPerRow = count <= 2 ? count : 2;
- imageSize =
- Math.min(availableWidth / imagesPerRow, availableHeight / 2) - 4;
- } else if (count <= 6) {
- imagesPerRow = 3;
- imageSize = Math.min(availableWidth / 3, availableHeight / 2) - 4;
- } else {
- imagesPerRow = 3;
- imageSize = Math.min(availableWidth / 3, availableHeight / 3) - 4;
- }
-
- const rows = Math.ceil(count / imagesPerRow);
- const imageSpacing =
- (availableWidth - imageSize * imagesPerRow) / (imagesPerRow + 1);
- const rowSpacing =
- rows > 1 ? (availableHeight - imageSize * rows) / (rows + 1) : 0;
-
- // 加载图片
- let boxImage: any = null;
- try {
- const imagePath = `/mathPages/assets/${folder}/${imageIndex}.png`;
- boxImage = await getImage(canvas, imagePath);
- } catch (error) {
- console.error(`加载${folder}/${imageIndex}图片失败:`, error);
- return;
- }
-
- // 绘制图片
- for (let i = 0; i < count; i++) {
- const row = Math.floor(i / imagesPerRow);
- const col = i % imagesPerRow;
-
- const imageX =
- boxX + padding + imageSpacing + col * (imageSize + imageSpacing);
- const imageY =
- boxY +
- padding +
- (rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) +
- row * (imageSize + (rows > 1 ? rowSpacing : 0));
-
- if (boxImage) {
- // 计算图片高度(等比例缩放)
- // @ts-ignore - 微信小程序图片对象有 width 和 height 属性
- const scaledHeight = (boxImage.height / boxImage.width) * imageSize;
-
- ctx.drawImage(boxImage, imageX, imageY, imageSize, scaledHeight);
- }
- }
-}
diff --git a/miniprogram/mathPages/shared/service/compareDraw.ts b/miniprogram/mathPages/shared/service/compareDraw.ts
index f1a1869..ab58ac5 100644
--- a/miniprogram/mathPages/shared/service/compareDraw.ts
+++ b/miniprogram/mathPages/shared/service/compareDraw.ts
@@ -1,20 +1,28 @@
+/**
+ * 数一数比大小绘制服务
+ */
+
import { BaseDrawService } from '../../../service/baseDraw';
-import { drawCompareContent } from './compareContentDraw';
+import { getImage } from '../../../utils/index';
+
+/**
+ * 数一数比大小数据
+ */
+export interface CompareData {
+ problems: Array<{
+ leftCount: number;
+ rightCount: number;
+ leftImageIndex: number;
+ rightImageIndex: number;
+ imageType: 'fruits' | 'twelve-animals';
+ }>;
+}
/**
* 数一数比大小绘制服务
- * 组合使用基础绘制服务和内容区域绘制服务
*/
class CompareDraw extends BaseDrawService {
- compareData: {
- problems: Array<{
- leftCount: number;
- rightCount: number;
- leftImageIndex: number;
- rightImageIndex: number;
- imageType: 'fruits' | 'twelve-animals';
- }>;
- } | null;
+ compareData: CompareData | null = null;
constructor(
canvas: Canvas,
@@ -25,34 +33,18 @@ class CompareDraw extends BaseDrawService {
this.compareData = null;
}
- async draw(compareData: {
- problems: Array<{
- leftCount: number;
- rightCount: number;
- leftImageIndex: number;
- rightImageIndex: number;
- imageType: 'fruits' | 'twelve-animals';
- }>;
- }) {
+ /**
+ * 绘制数一数比大小内容
+ */
+ async draw(compareData: CompareData) {
if (!compareData || !compareData.problems) {
return;
}
- this.setPrintConfig();
this.compareData = compareData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域(数一数比大小)
- this.drawDivider();
- await drawCompareContent({
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
+ await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
compareData: this.compareData,
@@ -60,6 +52,334 @@ class CompareDraw extends BaseDrawService {
startY: this.currentY,
});
}
+
+ /**
+ * 绘制内容区域
+ */
+ private async drawContent(params: {
+ canvas: WechatMiniprogram.Canvas;
+ ctx: RenderingContext;
+ compareData: CompareData;
+ canvasWidth: number;
+ startY: number;
+ }): Promise {
+ const { canvas, ctx, compareData, canvasWidth, startY } = params;
+ const { problems } = compareData;
+
+ // 布局参数
+ const leftMargin = 30;
+ const rightMargin = 30;
+ const topMargin = 12;
+ const rowSpacing = 116; // 行之间的间距
+ const columnSpacing = 40; // 两列之间的间距(可调节)
+
+ // 计算每列的宽度(减去边距和列间距)
+ const availableWidth =
+ canvasWidth - leftMargin - rightMargin - columnSpacing;
+ const columnWidth = availableWidth / 2;
+
+ // 框的尺寸
+ const boxWidth = 100;
+ const boxHeight = 100;
+ const borderRadius = 12;
+ const circleRadius = 18; // 圆圈半径
+
+ // 框与圆圈之间的间距
+ const boxToCircleSpacing = 12;
+
+ // 计算左侧列和右侧列的起始X位置
+ const leftColumnStartX = leftMargin;
+ const rightColumnStartX = leftMargin + columnWidth + columnSpacing;
+
+ // 计算每列内部的布局(左框 | 圆圈 | 右框,居中)
+ const totalWidthPerColumn =
+ boxWidth +
+ boxToCircleSpacing +
+ circleRadius * 2 +
+ boxToCircleSpacing +
+ boxWidth;
+ const columnPadding = (columnWidth - totalWidthPerColumn) / 2;
+
+ let currentY = startY + topMargin;
+
+ // 固定绘制6行
+ const totalRows = 6;
+ for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
+ const rowY = currentY + rowIndex * rowSpacing;
+ // 绘制左侧列
+ const leftProblemIndex = rowIndex * 2;
+ if (leftProblemIndex < problems.length) {
+ await this.drawCompareProblem(
+ ctx,
+ canvas,
+ problems[leftProblemIndex],
+ leftColumnStartX + columnPadding,
+ rowY,
+ boxWidth,
+ boxHeight,
+ borderRadius,
+ circleRadius,
+ boxToCircleSpacing,
+ );
+ }
+
+ // 绘制右侧列
+ const rightProblemIndex = rowIndex * 2 + 1;
+ if (rightProblemIndex < problems.length) {
+ await this.drawCompareProblem(
+ ctx,
+ canvas,
+ problems[rightProblemIndex],
+ rightColumnStartX + columnPadding,
+ rowY,
+ boxWidth,
+ boxHeight,
+ borderRadius,
+ circleRadius,
+ boxToCircleSpacing,
+ );
+ }
+ }
+ }
+
+ /**
+ * 绘制一个比较问题(左框 | 圆圈 | 右框)
+ */
+ private async drawCompareProblem(
+ ctx: RenderingContext,
+ canvas: WechatMiniprogram.Canvas,
+ problem: {
+ leftCount: number;
+ rightCount: number;
+ leftImageIndex: number;
+ rightImageIndex: number;
+ imageType: 'fruits' | 'twelve-animals';
+ },
+ startX: number,
+ startY: number,
+ boxWidth: number,
+ boxHeight: number,
+ borderRadius: number,
+ circleRadius: number,
+ boxToCircleSpacing: number,
+ ) {
+ const {
+ leftCount,
+ rightCount,
+ leftImageIndex,
+ rightImageIndex,
+ imageType,
+ } = problem;
+
+ // 图片配置
+ const imageConfig = {
+ 'twelve-animals': {
+ folder: 'twelve-animals',
+ maxIndex: 12,
+ },
+ fruits: {
+ folder: 'fruits',
+ maxIndex: 22,
+ },
+ };
+
+ const config = imageConfig[imageType] || imageConfig['twelve-animals'];
+
+ // 计算左框位置
+ const leftBoxX = startX;
+ const leftBoxY = startY;
+
+ // 计算圆圈位置
+ const circleX = leftBoxX + boxWidth + boxToCircleSpacing + circleRadius;
+ const circleY = startY + boxHeight / 2;
+
+ // 计算右框位置
+ const rightBoxX = circleX + circleRadius + boxToCircleSpacing;
+ const rightBoxY = startY;
+
+ // 绘制左框
+ ctx.strokeStyle = '#999';
+ ctx.lineWidth = 1;
+ this.drawRoundedRectInternal(
+ ctx,
+ leftBoxX,
+ leftBoxY,
+ boxWidth,
+ boxHeight,
+ borderRadius,
+ true,
+ );
+
+ // 绘制左框内的图片
+ await this.drawImagesInBox(
+ ctx,
+ canvas,
+ leftCount,
+ leftImageIndex,
+ config.folder,
+ leftBoxX,
+ leftBoxY,
+ boxWidth,
+ boxHeight,
+ );
+
+ // 绘制实心圆圈
+ ctx.fillStyle = '#fff';
+ ctx.strokeStyle = '#333';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.arc(circleX, circleY, circleRadius, 0, Math.PI * 2);
+ ctx.fill(); // 先填充白色
+ ctx.stroke(); // 再绘制边框
+
+ // 绘制右框
+ ctx.strokeStyle = '#999';
+ ctx.lineWidth = 1;
+ this.drawRoundedRectInternal(
+ ctx,
+ rightBoxX,
+ rightBoxY,
+ boxWidth,
+ boxHeight,
+ borderRadius,
+ true,
+ );
+
+ // 绘制右框内的图片
+ await this.drawImagesInBox(
+ ctx,
+ canvas,
+ rightCount,
+ rightImageIndex,
+ config.folder,
+ rightBoxX,
+ rightBoxY,
+ boxWidth,
+ boxHeight,
+ );
+ }
+
+ /**
+ * 在框内绘制多张图片
+ */
+ private async drawImagesInBox(
+ ctx: RenderingContext,
+ canvas: WechatMiniprogram.Canvas,
+ count: number,
+ imageIndex: number,
+ folder: string,
+ boxX: number,
+ boxY: number,
+ boxWidth: number,
+ boxHeight: number,
+ ) {
+ const padding = 8; // 框内边距
+ const availableWidth = boxWidth - padding * 2;
+ const availableHeight = boxHeight - padding * 2;
+
+ // 根据数量确定每行的图片数和图片大小
+ let imagesPerRow: number;
+ let imageSize: number;
+
+ if (count <= 4) {
+ imagesPerRow = count <= 2 ? count : 2;
+ imageSize =
+ Math.min(availableWidth / imagesPerRow, availableHeight / 2) -
+ 4;
+ } else if (count <= 6) {
+ imagesPerRow = 3;
+ imageSize = Math.min(availableWidth / 3, availableHeight / 2) - 4;
+ } else {
+ imagesPerRow = 3;
+ imageSize = Math.min(availableWidth / 3, availableHeight / 3) - 4;
+ }
+
+ const rows = Math.ceil(count / imagesPerRow);
+ const imageSpacing =
+ (availableWidth - imageSize * imagesPerRow) / (imagesPerRow + 1);
+ const rowSpacing =
+ rows > 1 ? (availableHeight - imageSize * rows) / (rows + 1) : 0;
+
+ // 加载图片
+ let boxImage: any = null;
+ try {
+ const imagePath = `/mathPages/shared/assets/${folder}/${imageIndex}.png`;
+ boxImage = await getImage(canvas, imagePath);
+ } catch (error) {
+ console.error(`加载${folder}/${imageIndex}图片失败:`, error);
+ return;
+ }
+
+ // 绘制图片
+ for (let i = 0; i < count; i++) {
+ const row = Math.floor(i / imagesPerRow);
+ const col = i % imagesPerRow;
+
+ const imageX =
+ boxX +
+ padding +
+ imageSpacing +
+ col * (imageSize + imageSpacing);
+ const imageY =
+ boxY +
+ padding +
+ (rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) +
+ row * (imageSize + (rows > 1 ? rowSpacing : 0));
+
+ if (boxImage) {
+ // 计算图片高度(等比例缩放)
+ // @ts-ignore - 微信小程序图片对象有 width 和 height 属性
+ const scaledHeight =
+ (boxImage.height / boxImage.width) * imageSize;
+
+ ctx.drawImage(
+ boxImage,
+ imageX,
+ imageY,
+ imageSize,
+ scaledHeight,
+ );
+ }
+ }
+ }
+
+ /**
+ * 绘制圆角矩形(虚线边框)
+ */
+ private drawRoundedRectInternal(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ radius: number,
+ isDashed: boolean = true,
+ ) {
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.quadraticCurveTo(
+ x + width,
+ y + height,
+ x + width - radius,
+ y + height,
+ );
+ ctx.lineTo(x + radius, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+
+ if (isDashed) {
+ ctx.setLineDash([6 / 3, 6 / 3]); // 约2px的虚线
+ } else {
+ ctx.setLineDash([]);
+ }
+ ctx.stroke();
+ ctx.setLineDash([]); // 重置为实线
+ }
}
export default CompareDraw;
diff --git a/miniprogram/mathPages/shared/service/countMatchContentDraw.ts b/miniprogram/mathPages/shared/service/countMatchContentDraw.ts
deleted file mode 100644
index 10546c5..0000000
--- a/miniprogram/mathPages/shared/service/countMatchContentDraw.ts
+++ /dev/null
@@ -1,294 +0,0 @@
-import { getImage } from '../../../utils/index';
-import { getRandomNumberColor } from '../../../constants/colors';
-
-interface DrawCountMatchContentParams {
- canvas: WechatMiniprogram.Canvas;
- ctx: RenderingContext;
- matchData: {
- leftNumbers: number[];
- rightNumbers: number[]; // 打乱顺序后的数字数组
- };
- canvasWidth: number;
- startY: number;
- imageType?: string; // 'twelve-animals' 或 'fruits'
-}
-
-/**
- * 绘制圆角矩形(虚线边框)
- */
-function drawRoundedRect(
- ctx: RenderingContext,
- x: number,
- y: number,
- width: number,
- height: number,
- radius: number,
- isDashed: boolean = true,
-) {
- ctx.beginPath();
- ctx.moveTo(x + radius, y);
- ctx.lineTo(x + width - radius, y);
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
- ctx.lineTo(x + width, y + height - radius);
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
- ctx.lineTo(x + radius, y + height);
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
- ctx.lineTo(x, y + radius);
- ctx.quadraticCurveTo(x, y, x + radius, y);
- ctx.closePath();
-
- if (isDashed) {
- ctx.setLineDash([6 / 3, 6 / 3]); // 约2px的虚线
- } else {
- ctx.setLineDash([]);
- }
- ctx.stroke();
- ctx.setLineDash([]); // 重置为实线
-}
-
-/**
- * 绘制数一数连一连内容区域
- * 左侧显示数字(虚线圆角框),右侧显示对应数量的图片(虚线圆角框)
- * 支持根据 imageType 选择不同的图片文件夹(twelve-animals 或 fruits)
- */
-export async function drawCountMatchContent({
- canvas,
- ctx,
- matchData,
- canvasWidth,
- startY,
- imageType = 'twelve-animals',
-}: DrawCountMatchContentParams): Promise {
- const { leftNumbers, rightNumbers } = matchData;
-
- // 根据图片类型确定图片目录和最大索引
- const imageConfig = {
- 'twelve-animals': {
- folder: 'twelve-animals',
- maxIndex: 12,
- },
- fruits: {
- folder: 'fruits',
- maxIndex: 22,
- },
- };
-
- const config =
- imageConfig[imageType as keyof typeof imageConfig] ||
- imageConfig['twelve-animals'];
- const leftMargin = 24;
- const rightMargin = 24;
- const itemSpacing = 140;
- const LeftBoxWidth = 120;
- const rightBoxWidth = 250;
- const boxHeight = 110;
- const borderRadius = 12;
- const imageSpacing = 5; // 图片之间的水平间距(同一行内)
- const rowSpacing = 10; // 行之间的垂直间距(两行之间)
- const imagesPerRow = 5; // 每行最多5张图片
- const maxImages = 10; // 最多10张图片
- const padding = 10; // 框内边距
- const startYPos = startY + 20;
- const linePointRadius = 8;
- const linePointSpacing = 12;
-
- /**
- * 根据图片数量动态计算图片宽度
- * @param count 图片数量
- * @returns 图片宽度
- */
- const getImageWidth = (count: number): number => {
- if (count <= 2) {
- return 90;
- } else if (count <= 3) {
- return 70;
- } else if (count <= 4) {
- return 55;
- } else {
- return 40;
- }
- };
-
- // 计算左侧和右侧的起始X位置
- const leftBoxX = leftMargin;
- const rightBoxX = canvasWidth - rightMargin - rightBoxWidth;
-
- // 绘制左侧数字框
- for (let i = 0; i < leftNumbers.length; i++) {
- const boxY = startYPos + i * itemSpacing;
- const number = leftNumbers[i];
-
- // 绘制虚线圆角矩形框
- ctx.strokeStyle = '#999';
- ctx.lineWidth = 1;
- drawRoundedRect(
- ctx,
- leftBoxX,
- boxY,
- LeftBoxWidth,
- boxHeight,
- borderRadius,
- true,
- );
-
- // 绘制数字(使用随机颜色)
- ctx.fillStyle = getRandomNumberColor();
- ctx.font = `bold ${56}px "Microsoft Yahei"`;
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText(
- String(number),
- leftBoxX + LeftBoxWidth / 2,
- boxY + boxHeight / 2,
- );
-
- // 绘制左侧框右侧的连线圆点(直径14,半径7)
- const leftCircleX =
- leftBoxX + LeftBoxWidth + linePointSpacing + linePointRadius;
- const leftCircleY = boxY + boxHeight / 2;
- ctx.fillStyle = '#93D333';
- ctx.beginPath();
- ctx.arc(leftCircleX, leftCircleY, linePointRadius, 0, Math.PI * 2);
- ctx.fill();
- }
-
- // 为每个框生成唯一的图片索引,确保不同框使用不同的图片
- const availableImageIndices = Array.from(
- { length: config.maxIndex },
- (_, i) => i + 1,
- );
- const boxImageIndices: number[] = [];
- for (let i = 0; i < rightNumbers.length; i++) {
- const randomIndex = Math.floor(
- Math.random() * availableImageIndices.length,
- );
- const imageIndex = availableImageIndices.splice(randomIndex, 1)[0];
- boxImageIndices.push(imageIndex);
- }
-
- // 绘制右侧图片框
- for (let i = 0; i < rightNumbers.length; i++) {
- const boxY = startYPos + i * itemSpacing;
- const number = rightNumbers[i]; // 这个数字决定了要绘制多少张图片
- const boxImageIndex = boxImageIndices[i]; // 这个框使用的图片索引(所有图片都一样)
-
- // 绘制虚线圆角矩形框
- ctx.strokeStyle = '#999';
- ctx.lineWidth = 1;
- drawRoundedRect(
- ctx,
- rightBoxX,
- boxY,
- rightBoxWidth,
- boxHeight,
- borderRadius,
- true,
- );
-
- // 计算要绘制的图片数量(最多10张)
- const imageCount = Math.min(number, maxImages);
-
- // 根据图片数量动态计算图片宽度
- const imageWidth = getImageWidth(imageCount);
-
- // 加载当前框使用的图片(只加载一次,用于计算高度和绘制)
- let boxImage: any = null;
- try {
- const imagePath = `/mathPages/assets/${config.folder}/${boxImageIndex}.png`;
- boxImage = await getImage(canvas, imagePath);
- } catch (error) {
- console.error(
- `加载${config.folder}/${boxImageIndex}图片失败:`,
- error,
- );
- }
-
- // 计算图片高度(等比例缩放)
- let imageHeight = imageWidth; // 默认高度
- if (boxImage) {
- // @ts-ignore - 微信小程序图片对象有 width 和 height 属性
- imageHeight = (boxImage.height / boxImage.width) * imageWidth;
- }
-
- // 计算每行的图片数量
- const imagesInFirstRow = Math.min(imageCount, imagesPerRow);
- const imagesInSecondRow =
- imageCount > imagesPerRow ? imageCount - imagesPerRow : 0;
-
- // 计算图片的总高度(考虑行数)
- const rowCount = imagesInSecondRow > 0 ? 2 : 1;
-
- const totalImageHeight =
- rowCount * imageHeight + (rowCount - 1) * rowSpacing;
- // 垂直居中:框顶部 + 内边距 + (框高度 - 上下内边距 - 图片总高度) / 2
- const startImageY =
- boxY + padding + (boxHeight - padding * 2 - totalImageHeight) / 2;
-
- // 计算每行的起始X位置(用于居中)
- const firstRowImageCount = imagesInFirstRow;
- const firstRowWidth =
- firstRowImageCount * imageWidth +
- (firstRowImageCount - 1) * imageSpacing;
- const firstRowStartX =
- rightBoxX +
- padding +
- (rightBoxWidth - padding * 2 - firstRowWidth) / 2;
-
- const secondRowImageCount = imagesInSecondRow;
- const secondRowWidth =
- secondRowImageCount * imageWidth +
- (secondRowImageCount - 1) * imageSpacing;
- const secondRowStartX =
- rightBoxX +
- padding +
- (rightBoxWidth - padding * 2 - secondRowWidth) / 2;
-
- // 绘制图片(同一个框内使用同一张图片)
- for (let imgIndex = 0; imgIndex < imageCount; imgIndex++) {
- const row = Math.floor(imgIndex / imagesPerRow);
- const col = imgIndex % imagesPerRow;
-
- // 计算图片的X位置(根据行数选择不同的起始位置)
- const rowStartX = row === 0 ? firstRowStartX : secondRowStartX;
- const imageX = rowStartX + col * (imageWidth + imageSpacing);
-
- // 计算图片的Y位置
- const imageY = startImageY + row * (imageHeight + rowSpacing);
-
- if (boxImage) {
- // 计算图片高度(等比例缩放)
- // @ts-ignore - 微信小程序图片对象有 width 和 height 属性
- const scaledHeight =
- (boxImage.height / boxImage.width) * imageWidth;
-
- ctx.drawImage(
- boxImage,
- imageX,
- imageY,
- imageWidth,
- scaledHeight,
- );
- } else {
- // 如果图片加载失败,绘制一个小圆点作为备用
- ctx.fillStyle = '#ccc';
- ctx.beginPath();
- ctx.arc(
- imageX + imageWidth / 2,
- imageY + imageHeight / 2,
- imageWidth / 4,
- 0,
- Math.PI * 2,
- );
- ctx.fill();
- }
- }
-
- // 绘制右侧框左侧的连线圆点(直径14,半径7)
- const rightCircleX = rightBoxX - linePointSpacing - linePointRadius; // 间距15 + 半径linePointRadius
- const rightCircleY = boxY + boxHeight / 2;
- ctx.fillStyle = '#93D333';
- ctx.beginPath();
- ctx.arc(rightCircleX, rightCircleY, linePointRadius, 0, Math.PI * 2);
- ctx.fill();
- }
-}
diff --git a/miniprogram/mathPages/shared/service/countMatchDraw.ts b/miniprogram/mathPages/shared/service/countMatchDraw.ts
index eee2f7e..69f026c 100644
--- a/miniprogram/mathPages/shared/service/countMatchDraw.ts
+++ b/miniprogram/mathPages/shared/service/countMatchDraw.ts
@@ -1,15 +1,24 @@
+/**
+ * 数一数连一连绘制服务
+ */
+
import { BaseDrawService } from '../../../service/baseDraw';
-import { drawCountMatchContent } from './countMatchContentDraw';
+import { getImage } from '../../../utils/index';
+import { getRandomNumberColor } from '../../../constants/colors';
+
+/**
+ * 数一数连一连数据
+ */
+export interface CountMatchData {
+ leftNumbers: number[];
+ rightNumbers: number[]; // 打乱顺序后的数字数组
+}
/**
* 数一数连一连绘制服务
- * 组合使用基础绘制服务和内容区域绘制服务
*/
class CountMatchDraw extends BaseDrawService {
- matchData: {
- leftNumbers: number[];
- rightNumbers: number[]; // 打乱顺序后的数字数组
- } | null;
+ matchData: CountMatchData | null = null;
constructor(
canvas: Canvas,
@@ -20,29 +29,21 @@ class CountMatchDraw extends BaseDrawService {
this.matchData = null;
}
+ /**
+ * 绘制数一数连一连内容
+ */
async draw(
- matchData: { leftNumbers: number[]; rightNumbers: number[] },
+ matchData: CountMatchData,
imageType: string = 'twelve-animals',
) {
if (!matchData || !matchData.leftNumbers || !matchData.rightNumbers) {
return;
}
- this.setPrintConfig();
this.matchData = matchData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域(数一数连一连)
- this.drawDivider();
- await drawCountMatchContent({
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
+ await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
matchData: this.matchData,
@@ -51,6 +52,305 @@ class CountMatchDraw extends BaseDrawService {
imageType,
});
}
+
+ /**
+ * 绘制内容区域
+ */
+ private async drawContent(params: {
+ canvas: WechatMiniprogram.Canvas;
+ ctx: RenderingContext;
+ matchData: CountMatchData;
+ canvasWidth: number;
+ startY: number;
+ imageType?: string; // 'twelve-animals' 或 'fruits'
+ }): Promise {
+ const {
+ canvas,
+ ctx,
+ matchData,
+ canvasWidth,
+ startY,
+ imageType = 'twelve-animals',
+ } = params;
+ const { leftNumbers, rightNumbers } = matchData;
+
+ // 根据图片类型确定图片目录和最大索引
+ const imageConfig = {
+ 'twelve-animals': {
+ folder: 'twelve-animals',
+ maxIndex: 12,
+ },
+ fruits: {
+ folder: 'fruits',
+ maxIndex: 22,
+ },
+ };
+
+ const config =
+ imageConfig[imageType as keyof typeof imageConfig] ||
+ imageConfig['twelve-animals'];
+ const leftMargin = 24;
+ const rightMargin = 24;
+ const itemSpacing = 140;
+ const LeftBoxWidth = 120;
+ const rightBoxWidth = 250;
+ const boxHeight = 110;
+ const borderRadius = 12;
+ const imageSpacing = 5; // 图片之间的水平间距(同一行内)
+ const rowSpacing = 10; // 行之间的垂直间距(两行之间)
+ const imagesPerRow = 5; // 每行最多5张图片
+ const maxImages = 10; // 最多10张图片
+ const padding = 10; // 框内边距
+ const startYPos = startY + 20;
+ const linePointRadius = 8;
+ const linePointSpacing = 12;
+
+ /**
+ * 根据图片数量动态计算图片宽度
+ * @param count 图片数量
+ * @returns 图片宽度
+ */
+ const getImageWidth = (count: number): number => {
+ if (count <= 2) {
+ return 90;
+ } else if (count <= 3) {
+ return 70;
+ } else if (count <= 4) {
+ return 55;
+ } else {
+ return 40;
+ }
+ };
+
+ // 计算左侧和右侧的起始X位置
+ const leftBoxX = leftMargin;
+ const rightBoxX = canvasWidth - rightMargin - rightBoxWidth;
+
+ // 绘制左侧数字框
+ for (let i = 0; i < leftNumbers.length; i++) {
+ const boxY = startYPos + i * itemSpacing;
+ const number = leftNumbers[i];
+
+ // 绘制虚线圆角矩形框
+ ctx.strokeStyle = '#999';
+ ctx.lineWidth = 1;
+ this.drawRoundedRectInternal(
+ ctx,
+ leftBoxX,
+ boxY,
+ LeftBoxWidth,
+ boxHeight,
+ borderRadius,
+ true,
+ );
+
+ // 绘制数字(使用随机颜色)
+ ctx.fillStyle = getRandomNumberColor();
+ ctx.font = `bold ${56}px "Microsoft Yahei"`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText(
+ String(number),
+ leftBoxX + LeftBoxWidth / 2,
+ boxY + boxHeight / 2,
+ );
+
+ // 绘制左侧框右侧的连线圆点(直径14,半径7)
+ const leftCircleX =
+ leftBoxX + LeftBoxWidth + linePointSpacing + linePointRadius;
+ const leftCircleY = boxY + boxHeight / 2;
+ ctx.fillStyle = '#93D333';
+ ctx.beginPath();
+ ctx.arc(leftCircleX, leftCircleY, linePointRadius, 0, Math.PI * 2);
+ ctx.fill();
+ }
+
+ // 为每个框生成唯一的图片索引,确保不同框使用不同的图片
+ const availableImageIndices = Array.from(
+ { length: config.maxIndex },
+ (_, i) => i + 1,
+ );
+ const boxImageIndices: number[] = [];
+ for (let i = 0; i < rightNumbers.length; i++) {
+ const randomIndex = Math.floor(
+ Math.random() * availableImageIndices.length,
+ );
+ const imageIndex = availableImageIndices.splice(randomIndex, 1)[0];
+ boxImageIndices.push(imageIndex);
+ }
+
+ // 绘制右侧图片框
+ for (let i = 0; i < rightNumbers.length; i++) {
+ const boxY = startYPos + i * itemSpacing;
+ const number = rightNumbers[i]; // 这个数字决定了要绘制多少张图片
+ const boxImageIndex = boxImageIndices[i]; // 这个框使用的图片索引(所有图片都一样)
+
+ // 绘制虚线圆角矩形框
+ ctx.strokeStyle = '#999';
+ ctx.lineWidth = 1;
+ this.drawRoundedRectInternal(
+ ctx,
+ rightBoxX,
+ boxY,
+ rightBoxWidth,
+ boxHeight,
+ borderRadius,
+ true,
+ );
+
+ // 计算要绘制的图片数量(最多10张)
+ const imageCount = Math.min(number, maxImages);
+
+ // 根据图片数量动态计算图片宽度
+ const imageWidth = getImageWidth(imageCount);
+
+ // 加载当前框使用的图片(只加载一次,用于计算高度和绘制)
+ let boxImage: any = null;
+ try {
+ const imagePath = `/mathPages/shared/assets/${config.folder}/${boxImageIndex}.png`;
+ boxImage = await getImage(canvas, imagePath);
+ } catch (error) {
+ console.error(
+ `加载${config.folder}/${boxImageIndex}图片失败:`,
+ error,
+ );
+ }
+
+ // 计算图片高度(等比例缩放)
+ let imageHeight = imageWidth; // 默认高度
+ if (boxImage) {
+ // @ts-ignore - 微信小程序图片对象有 width 和 height 属性
+ imageHeight = (boxImage.height / boxImage.width) * imageWidth;
+ }
+
+ // 计算每行的图片数量
+ const imagesInFirstRow = Math.min(imageCount, imagesPerRow);
+ const imagesInSecondRow =
+ imageCount > imagesPerRow ? imageCount - imagesPerRow : 0;
+
+ // 计算图片的总高度(考虑行数)
+ const rowCount = imagesInSecondRow > 0 ? 2 : 1;
+
+ const totalImageHeight =
+ rowCount * imageHeight + (rowCount - 1) * rowSpacing;
+ // 垂直居中:框顶部 + 内边距 + (框高度 - 上下内边距 - 图片总高度) / 2
+ const startImageY =
+ boxY +
+ padding +
+ (boxHeight - padding * 2 - totalImageHeight) / 2;
+
+ // 计算每行的起始X位置(用于居中)
+ const firstRowImageCount = imagesInFirstRow;
+ const firstRowWidth =
+ firstRowImageCount * imageWidth +
+ (firstRowImageCount - 1) * imageSpacing;
+ const firstRowStartX =
+ rightBoxX +
+ padding +
+ (rightBoxWidth - padding * 2 - firstRowWidth) / 2;
+
+ const secondRowImageCount = imagesInSecondRow;
+ const secondRowWidth =
+ secondRowImageCount * imageWidth +
+ (secondRowImageCount - 1) * imageSpacing;
+ const secondRowStartX =
+ rightBoxX +
+ padding +
+ (rightBoxWidth - padding * 2 - secondRowWidth) / 2;
+
+ // 绘制图片(同一个框内使用同一张图片)
+ for (let imgIndex = 0; imgIndex < imageCount; imgIndex++) {
+ const row = Math.floor(imgIndex / imagesPerRow);
+ const col = imgIndex % imagesPerRow;
+
+ // 计算图片的X位置(根据行数选择不同的起始位置)
+ const rowStartX = row === 0 ? firstRowStartX : secondRowStartX;
+ const imageX = rowStartX + col * (imageWidth + imageSpacing);
+
+ // 计算图片的Y位置
+ const imageY = startImageY + row * (imageHeight + rowSpacing);
+
+ if (boxImage) {
+ // 计算图片高度(等比例缩放)
+ // @ts-ignore - 微信小程序图片对象有 width 和 height 属性
+ const scaledHeight =
+ (boxImage.height / boxImage.width) * imageWidth;
+
+ ctx.drawImage(
+ boxImage,
+ imageX,
+ imageY,
+ imageWidth,
+ scaledHeight,
+ );
+ } else {
+ // 如果图片加载失败,绘制一个小圆点作为备用
+ ctx.fillStyle = '#ccc';
+ ctx.beginPath();
+ ctx.arc(
+ imageX + imageWidth / 2,
+ imageY + imageHeight / 2,
+ imageWidth / 4,
+ 0,
+ Math.PI * 2,
+ );
+ ctx.fill();
+ }
+ }
+
+ // 绘制右侧框左侧的连线圆点(直径14,半径7)
+ const rightCircleX = rightBoxX - linePointSpacing - linePointRadius; // 间距15 + 半径linePointRadius
+ const rightCircleY = boxY + boxHeight / 2;
+ ctx.fillStyle = '#93D333';
+ ctx.beginPath();
+ ctx.arc(
+ rightCircleX,
+ rightCircleY,
+ linePointRadius,
+ 0,
+ Math.PI * 2,
+ );
+ ctx.fill();
+ }
+ }
+
+ /**
+ * 绘制圆角矩形(虚线边框)
+ */
+ private drawRoundedRectInternal(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ radius: number,
+ isDashed: boolean = true,
+ ) {
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.quadraticCurveTo(
+ x + width,
+ y + height,
+ x + width - radius,
+ y + height,
+ );
+ ctx.lineTo(x + radius, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+
+ if (isDashed) {
+ ctx.setLineDash([6 / 3, 6 / 3]); // 约2px的虚线
+ } else {
+ ctx.setLineDash([]);
+ }
+ ctx.stroke();
+ ctx.setLineDash([]); // 重置为实线
+ }
}
export default CountMatchDraw;
diff --git a/miniprogram/mathPages/shared/service/countingSelectContentDraw.ts b/miniprogram/mathPages/shared/service/countingSelectContentDraw.ts
deleted file mode 100644
index 448050a..0000000
--- a/miniprogram/mathPages/shared/service/countingSelectContentDraw.ts
+++ /dev/null
@@ -1,312 +0,0 @@
-import { getImage } from '../../../utils/index';
-import { getRandomNumberColor } from '../../../constants/colors';
-
-interface DrawCountingSelectContentParams {
- canvas: WechatMiniprogram.Canvas;
- ctx: RenderingContext;
- countingData: {
- problems: Array<{
- count: number;
- imageIndex: number;
- imageType: 'fruits' | 'twelve-animals';
- options?: number[]; // 选一选模式需要,填一填模式不需要
- correctIndex?: number; // 选一选模式需要
- }>;
- };
- mode: 'select' | 'fill'; // 模式:'select' 显示选项,'fill' 显示括号
- canvasWidth: number;
- startY: number;
-}
-
-/**
- * 绘制圆角矩形(虚线边框)
- */
-function drawRoundedRect(
- ctx: RenderingContext,
- x: number,
- y: number,
- width: number,
- height: number,
- radius: number,
- isDashed: boolean = true,
-) {
- ctx.beginPath();
- ctx.moveTo(x + radius, y);
- ctx.lineTo(x + width - radius, y);
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
- ctx.lineTo(x + width, y + height - radius);
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
- ctx.lineTo(x + radius, y + height);
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
- ctx.lineTo(x, y + radius);
- ctx.quadraticCurveTo(x, y, x + radius, y);
- ctx.closePath();
-
- if (isDashed) {
- ctx.setLineDash([6 / 3, 6 / 3]); // 约2px的虚线
- } else {
- ctx.setLineDash([]);
- }
- ctx.stroke();
- ctx.setLineDash([]); // 重置为实线
-}
-
-/**
- * 绘制数一数选一选/填一填内容区域
- * 3x3网格布局,每个框上面是图片,下面根据模式显示选项或括号
- */
-export async function drawCountingSelectContent({
- canvas,
- ctx,
- countingData,
- mode,
- canvasWidth,
- startY,
-}: DrawCountingSelectContentParams): Promise {
- const { problems } = countingData;
-
- // 布局参数
- const leftMargin = 30;
- const rightMargin = 30;
- const topMargin = 20;
- const boxSpacing = 20; // 框之间的间距
-
- // 计算每个框的尺寸
- const availableWidth = canvasWidth - leftMargin - rightMargin;
- const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
- const boxHeight = 210; // 框的高度(增加)
-
- // 图片区域高度
- const imageAreaHeight = 160;
- // 底部区域高度(选项表格或括号区域)
- const bottomAreaHeight = boxHeight - imageAreaHeight;
-
- // 选项表格相关(仅 select 模式使用)
- const optionCellWidth = boxWidth / 3; // 每列宽度(均匀分三列)
-
- let currentY = startY + topMargin;
-
- // 绘制3x3网格
- for (let row = 0; row < 3; row++) {
- for (let col = 0; col < 3; col++) {
- const problemIndex = row * 3 + col;
- if (problemIndex >= problems.length) {
- continue;
- }
-
- const problem = problems[problemIndex];
-
- // 计算框的位置
- const boxX = leftMargin + col * (boxWidth + boxSpacing);
- const boxY = currentY + row * (boxHeight + boxSpacing);
-
- // 绘制一个完整的题目框
- await drawProblemBox(
- ctx,
- canvas,
- problem,
- boxX,
- boxY,
- boxWidth,
- boxHeight,
- imageAreaHeight,
- bottomAreaHeight,
- optionCellWidth,
- mode,
- );
- }
- }
-}
-
-/**
- * 绘制一个题目框(图片 + 选项表格 或 括号)
- */
-async function drawProblemBox(
- ctx: RenderingContext,
- canvas: WechatMiniprogram.Canvas,
- problem: {
- count: number;
- imageIndex: number;
- imageType: 'fruits' | 'twelve-animals';
- options?: number[];
- correctIndex?: number;
- },
- boxX: number,
- boxY: number,
- boxWidth: number,
- boxHeight: number,
- imageAreaHeight: number,
- bottomAreaHeight: number,
- optionCellWidth: number,
- mode: 'select' | 'fill',
-) {
- const borderRadius = 12;
-
- // 绘制实线框
- ctx.strokeStyle = '#333';
- ctx.lineWidth = 1;
- drawRoundedRect(ctx, boxX, boxY, boxWidth, boxHeight, borderRadius, false);
-
- // 图片区域
- const imageAreaX = boxX;
- const imageAreaY = boxY;
-
- // 绘制图片
- await drawImagesInBox(
- ctx,
- canvas,
- problem.count,
- problem.imageIndex,
- problem.imageType,
- imageAreaX,
- imageAreaY,
- boxWidth,
- imageAreaHeight,
- );
-
- // 底部区域
- const bottomAreaX = boxX;
- const bottomAreaY = boxY + imageAreaHeight;
-
- // 绘制底部区域的顶部横线(两种模式都需要)
- ctx.strokeStyle = '#333';
- ctx.lineWidth = 1;
- ctx.beginPath();
- ctx.moveTo(bottomAreaX, bottomAreaY);
- ctx.lineTo(bottomAreaX + boxWidth, bottomAreaY);
- ctx.stroke();
-
- if (mode === 'select' && problem.options) {
- // 选一选模式:绘制三个数字选项(表格样式)
- // 绘制三个单元格(均匀分三列)
- for (let i = 0; i < problem.options.length; i++) {
- const cellX = bottomAreaX + i * optionCellWidth;
- const cellY = bottomAreaY;
-
- // 绘制单元格边框(右边框,除了最后一个)
- if (i < problem.options.length - 1) {
- ctx.beginPath();
- ctx.moveTo(cellX + optionCellWidth, cellY);
- ctx.lineTo(cellX + optionCellWidth, cellY + bottomAreaHeight);
- ctx.stroke();
- }
-
- // 绘制数字(居中)
- const numberX = cellX + optionCellWidth / 2;
- const numberY = cellY + bottomAreaHeight / 2;
-
- ctx.fillStyle = getRandomNumberColor();
- ctx.font = `bold ${28}px "Microsoft Yahei"`;
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText(String(problem.options[i]), numberX, numberY);
- }
- } else if (mode === 'fill') {
- // 填一填模式:绘制括号 "( )"
- const bracketFontSize = 28;
- const bracketSpacing = 46; // 左右括号之间的间距(留空区域)
-
- // 计算括号位置(居中)
- const bracketY = bottomAreaY + bottomAreaHeight / 2;
- const centerX = boxX + boxWidth / 2;
-
- // 左括号
- ctx.fillStyle = '#333';
- ctx.font = `${bracketFontSize}px "Microsoft Yahei"`;
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- const leftBracketX = centerX - bracketSpacing / 2;
- ctx.fillText('(', leftBracketX, bracketY);
-
- // 右括号
- const rightBracketX = centerX + bracketSpacing / 2;
- ctx.fillText(')', rightBracketX, bracketY);
- }
-}
-
-/**
- * 在框内绘制多张图片
- */
-async function drawImagesInBox(
- ctx: RenderingContext,
- canvas: WechatMiniprogram.Canvas,
- count: number,
- imageIndex: number,
- imageType: 'fruits' | 'twelve-animals',
- boxX: number,
- boxY: number,
- boxWidth: number,
- boxHeight: number,
-) {
- // 图片配置
- const imageConfig = {
- 'twelve-animals': {
- folder: 'twelve-animals',
- maxIndex: 12,
- },
- fruits: {
- folder: 'fruits',
- maxIndex: 22,
- },
- };
-
- const config = imageConfig[imageType] || imageConfig['twelve-animals'];
-
- const padding = 8; // 框内边距
- const availableWidth = boxWidth - padding * 2;
- const availableHeight = boxHeight - padding * 2;
-
- // 根据数量确定每行的图片数和图片大小
- let imagesPerRow: number;
- let imageSize: number;
-
- if (count <= 4) {
- imagesPerRow = count <= 2 ? count : 2;
- imageSize =
- Math.min(availableWidth / imagesPerRow, availableHeight / 2) - 4;
- } else if (count <= 6) {
- imagesPerRow = 3;
- imageSize = Math.min(availableWidth / 3, availableHeight / 2) - 4;
- } else {
- imagesPerRow = 3;
- imageSize = Math.min(availableWidth / 3, availableHeight / 3) - 4;
- }
-
- const rows = Math.ceil(count / imagesPerRow);
- const imageSpacing =
- (availableWidth - imageSize * imagesPerRow) / (imagesPerRow + 1);
- const rowSpacing =
- rows > 1 ? (availableHeight - imageSize * rows) / (rows + 1) : 0;
-
- // 加载图片
- let boxImage: any = null;
- try {
- const imagePath = `/mathPages/assets/${config.folder}/${imageIndex}.png`;
- boxImage = await getImage(canvas, imagePath);
- } catch (error) {
- console.error(`加载${config.folder}/${imageIndex}图片失败:`, error);
- return;
- }
-
- // 绘制图片
- for (let i = 0; i < count; i++) {
- const row = Math.floor(i / imagesPerRow);
- const col = i % imagesPerRow;
-
- const imageX =
- boxX + padding + imageSpacing + col * (imageSize + imageSpacing);
- const imageY =
- boxY +
- padding +
- (rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) +
- row * (imageSize + (rows > 1 ? rowSpacing : 0));
-
- if (boxImage) {
- // 计算图片高度(等比例缩放)
- // @ts-ignore - 微信小程序图片对象有 width 和 height 属性
- const scaledHeight = (boxImage.height / boxImage.width) * imageSize;
-
- ctx.drawImage(boxImage, imageX, imageY, imageSize, scaledHeight);
- }
- }
-}
diff --git a/miniprogram/mathPages/shared/service/countingSelectDraw.ts b/miniprogram/mathPages/shared/service/countingSelectDraw.ts
index d6844aa..f4ecfc8 100644
--- a/miniprogram/mathPages/shared/service/countingSelectDraw.ts
+++ b/miniprogram/mathPages/shared/service/countingSelectDraw.ts
@@ -1,21 +1,30 @@
+/**
+ * 数一数选一选/填一填绘制服务
+ * 支持两种模式:'select'(选一选)和 'fill'(填一填)
+ */
+
import { BaseDrawService } from '../../../service/baseDraw';
-import { drawCountingSelectContent } from './countingSelectContentDraw';
+import { getImage } from '../../../utils/index';
+import { getRandomNumberColor } from '../../../constants/colors';
+
+/**
+ * 数一数选一选/填一填数据
+ */
+export interface CountingSelectData {
+ problems: Array<{
+ count: number;
+ imageIndex: number;
+ imageType: 'fruits' | 'twelve-animals';
+ options?: number[]; // 选一选模式需要,填一填模式不需要
+ correctIndex?: number; // 选一选模式需要
+ }>;
+}
/**
* 数一数选一选/填一填绘制服务
- * 组合使用基础绘制服务和内容区域绘制服务
- * 支持两种模式:'select'(选一选)和 'fill'(填一填)
*/
class CountingSelectDraw extends BaseDrawService {
- countingData: {
- problems: Array<{
- count: number;
- imageIndex: number;
- imageType: 'fruits' | 'twelve-animals';
- options?: number[];
- correctIndex?: number;
- }>;
- } | null;
+ countingData: CountingSelectData | null = null;
mode: 'select' | 'fill';
constructor(
@@ -28,38 +37,22 @@ class CountingSelectDraw extends BaseDrawService {
this.mode = 'select'; // 默认选一选模式
}
+ /**
+ * 绘制数一数选一选/填一填内容
+ */
async draw(
- countingData: {
- problems: Array<{
- count: number;
- imageIndex: number;
- imageType: 'fruits' | 'twelve-animals';
- options?: number[];
- correctIndex?: number;
- }>;
- },
+ countingData: CountingSelectData,
mode: 'select' | 'fill' = 'select',
) {
if (!countingData || !countingData.problems) {
return;
}
- this.setPrintConfig();
this.countingData = countingData;
this.mode = mode;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域
- this.drawDivider();
- await drawCountingSelectContent({
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
+ await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
countingData: this.countingData,
@@ -68,6 +61,326 @@ class CountingSelectDraw extends BaseDrawService {
startY: this.currentY,
});
}
+
+ /**
+ * 绘制内容区域
+ */
+ private async drawContent(params: {
+ canvas: WechatMiniprogram.Canvas;
+ ctx: RenderingContext;
+ countingData: CountingSelectData;
+ mode: 'select' | 'fill';
+ canvasWidth: number;
+ startY: number;
+ }): Promise {
+ const { canvas, ctx, countingData, mode, canvasWidth, startY } = params;
+ const { problems } = countingData;
+
+ // 布局参数
+ const leftMargin = 30;
+ const rightMargin = 30;
+ const topMargin = 20;
+ const boxSpacing = 20; // 框之间的间距
+
+ // 计算每个框的尺寸
+ const availableWidth = canvasWidth - leftMargin - rightMargin;
+ const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
+ const boxHeight = 210; // 框的高度(增加)
+
+ // 图片区域高度
+ const imageAreaHeight = 160;
+ // 底部区域高度(选项表格或括号区域)
+ const bottomAreaHeight = boxHeight - imageAreaHeight;
+
+ // 选项表格相关(仅 select 模式使用)
+ const optionCellWidth = boxWidth / 3; // 每列宽度(均匀分三列)
+
+ let currentY = startY + topMargin;
+
+ // 绘制3x3网格
+ for (let row = 0; row < 3; row++) {
+ for (let col = 0; col < 3; col++) {
+ const problemIndex = row * 3 + col;
+ if (problemIndex >= problems.length) {
+ continue;
+ }
+
+ const problem = problems[problemIndex];
+
+ // 计算框的位置
+ const boxX = leftMargin + col * (boxWidth + boxSpacing);
+ const boxY = currentY + row * (boxHeight + boxSpacing);
+
+ // 绘制一个完整的题目框
+ await this.drawProblemBox(
+ ctx,
+ canvas,
+ problem,
+ boxX,
+ boxY,
+ boxWidth,
+ boxHeight,
+ imageAreaHeight,
+ bottomAreaHeight,
+ optionCellWidth,
+ mode,
+ );
+ }
+ }
+ }
+
+ /**
+ * 绘制一个题目框(图片 + 选项表格 或 括号)
+ */
+ private async drawProblemBox(
+ ctx: RenderingContext,
+ canvas: WechatMiniprogram.Canvas,
+ problem: {
+ count: number;
+ imageIndex: number;
+ imageType: 'fruits' | 'twelve-animals';
+ options?: number[];
+ correctIndex?: number;
+ },
+ boxX: number,
+ boxY: number,
+ boxWidth: number,
+ boxHeight: number,
+ imageAreaHeight: number,
+ bottomAreaHeight: number,
+ optionCellWidth: number,
+ mode: 'select' | 'fill',
+ ) {
+ const borderRadius = 12;
+
+ // 绘制实线框
+ ctx.strokeStyle = '#333';
+ ctx.lineWidth = 1;
+ this.drawRoundedRectInternal(
+ ctx,
+ boxX,
+ boxY,
+ boxWidth,
+ boxHeight,
+ borderRadius,
+ false,
+ );
+
+ // 图片区域
+ const imageAreaX = boxX;
+ const imageAreaY = boxY;
+
+ // 绘制图片
+ await this.drawImagesInBox(
+ ctx,
+ canvas,
+ problem.count,
+ problem.imageIndex,
+ problem.imageType,
+ imageAreaX,
+ imageAreaY,
+ boxWidth,
+ imageAreaHeight,
+ );
+
+ // 底部区域
+ const bottomAreaX = boxX;
+ const bottomAreaY = boxY + imageAreaHeight;
+
+ // 绘制底部区域的顶部横线(两种模式都需要)
+ ctx.strokeStyle = '#333';
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.moveTo(bottomAreaX, bottomAreaY);
+ ctx.lineTo(bottomAreaX + boxWidth, bottomAreaY);
+ ctx.stroke();
+
+ if (mode === 'select' && problem.options) {
+ // 选一选模式:绘制三个数字选项(表格样式)
+ // 绘制三个单元格(均匀分三列)
+ for (let i = 0; i < problem.options.length; i++) {
+ const cellX = bottomAreaX + i * optionCellWidth;
+ const cellY = bottomAreaY;
+
+ // 绘制单元格边框(右边框,除了最后一个)
+ if (i < problem.options.length - 1) {
+ ctx.beginPath();
+ ctx.moveTo(cellX + optionCellWidth, cellY);
+ ctx.lineTo(
+ cellX + optionCellWidth,
+ cellY + bottomAreaHeight,
+ );
+ ctx.stroke();
+ }
+
+ // 绘制数字(居中)
+ const numberX = cellX + optionCellWidth / 2;
+ const numberY = cellY + bottomAreaHeight / 2;
+
+ ctx.fillStyle = getRandomNumberColor();
+ ctx.font = `bold ${28}px "Microsoft Yahei"`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText(String(problem.options[i]), numberX, numberY);
+ }
+ } else if (mode === 'fill') {
+ // 填一填模式:绘制括号 "( )"
+ const bracketFontSize = 28;
+ const bracketSpacing = 46; // 左右括号之间的间距(留空区域)
+
+ // 计算括号位置(居中)
+ const bracketY = bottomAreaY + bottomAreaHeight / 2;
+ const centerX = boxX + boxWidth / 2;
+
+ // 左括号
+ ctx.fillStyle = '#333';
+ ctx.font = `${bracketFontSize}px "Microsoft Yahei"`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ const leftBracketX = centerX - bracketSpacing / 2;
+ ctx.fillText('(', leftBracketX, bracketY);
+
+ // 右括号
+ const rightBracketX = centerX + bracketSpacing / 2;
+ ctx.fillText(')', rightBracketX, bracketY);
+ }
+ }
+
+ /**
+ * 在框内绘制多张图片
+ */
+ private async drawImagesInBox(
+ ctx: RenderingContext,
+ canvas: WechatMiniprogram.Canvas,
+ count: number,
+ imageIndex: number,
+ imageType: 'fruits' | 'twelve-animals',
+ boxX: number,
+ boxY: number,
+ boxWidth: number,
+ boxHeight: number,
+ ) {
+ // 图片配置
+ const imageConfig = {
+ 'twelve-animals': {
+ folder: 'twelve-animals',
+ maxIndex: 12,
+ },
+ fruits: {
+ folder: 'fruits',
+ maxIndex: 22,
+ },
+ };
+
+ const config = imageConfig[imageType] || imageConfig['twelve-animals'];
+
+ const padding = 8; // 框内边距
+ const availableWidth = boxWidth - padding * 2;
+ const availableHeight = boxHeight - padding * 2;
+
+ // 根据数量确定每行的图片数和图片大小
+ let imagesPerRow: number;
+ let imageSize: number;
+
+ if (count <= 4) {
+ imagesPerRow = count <= 2 ? count : 2;
+ imageSize =
+ Math.min(availableWidth / imagesPerRow, availableHeight / 2) -
+ 4;
+ } else if (count <= 6) {
+ imagesPerRow = 3;
+ imageSize = Math.min(availableWidth / 3, availableHeight / 2) - 4;
+ } else {
+ imagesPerRow = 3;
+ imageSize = Math.min(availableWidth / 3, availableHeight / 3) - 4;
+ }
+
+ const rows = Math.ceil(count / imagesPerRow);
+ const imageSpacing =
+ (availableWidth - imageSize * imagesPerRow) / (imagesPerRow + 1);
+ const rowSpacing =
+ rows > 1 ? (availableHeight - imageSize * rows) / (rows + 1) : 0;
+
+ // 加载图片
+ let boxImage: any = null;
+ try {
+ const imagePath = `/mathPages/shared/assets/${config.folder}/${imageIndex}.png`;
+ boxImage = await getImage(canvas, imagePath);
+ } catch (error) {
+ console.error(`加载${config.folder}/${imageIndex}图片失败:`, error);
+ return;
+ }
+
+ // 绘制图片
+ for (let i = 0; i < count; i++) {
+ const row = Math.floor(i / imagesPerRow);
+ const col = i % imagesPerRow;
+
+ const imageX =
+ boxX +
+ padding +
+ imageSpacing +
+ col * (imageSize + imageSpacing);
+ const imageY =
+ boxY +
+ padding +
+ (rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) +
+ row * (imageSize + (rows > 1 ? rowSpacing : 0));
+
+ if (boxImage) {
+ // 计算图片高度(等比例缩放)
+ // @ts-ignore - 微信小程序图片对象有 width 和 height 属性
+ const scaledHeight =
+ (boxImage.height / boxImage.width) * imageSize;
+
+ ctx.drawImage(
+ boxImage,
+ imageX,
+ imageY,
+ imageSize,
+ scaledHeight,
+ );
+ }
+ }
+ }
+
+ /**
+ * 绘制圆角矩形(虚线边框)
+ */
+ private drawRoundedRectInternal(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ radius: number,
+ isDashed: boolean = true,
+ ) {
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.quadraticCurveTo(
+ x + width,
+ y + height,
+ x + width - radius,
+ y + height,
+ );
+ ctx.lineTo(x + radius, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+
+ if (isDashed) {
+ ctx.setLineDash([6 / 3, 6 / 3]); // 约2px的虚线
+ } else {
+ ctx.setLineDash([]);
+ }
+ ctx.stroke();
+ ctx.setLineDash([]); // 重置为实线
+ }
}
export default CountingSelectDraw;
diff --git a/miniprogram/mathPages/shared/service/missingNumberContentDraw.ts b/miniprogram/mathPages/shared/service/missingNumberContentDraw.ts
deleted file mode 100644
index 01c52f4..0000000
--- a/miniprogram/mathPages/shared/service/missingNumberContentDraw.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-interface DrawMissingNumberContentParams {
- canvas: WechatMiniprogram.Canvas;
- ctx: RenderingContext;
- missingNumberData: {
- grids: Array<{
- numbers: (number | null)[];
- colors: (string | null)[];
- }>;
- maxNumber: number;
- };
- canvasWidth: number;
- startY: number;
-}
-
-const gridMap = {
- 10: {
- cellSize: 95,
- colsPerGrid: 5,
- fontSize: 50,
- gridSpacing: 40,
- },
- 20: {
- cellSize: 80,
- colsPerGrid: 5,
- fontSize: 40,
- gridSpacing: 30,
- },
- 40: {
- cellSize: 60,
- colsPerGrid: 8,
- fontSize: 35,
- gridSpacing: 30,
- },
- 50: {
- cellSize: 52,
- colsPerGrid: 10,
- fontSize: 28,
- gridSpacing: 40,
- },
- 80: {
- cellSize: 60,
- colsPerGrid: 8,
- fontSize: 30,
- gridSpacing: 30,
- },
- 100: {
- cellSize: 52,
- colsPerGrid: 10,
- fontSize: 28,
- gridSpacing: 20,
- },
- 120: {
- cellSize: 52,
- colsPerGrid: 10,
- fontSize: 28,
- gridSpacing: 20,
- },
-};
-
-/**
- * 绘制填上缺少的数字内容区域
- * 显示多个网格,每个网格包含一些数字和一些空白位置
- */
-export async function drawMissingNumberContent({
- ctx,
- missingNumberData,
- canvasWidth,
- startY,
-}: DrawMissingNumberContentParams): Promise {
- const { grids, maxNumber } = missingNumberData;
-
- // 计算网格布局参数
- const leftMargin = 40;
- const rightMargin = 40;
- const topMargin = 20;
-
- const config = gridMap[maxNumber as keyof typeof gridMap];
- const { cellSize, gridSpacing, colsPerGrid, fontSize } = config;
-
- const gridWidth = colsPerGrid * cellSize;
-
- // 计算网格的起始X位置(居中)
- const gridStartX =
- leftMargin + (canvasWidth - leftMargin - rightMargin - gridWidth) / 2;
-
- let currentY = startY + topMargin;
-
- // 绘制每个网格
- for (let gridIndex = 0; gridIndex < grids.length; gridIndex++) {
- const grid = grids[gridIndex];
- const gridX = gridStartX;
-
- // 根据实际数字数量计算需要的行数
- const rowsPerGrid = Math.ceil(grid.numbers.length / colsPerGrid);
- const gridHeight = rowsPerGrid * cellSize;
- const gridY = currentY;
-
- // 绘制每个单元格
- for (let index = 0; index < grid.numbers.length; index++) {
- const row = Math.floor(index / colsPerGrid);
- const col = index % colsPerGrid;
-
- const cellX = gridX + col * cellSize;
- const cellY = gridY + row * cellSize;
-
- // 绘制单元格边框
- ctx.strokeStyle = '#333';
- ctx.lineWidth = 1.5;
- ctx.strokeRect(cellX, cellY, cellSize, cellSize);
-
- // 绘制数字或空白
- const number = grid.numbers[index];
- const color = grid.colors[index];
-
- if (number !== null && color !== null) {
- // 绘制数字(带颜色)
- ctx.fillStyle = color;
- ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText(
- String(number),
- cellX + cellSize / 2,
- cellY + cellSize / 2,
- );
- }
- // 如果为null,则留空(用户填写)
- }
-
- // 更新下一个网格的Y位置
- currentY += gridHeight + gridSpacing;
- }
-}
diff --git a/miniprogram/mathPages/shared/service/missingNumberDraw.ts b/miniprogram/mathPages/shared/service/missingNumberDraw.ts
index bfb8051..aced97b 100644
--- a/miniprogram/mathPages/shared/service/missingNumberDraw.ts
+++ b/miniprogram/mathPages/shared/service/missingNumberDraw.ts
@@ -1,18 +1,25 @@
+/**
+ * 填上缺少的数字绘制服务
+ */
+
import { BaseDrawService } from '../../../service/baseDraw';
-import { drawMissingNumberContent } from './missingNumberContentDraw';
+
+/**
+ * 填上缺少的数字数据
+ */
+export interface MissingNumberData {
+ grids: Array<{
+ numbers: (number | null)[];
+ colors: (string | null)[];
+ }>;
+ maxNumber: number;
+}
/**
* 填上缺少的数字绘制服务
- * 组合使用基础绘制服务和内容区域绘制服务
*/
class MissingNumberDraw extends BaseDrawService {
- missingNumberData: {
- grids: Array<{
- numbers: (number | null)[];
- colors: (string | null)[];
- }>;
- maxNumber: number;
- } | null;
+ missingNumberData: MissingNumberData | null = null;
constructor(
canvas: Canvas,
@@ -23,35 +30,21 @@ class MissingNumberDraw extends BaseDrawService {
this.missingNumberData = null;
}
+ /**
+ * 绘制填上缺少的数字内容
+ */
async draw(
- missingNumberData: {
- grids: Array<{
- numbers: (number | null)[];
- colors: (string | null)[];
- }>;
- maxNumber: number;
- },
+ missingNumberData: MissingNumberData,
rangeType: string = 'within-10',
) {
if (!missingNumberData || !missingNumberData.grids) {
return;
}
- this.setPrintConfig();
this.missingNumberData = missingNumberData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域(填上缺少的数字)
- this.drawDivider();
- await drawMissingNumberContent({
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
+ await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
missingNumberData: this.missingNumberData,
@@ -59,6 +52,128 @@ class MissingNumberDraw extends BaseDrawService {
startY: this.currentY,
});
}
+
+ /**
+ * 绘制内容区域
+ */
+ private async drawContent(params: {
+ canvas: WechatMiniprogram.Canvas;
+ ctx: RenderingContext;
+ missingNumberData: MissingNumberData;
+ canvasWidth: number;
+ startY: number;
+ }): Promise {
+ const { ctx, missingNumberData, canvasWidth, startY } = params;
+ const { grids, maxNumber } = missingNumberData;
+
+ const gridMap = {
+ 10: {
+ cellSize: 95,
+ colsPerGrid: 5,
+ fontSize: 50,
+ gridSpacing: 40,
+ },
+ 20: {
+ cellSize: 80,
+ colsPerGrid: 5,
+ fontSize: 40,
+ gridSpacing: 30,
+ },
+ 40: {
+ cellSize: 60,
+ colsPerGrid: 8,
+ fontSize: 35,
+ gridSpacing: 30,
+ },
+ 50: {
+ cellSize: 52,
+ colsPerGrid: 10,
+ fontSize: 28,
+ gridSpacing: 40,
+ },
+ 80: {
+ cellSize: 60,
+ colsPerGrid: 8,
+ fontSize: 30,
+ gridSpacing: 30,
+ },
+ 100: {
+ cellSize: 52,
+ colsPerGrid: 10,
+ fontSize: 28,
+ gridSpacing: 20,
+ },
+ 120: {
+ cellSize: 52,
+ colsPerGrid: 10,
+ fontSize: 28,
+ gridSpacing: 20,
+ },
+ };
+
+ // 计算网格布局参数
+ const leftMargin = 40;
+ const rightMargin = 40;
+ const topMargin = 20;
+
+ const config = gridMap[maxNumber as keyof typeof gridMap];
+ const { cellSize, gridSpacing, colsPerGrid, fontSize } = config;
+
+ const gridWidth = colsPerGrid * cellSize;
+
+ // 计算网格的起始X位置(居中)
+ const gridStartX =
+ leftMargin +
+ (canvasWidth - leftMargin - rightMargin - gridWidth) / 2;
+
+ let currentY = startY + topMargin;
+
+ // 绘制每个网格
+ for (let gridIndex = 0; gridIndex < grids.length; gridIndex++) {
+ const grid = grids[gridIndex];
+ const gridX = gridStartX;
+
+ // 根据实际数字数量计算需要的行数
+ const rowsPerGrid = Math.ceil(grid.numbers.length / colsPerGrid);
+ const gridHeight = rowsPerGrid * cellSize;
+ const gridY = currentY;
+
+ // 绘制每个单元格
+ for (let index = 0; index < grid.numbers.length; index++) {
+ const row = Math.floor(index / colsPerGrid);
+ const col = index % colsPerGrid;
+
+ const cellX = gridX + col * cellSize;
+ const cellY = gridY + row * cellSize;
+
+ // 绘制单元格边框
+ ctx.strokeStyle = '#333';
+ ctx.lineWidth = 1.5;
+ ctx.strokeRect(cellX, cellY, cellSize, cellSize);
+
+ // 绘制数字或空白
+ const number = grid.numbers[index];
+ const color = grid.colors[index];
+
+ if (number !== null && color !== null) {
+ // 绘制数字(带颜色)
+ ctx.fillStyle = color;
+ ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText(
+ String(number),
+ cellX + cellSize / 2,
+ cellY + cellSize / 2,
+ );
+ }
+ // 如果为null,则留空(用户填写)
+ }
+
+ // 更新下一个网格的Y位置
+ currentY += gridHeight + gridSpacing;
+ }
+ }
}
export default MissingNumberDraw;
diff --git a/miniprogram/mathPages/shared/service/numberColorContentDraw.ts b/miniprogram/mathPages/shared/service/numberColorContentDraw.ts
deleted file mode 100644
index 4718c46..0000000
--- a/miniprogram/mathPages/shared/service/numberColorContentDraw.ts
+++ /dev/null
@@ -1,459 +0,0 @@
-import { getNumberColors } from '../../../constants/colors';
-
-interface DrawNumberColorContentParams {
- canvas: WechatMiniprogram.Canvas;
- ctx: RenderingContext;
- colorData: {
- numbers: number[];
- };
- canvasWidth: number;
- startY: number;
- patternType?: string; // 'circle' 或 'caterpillar'
-}
-
-/**
- * 绘制圆角矩形(虚线边框)
- */
-function drawRoundedRect(
- ctx: RenderingContext,
- x: number,
- y: number,
- width: number,
- height: number,
- radius: number,
- isDashed: boolean = true,
-) {
- ctx.beginPath();
- ctx.moveTo(x + radius, y);
- ctx.lineTo(x + width - radius, y);
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
- ctx.lineTo(x + width, y + height - radius);
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
- ctx.lineTo(x + radius, y + height);
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
- ctx.lineTo(x, y + radius);
- ctx.quadraticCurveTo(x, y, x + radius, y);
- ctx.closePath();
-
- if (isDashed) {
- ctx.setLineDash([6, 6]); // 虚线
- } else {
- ctx.setLineDash([]);
- }
- ctx.stroke();
- ctx.setLineDash([]); // 重置为实线
-}
-
-/**
- * 绘制毛毛虫头部
- */
-function drawCaterpillarHead(
- ctx: RenderingContext,
- x: number,
- y: number,
- size: number,
- color: string,
-) {
- const headRadius = 18; // 固定头部半径
- const headCenterX = x + headRadius;
- const headCenterY = y + size / 2;
-
- // 绘制头部(圆形)
- ctx.fillStyle = color;
- ctx.beginPath();
- ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2);
- ctx.fill();
-
- // 绘制头部圆圈实线边框
- ctx.strokeStyle = '#000';
- ctx.lineWidth = 1;
- ctx.setLineDash([]); // 确保是实线
- ctx.beginPath();
- ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2);
- ctx.stroke();
-
- // 绘制触角(在头部圆圈的正上方,带弧度的弯曲)
- const antennaLength = headRadius * 0.6;
- const antennaStartY = headCenterY - headRadius; // 头部圆圈的正上方
- const antennaOffset = headRadius * 0.3; // 触角左右偏移距离
-
- // 左侧触角:从顶部左侧开始,先向上,然后向左弯曲(带弧度)
- const leftAntennaStartX = headCenterX - antennaOffset;
- const leftAntennaStartY = antennaStartY;
- const leftAntennaMidY = leftAntennaStartY - antennaLength * 0.5; // 向上延伸的中点
- const leftAntennaEndX = leftAntennaStartX - antennaLength * 0.4; // 向左弯曲
- const leftAntennaEndY = leftAntennaMidY - antennaLength * 0.3; // 向上并向左
-
- // 右侧触角:从顶部右侧开始,先向上,然后向右弯曲(带弧度)
- const rightAntennaStartX = headCenterX + antennaOffset;
- const rightAntennaStartY = antennaStartY;
- const rightAntennaMidY = rightAntennaStartY - antennaLength * 0.5; // 向上延伸的中点
- const rightAntennaEndX = rightAntennaStartX + antennaLength * 0.4; // 向右弯曲
- const rightAntennaEndY = rightAntennaMidY - antennaLength * 0.3; // 向上并向右
-
- ctx.strokeStyle = '#000';
- ctx.lineWidth = 1.5;
- ctx.lineCap = 'round'; // 圆角端点
-
- // 绘制左侧触角(使用二次贝塞尔曲线实现平滑弧度)
- ctx.beginPath();
- ctx.moveTo(leftAntennaStartX, leftAntennaStartY);
- // 使用二次贝塞尔曲线:起点、控制点、终点
- // 控制点在中间位置,使曲线平滑向左弯曲
- const leftControlX = leftAntennaStartX - antennaLength * 0.2;
- const leftControlY = leftAntennaMidY;
- ctx.quadraticCurveTo(
- leftControlX,
- leftControlY,
- leftAntennaEndX,
- leftAntennaEndY,
- );
- ctx.stroke();
-
- // 绘制右侧触角(使用二次贝塞尔曲线实现平滑弧度)
- ctx.beginPath();
- ctx.moveTo(rightAntennaStartX, rightAntennaStartY);
- // 使用二次贝塞尔曲线:起点、控制点、终点
- // 控制点在中间位置,使曲线平滑向右弯曲
- const rightControlX = rightAntennaStartX + antennaLength * 0.2;
- const rightControlY = rightAntennaMidY;
- ctx.quadraticCurveTo(
- rightControlX,
- rightControlY,
- rightAntennaEndX,
- rightAntennaEndY,
- );
- ctx.stroke();
-
- // 绘制触角末端的小圆点
- ctx.fillStyle = '#000';
- ctx.beginPath();
- ctx.arc(leftAntennaEndX, leftAntennaEndY, 2, 0, Math.PI * 2);
- ctx.fill();
- ctx.beginPath();
- ctx.arc(rightAntennaEndX, rightAntennaEndY, 2, 0, Math.PI * 2);
- ctx.fill();
-
- // 绘制眼睛
- const eyeSize = headRadius * 0.15;
- const eyeOffsetX = headRadius * 0.25;
- const eyeOffsetY = headRadius * 0.22;
- ctx.fillStyle = '#fff';
- ctx.beginPath();
- ctx.arc(
- headCenterX - eyeOffsetX - 2,
- headCenterY - eyeOffsetY,
- eyeSize,
- 0,
- Math.PI * 2,
- );
- ctx.fill();
- ctx.beginPath();
- ctx.arc(
- headCenterX + eyeOffsetX - 2,
- headCenterY - eyeOffsetY,
- eyeSize,
- 0,
- Math.PI * 2,
- );
- ctx.fill();
-
- // 绘制眼珠
- ctx.fillStyle = '#000';
- ctx.beginPath();
- ctx.arc(
- headCenterX - eyeOffsetX - 2,
- headCenterY - eyeOffsetY,
- eyeSize * 0.6,
- 0,
- Math.PI * 2,
- );
- ctx.fill();
- ctx.beginPath();
- ctx.arc(
- headCenterX + eyeOffsetX - 2,
- headCenterY - eyeOffsetY,
- eyeSize * 0.6,
- 0,
- Math.PI * 2,
- );
- ctx.fill();
-
- // 绘制嘴巴(微笑)
- ctx.strokeStyle = '#000';
- ctx.lineWidth = 1.5;
- ctx.beginPath();
- ctx.arc(
- headCenterX - 2,
- headCenterY + headRadius * 0.22,
- headRadius * 0.3,
- 0,
- Math.PI,
- );
- ctx.stroke();
-}
-
-/**
- * 绘制毛毛虫身体(带弧度的圆圈)
- */
-function drawCaterpillarBody(
- ctx: RenderingContext,
- startX: number,
- startY: number,
- count: number,
- bodyRadius: number,
- color: string,
-) {
- const spacing = bodyRadius * 2; // 身体圆圈之间的间距(稍微重叠)
- const waveAmplitude = bodyRadius * 0.4; // 波浪幅度
-
- for (let i = 0; i < count; i++) {
- // 计算每个身体圆圈的位置(带弧度,形成弯曲效果)
- const baseX = startX + i * spacing;
- // 使用正弦函数创建波浪效果,使身体有弧度
- const waveOffset = Math.sin((i * Math.PI) / 2.5) * waveAmplitude;
- const bodyX = baseX;
- const bodyY = startY + waveOffset;
-
- // 绘制身体圆圈
- ctx.fillStyle = color;
- ctx.beginPath();
- ctx.arc(bodyX, bodyY, bodyRadius, 0, Math.PI * 2);
- ctx.fill();
-
- // 绘制边框(可选,根据设计需求)
- ctx.strokeStyle = '#000';
- ctx.lineWidth = 1;
- ctx.stroke();
- }
-}
-
-/**
- * 绘制圆圈模式的内容
- */
-function drawCirclePattern(
- ctx: RenderingContext,
- x: number,
- y: number,
- width: number,
- height: number,
- number: number,
- numberColor: string,
- isFirstRow: boolean,
-) {
- const totalCircles = 10;
- const circleRadius = 16; // 固定半径
- const circleSpacing = 6; // 固定间距
-
- // 计算所有圆圈的总宽度
- const totalWidth =
- totalCircles * circleRadius * 2 + (totalCircles - 1) * circleSpacing;
-
- // 计算水平居中位置
- const startX = x + (width - totalWidth) / 2 + circleRadius;
-
- // 计算垂直居中位置
- const centerY = y + height / 2;
-
- // 绘制10个虚线圆圈(一行)
- for (let i = 0; i < totalCircles; i++) {
- const circleX = startX + i * (circleRadius * 2 + circleSpacing);
-
- // 绘制虚线圆圈
- ctx.strokeStyle = numberColor;
- ctx.lineWidth = 1;
- ctx.setLineDash([3, 3]);
- ctx.beginPath();
- ctx.arc(circleX, centerY, circleRadius, 0, Math.PI * 2);
- ctx.stroke();
- ctx.setLineDash([]);
-
- // 第一行:根据数字数量涂色
- if (isFirstRow && i < number) {
- ctx.fillStyle = numberColor;
- ctx.beginPath();
- ctx.arc(circleX, centerY, circleRadius - 1, 0, Math.PI * 2);
- ctx.fill();
- }
- }
-}
-
-/**
- * 绘制毛毛虫模式的内容
- */
-function drawCaterpillarPattern(
- ctx: RenderingContext,
- x: number,
- y: number,
- width: number,
- height: number,
- number: number,
- numberColor: string,
- isFirstRow: boolean,
-) {
- const padding = 15;
- const headSize = 45; // 头部区域高度(用于垂直居中)
- const headRadius = 18; // 固定头部半径
- const bodyRadius = 16; // 固定身体半径
-
- if (isFirstRow) {
- // 第一行:绘制完整的毛毛虫(头部+身体)
- const headX = x + padding;
- const headY = y + (height - headSize) / 2;
-
- // 绘制头部
- drawCaterpillarHead(ctx, headX, headY, headSize, numberColor);
-
- // 计算头部的实际宽度(头部半径 * 2)
- const headWidth = headRadius * 2; // 头部的实际宽度 = 36
-
- // 绘制身体(从头部右侧边缘开始,留一点间距)
- const bodyStartX = headX + headWidth + 17; // 头部右边缘 + 5px间距
- const bodyStartY = headY + headSize / 2; // 与头部中心Y对齐
- drawCaterpillarBody(
- ctx,
- bodyStartX,
- bodyStartY,
- number,
- bodyRadius,
- numberColor,
- );
- } else {
- // 其他行:只绘制头部
- const headX = x + padding;
- const headY = y + (height - headSize) / 2;
- drawCaterpillarHead(ctx, headX, headY, headSize, numberColor);
- }
-}
-
-/**
- * 绘制按数字涂颜色内容区域
- */
-export async function drawNumberColorContent({
- canvas,
- ctx,
- colorData,
- canvasWidth,
- startY,
- patternType = 'circle',
-}: DrawNumberColorContentParams): Promise {
- const { numbers } = colorData;
-
- const leftMargin = 24;
- const rightMargin = 24;
- const itemSpacing = 115; // 行间距
- const leftBoxWidth = 100;
- const rightBoxWidth = 420;
- const boxHeight = 80;
- const borderRadius = 12;
- const startYPos = startY + 20;
-
- // 获取所有可用颜色值
- const availableColors = getNumberColors();
-
- // 为每一行随机分配颜色,确保不重复
- const shuffledColors = [...availableColors];
- // Fisher-Yates 洗牌算法
- for (let i = shuffledColors.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [shuffledColors[i], shuffledColors[j]] = [
- shuffledColors[j],
- shuffledColors[i],
- ];
- }
-
- // 为每一行分配颜色(确保不重复)
- // 如果行数超过颜色数量,从剩余颜色中随机选择,但确保相邻行不同
- const rowColors: string[] = [];
-
- for (let i = 0; i < numbers.length; i++) {
- if (i < shuffledColors.length) {
- // 前 N 行(N <= 颜色数量)使用不同的颜色
- rowColors.push(shuffledColors[i]);
- } else {
- // 如果行数超过颜色数量,从剩余颜色中选择(确保与上一行不同)
- const remainingColors = shuffledColors.filter(
- (color) => color !== rowColors[i - 1],
- );
- const randomColor =
- remainingColors[
- Math.floor(Math.random() * remainingColors.length)
- ];
- rowColors.push(randomColor);
- }
- }
-
- // 计算左侧和右侧的起始X位置
- const leftBoxX = leftMargin;
- const rightBoxX = canvasWidth - rightMargin - rightBoxWidth;
-
- // 绘制每一行
- for (let i = 0; i < numbers.length; i++) {
- const boxY = startYPos + i * itemSpacing;
- const number = numbers[i];
- const numberColor = rowColors[i]; // 使用随机分配的颜色
- const isFirstRow = i === 0;
-
- // 绘制左侧数字框(虚线)
- ctx.strokeStyle = '#999';
- ctx.lineWidth = 1;
- drawRoundedRect(
- ctx,
- leftBoxX,
- boxY,
- leftBoxWidth,
- boxHeight,
- borderRadius,
- true,
- );
-
- // 绘制数字(带颜色)
- ctx.fillStyle = numberColor;
- ctx.font = `bold ${56}px "Microsoft Yahei"`;
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText(
- String(number),
- leftBoxX + leftBoxWidth / 2,
- boxY + boxHeight / 2,
- );
-
- // 绘制右侧内容框(虚线)
- ctx.strokeStyle = '#999';
- ctx.lineWidth = 1;
- drawRoundedRect(
- ctx,
- rightBoxX,
- boxY,
- rightBoxWidth,
- boxHeight,
- borderRadius,
- true,
- );
-
- // 根据模式绘制右侧内容
- if (patternType === 'circle') {
- drawCirclePattern(
- ctx,
- rightBoxX,
- boxY,
- rightBoxWidth,
- boxHeight,
- number,
- numberColor,
- isFirstRow,
- );
- } else if (patternType === 'caterpillar') {
- drawCaterpillarPattern(
- ctx,
- rightBoxX,
- boxY,
- rightBoxWidth,
- boxHeight,
- number,
- numberColor,
- isFirstRow,
- );
- }
- }
-}
diff --git a/miniprogram/mathPages/shared/service/numberColorDraw.ts b/miniprogram/mathPages/shared/service/numberColorDraw.ts
index 01d0021..f7b05b3 100644
--- a/miniprogram/mathPages/shared/service/numberColorDraw.ts
+++ b/miniprogram/mathPages/shared/service/numberColorDraw.ts
@@ -1,14 +1,22 @@
+/**
+ * 按数字涂颜色绘制服务
+ */
+
import { BaseDrawService } from '../../../service/baseDraw';
-import { drawNumberColorContent } from './numberColorContentDraw';
+import { getNumberColors } from '../../../constants/colors';
+
+/**
+ * 按数字涂颜色数据
+ */
+export interface NumberColorData {
+ numbers: number[];
+}
/**
* 按数字涂颜色绘制服务
- * 组合使用基础绘制服务和内容区域绘制服务
*/
class NumberColorDraw extends BaseDrawService {
- colorData: {
- numbers: number[];
- } | null;
+ colorData: NumberColorData | null = null;
constructor(
canvas: Canvas,
@@ -19,29 +27,21 @@ class NumberColorDraw extends BaseDrawService {
this.colorData = null;
}
+ /**
+ * 绘制按数字涂颜色内容
+ */
async draw(
- colorData: { numbers: number[] },
+ colorData: NumberColorData,
patternType: string = 'circle', // 'circle' 或 'caterpillar'
) {
if (!colorData || !colorData.numbers) {
return;
}
- this.setPrintConfig();
this.colorData = colorData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域(按数字涂颜色)
- this.drawDivider();
- await drawNumberColorContent({
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
+ await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
colorData: this.colorData,
@@ -50,6 +50,466 @@ class NumberColorDraw extends BaseDrawService {
patternType,
});
}
+
+ /**
+ * 绘制内容区域
+ */
+ private async drawContent(params: {
+ canvas: WechatMiniprogram.Canvas;
+ ctx: RenderingContext;
+ colorData: NumberColorData;
+ canvasWidth: number;
+ startY: number;
+ patternType?: string; // 'circle' 或 'caterpillar'
+ }): Promise {
+ const {
+ ctx,
+ colorData,
+ canvasWidth,
+ startY,
+ patternType = 'circle',
+ } = params;
+ const { numbers } = colorData;
+
+ const leftMargin = 24;
+ const rightMargin = 24;
+ const itemSpacing = 115; // 行间距
+ const leftBoxWidth = 100;
+ const rightBoxWidth = 420;
+ const boxHeight = 80;
+ const borderRadius = 12;
+ const startYPos = startY + 20;
+
+ // 获取所有可用颜色值
+ const availableColors = getNumberColors();
+
+ // 为每一行随机分配颜色,确保不重复
+ const shuffledColors = [...availableColors];
+ // Fisher-Yates 洗牌算法
+ for (let i = shuffledColors.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [shuffledColors[i], shuffledColors[j]] = [
+ shuffledColors[j],
+ shuffledColors[i],
+ ];
+ }
+
+ // 为每一行分配颜色(确保不重复)
+ // 如果行数超过颜色数量,从剩余颜色中随机选择,但确保相邻行不同
+ const rowColors: string[] = [];
+
+ for (let i = 0; i < numbers.length; i++) {
+ if (i < shuffledColors.length) {
+ // 前 N 行(N <= 颜色数量)使用不同的颜色
+ rowColors.push(shuffledColors[i]);
+ } else {
+ // 如果行数超过颜色数量,从剩余颜色中选择(确保与上一行不同)
+ const remainingColors = shuffledColors.filter(
+ (color) => color !== rowColors[i - 1],
+ );
+ const randomColor =
+ remainingColors[
+ Math.floor(Math.random() * remainingColors.length)
+ ];
+ rowColors.push(randomColor);
+ }
+ }
+
+ // 计算左侧和右侧的起始X位置
+ const leftBoxX = leftMargin;
+ const rightBoxX = canvasWidth - rightMargin - rightBoxWidth;
+
+ // 绘制每一行
+ for (let i = 0; i < numbers.length; i++) {
+ const boxY = startYPos + i * itemSpacing;
+ const number = numbers[i];
+ const numberColor = rowColors[i]; // 使用随机分配的颜色
+ const isFirstRow = i === 0;
+
+ // 绘制左侧数字框(虚线)
+ ctx.strokeStyle = '#999';
+ ctx.lineWidth = 1;
+ this.drawRoundedRectInternal(
+ ctx,
+ leftBoxX,
+ boxY,
+ leftBoxWidth,
+ boxHeight,
+ borderRadius,
+ true,
+ );
+
+ // 绘制数字(带颜色)
+ ctx.fillStyle = numberColor;
+ ctx.font = `bold ${56}px "Microsoft Yahei"`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText(
+ String(number),
+ leftBoxX + leftBoxWidth / 2,
+ boxY + boxHeight / 2,
+ );
+
+ // 绘制右侧内容框(虚线)
+ ctx.strokeStyle = '#999';
+ ctx.lineWidth = 1;
+ this.drawRoundedRectInternal(
+ ctx,
+ rightBoxX,
+ boxY,
+ rightBoxWidth,
+ boxHeight,
+ borderRadius,
+ true,
+ );
+
+ // 根据模式绘制右侧内容
+ if (patternType === 'circle') {
+ this.drawCirclePattern(
+ ctx,
+ rightBoxX,
+ boxY,
+ rightBoxWidth,
+ boxHeight,
+ number,
+ numberColor,
+ isFirstRow,
+ );
+ } else if (patternType === 'caterpillar') {
+ this.drawCaterpillarPattern(
+ ctx,
+ rightBoxX,
+ boxY,
+ rightBoxWidth,
+ boxHeight,
+ number,
+ numberColor,
+ isFirstRow,
+ );
+ }
+ }
+ }
+
+ /**
+ * 绘制圆圈模式的内容
+ */
+ private drawCirclePattern(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ number: number,
+ numberColor: string,
+ isFirstRow: boolean,
+ ) {
+ const totalCircles = 10;
+ const circleRadius = 16; // 固定半径
+ const circleSpacing = 6; // 固定间距
+
+ // 计算所有圆圈的总宽度
+ const totalWidth =
+ totalCircles * circleRadius * 2 +
+ (totalCircles - 1) * circleSpacing;
+
+ // 计算水平居中位置
+ const startX = x + (width - totalWidth) / 2 + circleRadius;
+
+ // 计算垂直居中位置
+ const centerY = y + height / 2;
+
+ // 绘制10个虚线圆圈(一行)
+ for (let i = 0; i < totalCircles; i++) {
+ const circleX = startX + i * (circleRadius * 2 + circleSpacing);
+
+ // 绘制虚线圆圈
+ ctx.strokeStyle = numberColor;
+ ctx.lineWidth = 1;
+ ctx.setLineDash([3, 3]);
+ ctx.beginPath();
+ ctx.arc(circleX, centerY, circleRadius, 0, Math.PI * 2);
+ ctx.stroke();
+ ctx.setLineDash([]);
+
+ // 第一行:根据数字数量涂色
+ if (isFirstRow && i < number) {
+ ctx.fillStyle = numberColor;
+ ctx.beginPath();
+ ctx.arc(circleX, centerY, circleRadius - 1, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+ }
+
+ /**
+ * 绘制毛毛虫模式的内容
+ */
+ private drawCaterpillarPattern(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ number: number,
+ numberColor: string,
+ isFirstRow: boolean,
+ ) {
+ const padding = 15;
+ const headSize = 45; // 头部区域高度(用于垂直居中)
+ const headRadius = 18; // 固定头部半径
+ const bodyRadius = 16; // 固定身体半径
+
+ if (isFirstRow) {
+ // 第一行:绘制完整的毛毛虫(头部+身体)
+ const headX = x + padding;
+ const headY = y + (height - headSize) / 2;
+
+ // 绘制头部
+ this.drawCaterpillarHead(ctx, headX, headY, headSize, numberColor);
+
+ // 计算头部的实际宽度(头部半径 * 2)
+ const headWidth = headRadius * 2; // 头部的实际宽度 = 36
+
+ // 绘制身体(从头部右侧边缘开始,留一点间距)
+ const bodyStartX = headX + headWidth + 17; // 头部右边缘 + 5px间距
+ const bodyStartY = headY + headSize / 2; // 与头部中心Y对齐
+ this.drawCaterpillarBody(
+ ctx,
+ bodyStartX,
+ bodyStartY,
+ number,
+ bodyRadius,
+ numberColor,
+ );
+ } else {
+ // 其他行:只绘制头部
+ const headX = x + padding;
+ const headY = y + (height - headSize) / 2;
+ this.drawCaterpillarHead(ctx, headX, headY, headSize, numberColor);
+ }
+ }
+
+ /**
+ * 绘制毛毛虫头部
+ */
+ private drawCaterpillarHead(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ size: number,
+ color: string,
+ ) {
+ const headRadius = 18; // 固定头部半径
+ const headCenterX = x + headRadius;
+ const headCenterY = y + size / 2;
+
+ // 绘制头部(圆形)
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2);
+ ctx.fill();
+
+ // 绘制头部圆圈实线边框
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 1;
+ ctx.setLineDash([]); // 确保是实线
+ ctx.beginPath();
+ ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2);
+ ctx.stroke();
+
+ // 绘制触角(在头部圆圈的正上方,带弧度的弯曲)
+ const antennaLength = headRadius * 0.6;
+ const antennaStartY = headCenterY - headRadius; // 头部圆圈的正上方
+ const antennaOffset = headRadius * 0.3; // 触角左右偏移距离
+
+ // 左侧触角:从顶部左侧开始,先向上,然后向左弯曲(带弧度)
+ const leftAntennaStartX = headCenterX - antennaOffset;
+ const leftAntennaStartY = antennaStartY;
+ const leftAntennaMidY = leftAntennaStartY - antennaLength * 0.5; // 向上延伸的中点
+ const leftAntennaEndX = leftAntennaStartX - antennaLength * 0.4; // 向左弯曲
+ const leftAntennaEndY = leftAntennaMidY - antennaLength * 0.3; // 向上并向左
+
+ // 右侧触角:从顶部右侧开始,先向上,然后向右弯曲(带弧度)
+ const rightAntennaStartX = headCenterX + antennaOffset;
+ const rightAntennaStartY = antennaStartY;
+ const rightAntennaMidY = rightAntennaStartY - antennaLength * 0.5; // 向上延伸的中点
+ const rightAntennaEndX = rightAntennaStartX + antennaLength * 0.4; // 向右弯曲
+ const rightAntennaEndY = rightAntennaMidY - antennaLength * 0.3; // 向上并向右
+
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 1.5;
+ ctx.lineCap = 'round'; // 圆角端点
+
+ // 绘制左侧触角(使用二次贝塞尔曲线实现平滑弧度)
+ ctx.beginPath();
+ ctx.moveTo(leftAntennaStartX, leftAntennaStartY);
+ // 使用二次贝塞尔曲线:起点、控制点、终点
+ // 控制点在中间位置,使曲线平滑向左弯曲
+ const leftControlX = leftAntennaStartX - antennaLength * 0.2;
+ const leftControlY = leftAntennaMidY;
+ ctx.quadraticCurveTo(
+ leftControlX,
+ leftControlY,
+ leftAntennaEndX,
+ leftAntennaEndY,
+ );
+ ctx.stroke();
+
+ // 绘制右侧触角(使用二次贝塞尔曲线实现平滑弧度)
+ ctx.beginPath();
+ ctx.moveTo(rightAntennaStartX, rightAntennaStartY);
+ // 使用二次贝塞尔曲线:起点、控制点、终点
+ // 控制点在中间位置,使曲线平滑向右弯曲
+ const rightControlX = rightAntennaStartX + antennaLength * 0.2;
+ const rightControlY = rightAntennaMidY;
+ ctx.quadraticCurveTo(
+ rightControlX,
+ rightControlY,
+ rightAntennaEndX,
+ rightAntennaEndY,
+ );
+ ctx.stroke();
+
+ // 绘制触角末端的小圆点
+ ctx.fillStyle = '#000';
+ ctx.beginPath();
+ ctx.arc(leftAntennaEndX, leftAntennaEndY, 2, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.beginPath();
+ ctx.arc(rightAntennaEndX, rightAntennaEndY, 2, 0, Math.PI * 2);
+ ctx.fill();
+
+ // 绘制眼睛
+ const eyeSize = headRadius * 0.15;
+ const eyeOffsetX = headRadius * 0.25;
+ const eyeOffsetY = headRadius * 0.22;
+ ctx.fillStyle = '#fff';
+ ctx.beginPath();
+ ctx.arc(
+ headCenterX - eyeOffsetX - 2,
+ headCenterY - eyeOffsetY,
+ eyeSize,
+ 0,
+ Math.PI * 2,
+ );
+ ctx.fill();
+ ctx.beginPath();
+ ctx.arc(
+ headCenterX + eyeOffsetX - 2,
+ headCenterY - eyeOffsetY,
+ eyeSize,
+ 0,
+ Math.PI * 2,
+ );
+ ctx.fill();
+
+ // 绘制眼珠
+ ctx.fillStyle = '#000';
+ ctx.beginPath();
+ ctx.arc(
+ headCenterX - eyeOffsetX - 2,
+ headCenterY - eyeOffsetY,
+ eyeSize * 0.6,
+ 0,
+ Math.PI * 2,
+ );
+ ctx.fill();
+ ctx.beginPath();
+ ctx.arc(
+ headCenterX + eyeOffsetX - 2,
+ headCenterY - eyeOffsetY,
+ eyeSize * 0.6,
+ 0,
+ Math.PI * 2,
+ );
+ ctx.fill();
+
+ // 绘制嘴巴(微笑)
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 1.5;
+ ctx.beginPath();
+ ctx.arc(
+ headCenterX - 2,
+ headCenterY + headRadius * 0.22,
+ headRadius * 0.3,
+ 0,
+ Math.PI,
+ );
+ ctx.stroke();
+ }
+
+ /**
+ * 绘制毛毛虫身体(带弧度的圆圈)
+ */
+ private drawCaterpillarBody(
+ ctx: RenderingContext,
+ startX: number,
+ startY: number,
+ count: number,
+ bodyRadius: number,
+ color: string,
+ ) {
+ const spacing = bodyRadius * 2; // 身体圆圈之间的间距(稍微重叠)
+ const waveAmplitude = bodyRadius * 0.4; // 波浪幅度
+
+ for (let i = 0; i < count; i++) {
+ // 计算每个身体圆圈的位置(带弧度,形成弯曲效果)
+ const baseX = startX + i * spacing;
+ // 使用正弦函数创建波浪效果,使身体有弧度
+ const waveOffset = Math.sin((i * Math.PI) / 2.5) * waveAmplitude;
+ const bodyX = baseX;
+ const bodyY = startY + waveOffset;
+
+ // 绘制身体圆圈
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(bodyX, bodyY, bodyRadius, 0, Math.PI * 2);
+ ctx.fill();
+
+ // 绘制边框(可选,根据设计需求)
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ }
+ }
+
+ /**
+ * 绘制圆角矩形(虚线边框)
+ */
+ private drawRoundedRectInternal(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ radius: number,
+ isDashed: boolean = true,
+ ) {
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.quadraticCurveTo(
+ x + width,
+ y + height,
+ x + width - radius,
+ y + height,
+ );
+ ctx.lineTo(x + radius, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+
+ if (isDashed) {
+ ctx.setLineDash([6, 6]); // 虚线
+ } else {
+ ctx.setLineDash([]);
+ }
+ ctx.stroke();
+ ctx.setLineDash([]); // 重置为实线
+ }
}
export default NumberColorDraw;
diff --git a/miniprogram/mathPages/shared/service/numberContentDraw.ts b/miniprogram/mathPages/shared/service/numberContentDraw.ts
deleted file mode 100644
index f188706..0000000
--- a/miniprogram/mathPages/shared/service/numberContentDraw.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * 绘制内容区域的参数接口
- */
-interface DrawNumberContentParams {
- ctx: RenderingContext;
- selectedNumber: number;
- canvasWidth: number; // 逻辑像素宽度(已除以3)
- startY: number; // 起始Y坐标
-}
-
-/**
- * 绘制数字涂色内容区域服务
- * 随机绘制1-10的数字,尺寸除以3
- */
-export function drawNumberContent({
- ctx,
- selectedNumber,
- canvasWidth,
- startY,
-}: DrawNumberContentParams): void {
- const rows = 6;
- const radius = 26; // 约26.67
- const fontSize = 24; // 24
-
- const startYPos = startY + 10 + radius; // 约47.33
- const startX1 = 100 + radius; // 约112.22
- const startX2 = 60 + radius; // 约75.56
- const spaceWidthFirst = (canvasWidth - startX1 * 2) / (5 - 1);
- const spaceWidthSecond = (canvasWidth - startX2 * 2) / (6 - 1);
- const spaceHeight = 18; // 18
-
- ctx.moveTo(startX1, startYPos);
- for (let i = 0; i < rows; i++) {
- const cols = i % 2 === 0 ? 5 : 6;
-
- for (let j = 0; j < cols; j++) {
- const y = startYPos + i * (spaceHeight + radius * 2);
- const x =
- i % 2 === 0
- ? startX1 + j * spaceWidthFirst
- : startX2 + j * spaceWidthSecond;
-
- // 随机生成1-10的数字,但确保包含选中的数字
- let number: number;
- if (i === 0 && j === 0) {
- // 第一个位置固定为选中的数字
- number = selectedNumber;
- } else {
- // 其他位置随机,但增加选中数字出现的概率
- const random = Math.random();
- if (random < 0.3) {
- // 30%概率是选中的数字
- number = selectedNumber;
- } else {
- // 70%概率是其他随机数字
- number = Math.floor(Math.random() * 10) + 1;
- }
- }
-
- // 绘制圆形背景(线宽除以3)
- ctx.fillStyle = '#fff';
- ctx.strokeStyle = '#000';
- ctx.lineWidth = 1; // 约1.33
- ctx.beginPath();
- ctx.arc(x, y, radius, 0, Math.PI * 2);
- ctx.fill();
- ctx.stroke();
- ctx.closePath();
-
- // 绘制数字(字体大小除以3)
- ctx.fillStyle = '#333';
- ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
- ctx.textBaseline = 'middle';
- ctx.textAlign = 'center';
- ctx.fillText(String(number), x, y, radius * 2);
- }
- }
-}
diff --git a/miniprogram/mathPages/shared/service/numberDecomposeContentDraw.ts b/miniprogram/mathPages/shared/service/numberDecomposeContentDraw.ts
deleted file mode 100644
index 157fac1..0000000
--- a/miniprogram/mathPages/shared/service/numberDecomposeContentDraw.ts
+++ /dev/null
@@ -1,504 +0,0 @@
-import { getImage } from '../../../utils/index';
-import { getRandomNumberColor } from '../../../constants/colors';
-
-interface DrawNumberDecomposeContentParams {
- canvas: WechatMiniprogram.Canvas;
- ctx: RenderingContext;
- decomposeData: {
- problems: Array<{
- whole: number | null;
- part1: number | null;
- part2: number | null;
- imageIndex?: number;
- imageType?: 'fruits' | 'twelve-animals';
- }>;
- mode: 'with-image' | 'decompose' | 'compose';
- };
- canvasWidth: number;
- startY: number;
-}
-
-/**
- * 绘制圆角矩形(虚线边框)
- */
-function drawRoundedRect(
- ctx: RenderingContext,
- x: number,
- y: number,
- width: number,
- height: number,
- radius: number,
- isDashed: boolean = false,
-) {
- ctx.beginPath();
- ctx.moveTo(x + radius, y);
- ctx.lineTo(x + width - radius, y);
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
- ctx.lineTo(x + width, y + height - radius);
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
- ctx.lineTo(x + radius, y + height);
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
- ctx.lineTo(x, y + radius);
- ctx.quadraticCurveTo(x, y, x + radius, y);
- ctx.closePath();
-
- if (isDashed) {
- ctx.setLineDash([6 / 3, 6 / 3]); // 约2px的虚线
- } else {
- ctx.setLineDash([]);
- }
- ctx.stroke();
- ctx.setLineDash([]); // 重置为实线
-}
-
-/**
- * 绘制数的分与合内容区域
- */
-export async function drawNumberDecomposeContent({
- canvas,
- ctx,
- decomposeData,
- canvasWidth,
- startY,
-}: DrawNumberDecomposeContentParams): Promise {
- const { problems, mode } = decomposeData;
-
- if (mode === 'with-image') {
- await drawWithImageMode(canvas, ctx, problems, canvasWidth, startY);
- } else if (mode === 'decompose') {
- await drawDecomposeOrComposeMode(
- ctx,
- problems,
- canvasWidth,
- startY,
- false,
- );
- } else if (mode === 'compose') {
- await drawDecomposeOrComposeMode(
- ctx,
- problems,
- canvasWidth,
- startY,
- true,
- );
- }
-}
-
-/**
- * 有图片模式:一行三列,总共三行(9个题目)
- */
-async function drawWithImageMode(
- canvas: WechatMiniprogram.Canvas,
- ctx: RenderingContext,
- problems: Array<{
- whole: number | null;
- part1: number | null;
- part2: number | null;
- imageIndex?: number;
- imageType?: 'fruits' | 'twelve-animals';
- }>,
- canvasWidth: number,
- startY: number,
-) {
- const leftMargin = 30;
- const rightMargin = 30;
- const topMargin = 20;
- const boxSpacing = 20;
- const rowSpacing = 32;
-
- // 计算每个框的尺寸
- const availableWidth = canvasWidth - leftMargin - rightMargin;
- const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
- const imageAreaHeight = 120; // 图片区域高度
- const treeAreaHeight = 80; // 二叉树区域高度
-
- let currentY = startY + topMargin;
-
- // 绘制3列3行
- for (let row = 0; row < 3; row++) {
- for (let col = 0; col < 3; col++) {
- const problemIndex = row * 3 + col;
- if (problemIndex >= problems.length) {
- continue;
- }
-
- const problem = problems[problemIndex];
- const boxX = leftMargin + col * (boxWidth + boxSpacing);
- const boxY =
- currentY +
- row * (imageAreaHeight + treeAreaHeight + rowSpacing);
-
- // 绘制一个完整的题目框
- await drawWithImageProblem(
- ctx,
- canvas,
- problem,
- boxX,
- boxY,
- boxWidth,
- imageAreaHeight,
- treeAreaHeight,
- );
- }
- }
-}
-
-/**
- * 分模式/组合模式:一行3个,总共5行(15个题目)
- * @param isInverted true 为组合模式(倒置二叉树),false 为分模式(正常二叉树)
- */
-async function drawDecomposeOrComposeMode(
- ctx: RenderingContext,
- problems: Array<{
- whole: number | null;
- part1: number | null;
- part2: number | null;
- }>,
- canvasWidth: number,
- startY: number,
- isInverted: boolean,
-) {
- const leftMargin = 30;
- const rightMargin = 30;
- const topMargin = 20;
- const boxSpacing = 15;
- const rowSpacing = 140;
-
- // 计算每个框的尺寸
- const availableWidth = canvasWidth - leftMargin - rightMargin;
- const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
- const boxHeight = 80; // 二叉树区域高度
-
- let currentY = startY + topMargin;
-
- // 绘制3列5行
- for (let row = 0; row < 5; row++) {
- for (let col = 0; col < 3; col++) {
- const problemIndex = row * 3 + col;
- if (problemIndex >= problems.length) {
- continue;
- }
-
- const problem = problems[problemIndex];
- const boxX = leftMargin + col * (boxWidth + boxSpacing);
- const boxY = currentY + row * rowSpacing;
-
- // 绘制二叉树
- drawTree(ctx, problem, boxX, boxY, boxWidth, boxHeight, isInverted);
- }
- }
-}
-
-/**
- * 绘制有图片模式的单个题目
- */
-async function drawWithImageProblem(
- ctx: RenderingContext,
- canvas: WechatMiniprogram.Canvas,
- problem: {
- whole: number | null;
- part1: number | null;
- part2: number | null;
- imageIndex?: number;
- imageType?: 'fruits' | 'twelve-animals';
- },
- boxX: number,
- boxY: number,
- boxWidth: number,
- imageAreaHeight: number,
- treeAreaHeight: number,
-) {
- const borderRadius = 12;
-
- // 绘制实线框(只绘制图片区域的框)
- ctx.strokeStyle = '#333';
- ctx.lineWidth = 1;
- drawRoundedRect(
- ctx,
- boxX,
- boxY,
- boxWidth,
- imageAreaHeight,
- borderRadius,
- false,
- );
-
- // 图片区域(增加底部间距,避免与二叉树根节点重叠)
- const imageBottomPadding = 20; // 图片区域底部额外间距
- const effectiveImageHeight = imageAreaHeight - imageBottomPadding;
-
- if (problem.imageIndex && problem.imageType && problem.whole) {
- await drawImagesInBox(
- ctx,
- canvas,
- problem.whole,
- problem.imageIndex,
- problem.imageType,
- boxX,
- boxY,
- boxWidth,
- effectiveImageHeight,
- );
- }
-
- // 二叉树区域(增加与图片区域的间距)
- const treeTopSpacing = -5; // 二叉树区域顶部间距
- const treeY = boxY + imageAreaHeight + treeTopSpacing;
- drawTree(
- ctx,
- problem,
- boxX,
- treeY,
- boxWidth,
- treeAreaHeight,
- false, // 不是倒置
- );
-}
-
-/**
- * 绘制二叉树结构
- * @param isInverted 是否倒置(组合模式为true)
- */
-function drawTree(
- ctx: RenderingContext,
- problem: {
- whole: number | null;
- part1: number | null;
- part2: number | null;
- },
- boxX: number,
- boxY: number,
- boxWidth: number,
- boxHeight: number,
- isInverted: boolean,
-) {
- const nodeRadius = 18;
- const nodeSpacing = 40; // 节点之间的水平间距
- const verticalSpacing = 70; // 根节点和子节点的垂直间距
-
- if (isInverted) {
- // 组合模式:倒置二叉树
- // 顶部:两个子节点(part1, part2)
- // 底部:根节点(whole,需要填写)
-
- const centerX = boxX + boxWidth / 2;
- const topY = boxY + boxHeight / 2 - verticalSpacing / 2;
- const bottomY = boxY + boxHeight / 2 + verticalSpacing / 2;
-
- // 绘制两个子节点(顶部)
- const leftNodeX = centerX - nodeSpacing / 2 - nodeRadius;
- const rightNodeX = centerX + nodeSpacing / 2 + nodeRadius;
-
- // 左子节点
- drawNode(ctx, leftNodeX, topY, nodeRadius, problem.part1);
- // 右子节点
- drawNode(ctx, rightNodeX, topY, nodeRadius, problem.part2);
-
- // 绘制根节点(底部)
- drawNode(ctx, centerX, bottomY, nodeRadius, problem.whole);
-
- // 绘制连接线(从子节点到根节点)
- ctx.strokeStyle = '#333';
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(leftNodeX, topY + nodeRadius);
- ctx.lineTo(centerX, bottomY - nodeRadius);
- ctx.stroke();
-
- ctx.beginPath();
- ctx.moveTo(rightNodeX, topY + nodeRadius);
- ctx.lineTo(centerX, bottomY - nodeRadius);
- ctx.stroke();
- } else {
- // 分解模式:正常二叉树
- // 顶部:根节点(whole)
- // 底部:两个子节点(part1, part2,其中一个需要填写)
-
- const centerX = boxX + boxWidth / 2;
- const topY = boxY + boxHeight / 2 - verticalSpacing / 2;
- const bottomY = boxY + boxHeight / 2 + verticalSpacing / 2;
-
- // 绘制根节点(顶部)
- drawNode(ctx, centerX, topY, nodeRadius, problem.whole);
-
- // 绘制两个子节点(底部)
- const leftNodeX = centerX - nodeSpacing / 2 - nodeRadius;
- const rightNodeX = centerX + nodeSpacing / 2 + nodeRadius;
-
- // 左子节点
- drawNode(ctx, leftNodeX, bottomY, nodeRadius, problem.part1);
- // 右子节点
- drawNode(ctx, rightNodeX, bottomY, nodeRadius, problem.part2);
-
- // 绘制连接线(从根节点到子节点)
- ctx.strokeStyle = '#333';
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(centerX, topY + nodeRadius);
- ctx.lineTo(leftNodeX, bottomY - nodeRadius);
- ctx.stroke();
-
- ctx.beginPath();
- ctx.moveTo(centerX, topY + nodeRadius);
- ctx.lineTo(rightNodeX, bottomY - nodeRadius);
- ctx.stroke();
- }
-}
-
-/**
- * 绘制节点(圆圈和数字)
- */
-function drawNode(
- ctx: RenderingContext,
- x: number,
- y: number,
- radius: number,
- value: number | null,
-) {
- // 绘制圆角正方形
- const size = radius * 2; // 正方形边长
- const cornerRadius = 8; // 圆角半径
- const halfSize = size / 2;
-
- ctx.fillStyle = '#fff';
- ctx.strokeStyle = '#333';
- ctx.lineWidth = 2;
-
- // 绘制圆角矩形
- ctx.beginPath();
- ctx.moveTo(x - halfSize + cornerRadius, y - halfSize);
- ctx.lineTo(x + halfSize - cornerRadius, y - halfSize);
- ctx.quadraticCurveTo(
- x + halfSize,
- y - halfSize,
- x + halfSize,
- y - halfSize + cornerRadius,
- );
- ctx.lineTo(x + halfSize, y + halfSize - cornerRadius);
- ctx.quadraticCurveTo(
- x + halfSize,
- y + halfSize,
- x + halfSize - cornerRadius,
- y + halfSize,
- );
- ctx.lineTo(x - halfSize + cornerRadius, y + halfSize);
- ctx.quadraticCurveTo(
- x - halfSize,
- y + halfSize,
- x - halfSize,
- y + halfSize - cornerRadius,
- );
- ctx.lineTo(x - halfSize, y - halfSize + cornerRadius);
- ctx.quadraticCurveTo(
- x - halfSize,
- y - halfSize,
- x - halfSize + cornerRadius,
- y - halfSize,
- );
- ctx.closePath();
- ctx.fill();
- ctx.stroke();
-
- // 如果有值,绘制数字
- if (value !== null) {
- ctx.fillStyle = getRandomNumberColor();
- ctx.font = `bold ${24}px "Microsoft Yahei"`;
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText(String(value), x, y);
- }
-}
-
-/**
- * 在框内绘制多张图片
- */
-async function drawImagesInBox(
- ctx: RenderingContext,
- canvas: WechatMiniprogram.Canvas,
- count: number,
- imageIndex: number,
- imageType: 'fruits' | 'twelve-animals',
- boxX: number,
- boxY: number,
- boxWidth: number,
- boxHeight: number,
-) {
- // 图片配置
- const imageConfig = {
- 'twelve-animals': {
- folder: 'twelve-animals',
- maxIndex: 12,
- },
- fruits: {
- folder: 'fruits',
- maxIndex: 22,
- },
- };
-
- const config = imageConfig[imageType] || imageConfig['twelve-animals'];
-
- const horizontalPadding = 5;
- const verticalPadding = 10;
- const availableWidth = boxWidth - horizontalPadding * 2;
- const availableHeight = boxHeight - verticalPadding * 2;
-
- // 根据数量确定每行的图片数和图片大小
- let imagesPerRow: number;
- let imageSize: number;
-
- if (count <= 4) {
- imagesPerRow = count <= 2 ? count : 2;
- imageSize =
- Math.min(availableWidth / imagesPerRow, availableHeight / 2) - 4;
- } else if (count <= 6) {
- imagesPerRow = 3;
- imageSize = Math.min(availableWidth / 3, availableHeight / 2) - 4;
- } else {
- imagesPerRow = 3;
- imageSize = Math.min(availableWidth / 3, availableHeight / 3) - 4;
- }
-
- const rows = Math.ceil(count / imagesPerRow);
- const imageSpacing =
- (availableWidth - imageSize * imagesPerRow) / (imagesPerRow + 1);
- const rowSpacing =
- rows > 1 ? (availableHeight - imageSize * rows) / (rows + 1) : 0;
-
- // 加载图片
- let boxImage: any = null;
- try {
- const imagePath = `/mathPages/assets/${config.folder}/${imageIndex}.png`;
- boxImage = await getImage(canvas, imagePath);
- } catch (error) {
- console.error(`加载${config.folder}/${imageIndex}图片失败:`, error);
- return;
- }
-
- // 绘制图片
- for (let i = 0; i < count; i++) {
- const row = Math.floor(i / imagesPerRow);
- const col = i % imagesPerRow;
-
- const imageX =
- boxX +
- horizontalPadding +
- imageSpacing +
- col * (imageSize + imageSpacing);
- const imageY =
- boxY +
- verticalPadding +
- // 这两段代码是用于计算每一张图片在Y轴方向上的实际位置(imageY 变量)
- // 第一行:如果有多于一行,行间用 rowSpacing,否则单行时图片整体垂直居中排列。
- (rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) +
- // 第二行:加上具体第 row 行的纵向偏移(每张图片的高度加上行间距,再乘以行号)。
- row * (imageSize + (rows > 1 ? rowSpacing : 0));
-
- if (boxImage) {
- // 计算图片高度(等比例缩放)
- // @ts-ignore - 微信小程序图片对象有 width 和 height 属性
- const scaledHeight = (boxImage.height / boxImage.width) * imageSize;
-
- ctx.drawImage(boxImage, imageX, imageY, imageSize, scaledHeight);
- }
- }
-}
diff --git a/miniprogram/mathPages/shared/service/numberDecomposeDraw.ts b/miniprogram/mathPages/shared/service/numberDecomposeDraw.ts
index 6fc86dc..1e715d6 100644
--- a/miniprogram/mathPages/shared/service/numberDecomposeDraw.ts
+++ b/miniprogram/mathPages/shared/service/numberDecomposeDraw.ts
@@ -1,21 +1,30 @@
+/**
+ * 10以内数的分与合绘制服务
+ */
+
import { BaseDrawService } from '../../../service/baseDraw';
-import { drawNumberDecomposeContent } from './numberDecomposeContentDraw';
+import { getImage } from '../../../utils/index';
+import { getRandomNumberColor } from '../../../constants/colors';
+
+/**
+ * 10以内数的分与合数据
+ */
+export interface NumberDecomposeData {
+ problems: Array<{
+ whole: number | null;
+ part1: number | null;
+ part2: number | null;
+ imageIndex?: number;
+ imageType?: 'fruits' | 'twelve-animals';
+ }>;
+ mode: 'with-image' | 'decompose' | 'compose';
+}
/**
* 10以内数的分与合绘制服务
- * 组合使用基础绘制服务和内容区域绘制服务
*/
class NumberDecomposeDraw extends BaseDrawService {
- decomposeData: {
- problems: Array<{
- whole: number | null;
- part1: number | null;
- part2: number | null;
- imageIndex?: number;
- imageType?: 'fruits' | 'twelve-animals';
- }>;
- mode: 'with-image' | 'decompose' | 'compose';
- } | null;
+ decomposeData: NumberDecomposeData | null = null;
constructor(
canvas: Canvas,
@@ -26,35 +35,18 @@ class NumberDecomposeDraw extends BaseDrawService {
this.decomposeData = null;
}
- async draw(decomposeData: {
- problems: Array<{
- whole: number | null;
- part1: number | null;
- part2: number | null;
- imageIndex?: number;
- imageType?: 'fruits' | 'twelve-animals';
- }>;
- mode: 'with-image' | 'decompose' | 'compose';
- }) {
+ /**
+ * 绘制10以内数的分与合内容
+ */
+ async draw(decomposeData: NumberDecomposeData) {
if (!decomposeData || !decomposeData.problems) {
return;
}
- this.setPrintConfig();
this.decomposeData = decomposeData;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
-
- // 绘制内容区域
- this.drawDivider();
- await drawNumberDecomposeContent({
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
+ await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
decomposeData: this.decomposeData,
@@ -62,6 +54,519 @@ class NumberDecomposeDraw extends BaseDrawService {
startY: this.currentY,
});
}
+
+ /**
+ * 绘制内容区域
+ */
+ private async drawContent(params: {
+ canvas: WechatMiniprogram.Canvas;
+ ctx: RenderingContext;
+ decomposeData: NumberDecomposeData;
+ canvasWidth: number;
+ startY: number;
+ }): Promise {
+ const { canvas, ctx, decomposeData, canvasWidth, startY } = params;
+ const { problems, mode } = decomposeData;
+
+ if (mode === 'with-image') {
+ await this.drawWithImageMode(
+ canvas,
+ ctx,
+ problems,
+ canvasWidth,
+ startY,
+ );
+ } else if (mode === 'decompose') {
+ await this.drawDecomposeOrComposeMode(
+ ctx,
+ problems,
+ canvasWidth,
+ startY,
+ false,
+ );
+ } else if (mode === 'compose') {
+ await this.drawDecomposeOrComposeMode(
+ ctx,
+ problems,
+ canvasWidth,
+ startY,
+ true,
+ );
+ }
+ }
+
+ /**
+ * 有图片模式:一行三列,总共三行(9个题目)
+ */
+ private async drawWithImageMode(
+ canvas: WechatMiniprogram.Canvas,
+ ctx: RenderingContext,
+ problems: Array<{
+ whole: number | null;
+ part1: number | null;
+ part2: number | null;
+ imageIndex?: number;
+ imageType?: 'fruits' | 'twelve-animals';
+ }>,
+ canvasWidth: number,
+ startY: number,
+ ) {
+ const leftMargin = 30;
+ const rightMargin = 30;
+ const topMargin = 20;
+ const boxSpacing = 20;
+ const rowSpacing = 32;
+
+ // 计算每个框的尺寸
+ const availableWidth = canvasWidth - leftMargin - rightMargin;
+ const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
+ const imageAreaHeight = 120; // 图片区域高度
+ const treeAreaHeight = 80; // 二叉树区域高度
+
+ let currentY = startY + topMargin;
+
+ // 绘制3列3行
+ for (let row = 0; row < 3; row++) {
+ for (let col = 0; col < 3; col++) {
+ const problemIndex = row * 3 + col;
+ if (problemIndex >= problems.length) {
+ continue;
+ }
+
+ const problem = problems[problemIndex];
+ const boxX = leftMargin + col * (boxWidth + boxSpacing);
+ const boxY =
+ currentY +
+ row * (imageAreaHeight + treeAreaHeight + rowSpacing);
+
+ // 绘制一个完整的题目框
+ await this.drawWithImageProblem(
+ ctx,
+ canvas,
+ problem,
+ boxX,
+ boxY,
+ boxWidth,
+ imageAreaHeight,
+ treeAreaHeight,
+ );
+ }
+ }
+ }
+
+ /**
+ * 分模式/组合模式:一行3个,总共5行(15个题目)
+ * @param isInverted true 为组合模式(倒置二叉树),false 为分模式(正常二叉树)
+ */
+ private async drawDecomposeOrComposeMode(
+ ctx: RenderingContext,
+ problems: Array<{
+ whole: number | null;
+ part1: number | null;
+ part2: number | null;
+ }>,
+ canvasWidth: number,
+ startY: number,
+ isInverted: boolean,
+ ) {
+ const leftMargin = 30;
+ const rightMargin = 30;
+ const topMargin = 20;
+ const boxSpacing = 15;
+ const rowSpacing = 140;
+
+ // 计算每个框的尺寸
+ const availableWidth = canvasWidth - leftMargin - rightMargin;
+ const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
+ const boxHeight = 80; // 二叉树区域高度
+
+ let currentY = startY + topMargin;
+
+ // 绘制3列5行
+ for (let row = 0; row < 5; row++) {
+ for (let col = 0; col < 3; col++) {
+ const problemIndex = row * 3 + col;
+ if (problemIndex >= problems.length) {
+ continue;
+ }
+
+ const problem = problems[problemIndex];
+ const boxX = leftMargin + col * (boxWidth + boxSpacing);
+ const boxY = currentY + row * rowSpacing;
+
+ // 绘制二叉树
+ this.drawTree(
+ ctx,
+ problem,
+ boxX,
+ boxY,
+ boxWidth,
+ boxHeight,
+ isInverted,
+ );
+ }
+ }
+ }
+
+ /**
+ * 绘制有图片模式的单个题目
+ */
+ private async drawWithImageProblem(
+ ctx: RenderingContext,
+ canvas: WechatMiniprogram.Canvas,
+ problem: {
+ whole: number | null;
+ part1: number | null;
+ part2: number | null;
+ imageIndex?: number;
+ imageType?: 'fruits' | 'twelve-animals';
+ },
+ boxX: number,
+ boxY: number,
+ boxWidth: number,
+ imageAreaHeight: number,
+ treeAreaHeight: number,
+ ) {
+ const borderRadius = 12;
+
+ // 绘制实线框(只绘制图片区域的框)
+ ctx.strokeStyle = '#333';
+ ctx.lineWidth = 1;
+ this.drawRoundedRectInternal(
+ ctx,
+ boxX,
+ boxY,
+ boxWidth,
+ imageAreaHeight,
+ borderRadius,
+ false,
+ );
+
+ // 图片区域(增加底部间距,避免与二叉树根节点重叠)
+ const imageBottomPadding = 20; // 图片区域底部额外间距
+ const effectiveImageHeight = imageAreaHeight - imageBottomPadding;
+
+ if (problem.imageIndex && problem.imageType && problem.whole) {
+ await this.drawImagesInBox(
+ ctx,
+ canvas,
+ problem.whole,
+ problem.imageIndex,
+ problem.imageType,
+ boxX,
+ boxY,
+ boxWidth,
+ effectiveImageHeight,
+ );
+ }
+
+ // 二叉树区域(增加与图片区域的间距)
+ const treeTopSpacing = -5; // 二叉树区域顶部间距
+ const treeY = boxY + imageAreaHeight + treeTopSpacing;
+ this.drawTree(
+ ctx,
+ problem,
+ boxX,
+ treeY,
+ boxWidth,
+ treeAreaHeight,
+ false, // 不是倒置
+ );
+ }
+
+ /**
+ * 绘制二叉树结构
+ * @param isInverted 是否倒置(组合模式为true)
+ */
+ private drawTree(
+ ctx: RenderingContext,
+ problem: {
+ whole: number | null;
+ part1: number | null;
+ part2: number | null;
+ },
+ boxX: number,
+ boxY: number,
+ boxWidth: number,
+ boxHeight: number,
+ isInverted: boolean,
+ ) {
+ const nodeRadius = 18;
+ const nodeSpacing = 40; // 节点之间的水平间距
+ const verticalSpacing = 70; // 根节点和子节点的垂直间距
+
+ if (isInverted) {
+ // 组合模式:倒置二叉树
+ // 顶部:两个子节点(part1, part2)
+ // 底部:根节点(whole,需要填写)
+
+ const centerX = boxX + boxWidth / 2;
+ const topY = boxY + boxHeight / 2 - verticalSpacing / 2;
+ const bottomY = boxY + boxHeight / 2 + verticalSpacing / 2;
+
+ // 绘制两个子节点(顶部)
+ const leftNodeX = centerX - nodeSpacing / 2 - nodeRadius;
+ const rightNodeX = centerX + nodeSpacing / 2 + nodeRadius;
+
+ // 左子节点
+ this.drawNode(ctx, leftNodeX, topY, nodeRadius, problem.part1);
+ // 右子节点
+ this.drawNode(ctx, rightNodeX, topY, nodeRadius, problem.part2);
+
+ // 绘制根节点(底部)
+ this.drawNode(ctx, centerX, bottomY, nodeRadius, problem.whole);
+
+ // 绘制连接线(从子节点到根节点)
+ ctx.strokeStyle = '#333';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(leftNodeX, topY + nodeRadius);
+ ctx.lineTo(centerX, bottomY - nodeRadius);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(rightNodeX, topY + nodeRadius);
+ ctx.lineTo(centerX, bottomY - nodeRadius);
+ ctx.stroke();
+ } else {
+ // 分解模式:正常二叉树
+ // 顶部:根节点(whole)
+ // 底部:两个子节点(part1, part2,其中一个需要填写)
+
+ const centerX = boxX + boxWidth / 2;
+ const topY = boxY + boxHeight / 2 - verticalSpacing / 2;
+ const bottomY = boxY + boxHeight / 2 + verticalSpacing / 2;
+
+ // 绘制根节点(顶部)
+ this.drawNode(ctx, centerX, topY, nodeRadius, problem.whole);
+
+ // 绘制两个子节点(底部)
+ const leftNodeX = centerX - nodeSpacing / 2 - nodeRadius;
+ const rightNodeX = centerX + nodeSpacing / 2 + nodeRadius;
+
+ // 左子节点
+ this.drawNode(ctx, leftNodeX, bottomY, nodeRadius, problem.part1);
+ // 右子节点
+ this.drawNode(ctx, rightNodeX, bottomY, nodeRadius, problem.part2);
+
+ // 绘制连接线(从根节点到子节点)
+ ctx.strokeStyle = '#333';
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(centerX, topY + nodeRadius);
+ ctx.lineTo(leftNodeX, bottomY - nodeRadius);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(centerX, topY + nodeRadius);
+ ctx.lineTo(rightNodeX, bottomY - nodeRadius);
+ ctx.stroke();
+ }
+ }
+
+ /**
+ * 绘制节点(圆圈和数字)
+ */
+ private drawNode(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ radius: number,
+ value: number | null,
+ ) {
+ // 绘制圆角正方形
+ const size = radius * 2; // 正方形边长
+ const cornerRadius = 8; // 圆角半径
+ const halfSize = size / 2;
+
+ ctx.fillStyle = '#fff';
+ ctx.strokeStyle = '#333';
+ ctx.lineWidth = 2;
+
+ // 绘制圆角矩形
+ ctx.beginPath();
+ ctx.moveTo(x - halfSize + cornerRadius, y - halfSize);
+ ctx.lineTo(x + halfSize - cornerRadius, y - halfSize);
+ ctx.quadraticCurveTo(
+ x + halfSize,
+ y - halfSize,
+ x + halfSize,
+ y - halfSize + cornerRadius,
+ );
+ ctx.lineTo(x + halfSize, y + halfSize - cornerRadius);
+ ctx.quadraticCurveTo(
+ x + halfSize,
+ y + halfSize,
+ x + halfSize - cornerRadius,
+ y + halfSize,
+ );
+ ctx.lineTo(x - halfSize + cornerRadius, y + halfSize);
+ ctx.quadraticCurveTo(
+ x - halfSize,
+ y + halfSize,
+ x - halfSize,
+ y + halfSize - cornerRadius,
+ );
+ ctx.lineTo(x - halfSize, y - halfSize + cornerRadius);
+ ctx.quadraticCurveTo(
+ x - halfSize,
+ y - halfSize,
+ x - halfSize + cornerRadius,
+ y - halfSize,
+ );
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+
+ // 如果有值,绘制数字
+ if (value !== null) {
+ ctx.fillStyle = getRandomNumberColor();
+ ctx.font = `bold ${24}px "Microsoft Yahei"`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText(String(value), x, y);
+ }
+ }
+
+ /**
+ * 在框内绘制多张图片
+ */
+ private async drawImagesInBox(
+ ctx: RenderingContext,
+ canvas: WechatMiniprogram.Canvas,
+ count: number,
+ imageIndex: number,
+ imageType: 'fruits' | 'twelve-animals',
+ boxX: number,
+ boxY: number,
+ boxWidth: number,
+ boxHeight: number,
+ ) {
+ // 图片配置
+ const imageConfig = {
+ 'twelve-animals': {
+ folder: 'twelve-animals',
+ maxIndex: 12,
+ },
+ fruits: {
+ folder: 'fruits',
+ maxIndex: 22,
+ },
+ };
+
+ const config = imageConfig[imageType] || imageConfig['twelve-animals'];
+
+ const horizontalPadding = 5;
+ const verticalPadding = 10;
+ const availableWidth = boxWidth - horizontalPadding * 2;
+ const availableHeight = boxHeight - verticalPadding * 2;
+
+ // 根据数量确定每行的图片数和图片大小
+ let imagesPerRow: number;
+ let imageSize: number;
+
+ if (count <= 4) {
+ imagesPerRow = count <= 2 ? count : 2;
+ imageSize =
+ Math.min(availableWidth / imagesPerRow, availableHeight / 2) -
+ 4;
+ } else if (count <= 6) {
+ imagesPerRow = 3;
+ imageSize = Math.min(availableWidth / 3, availableHeight / 2) - 4;
+ } else {
+ imagesPerRow = 3;
+ imageSize = Math.min(availableWidth / 3, availableHeight / 3) - 4;
+ }
+
+ const rows = Math.ceil(count / imagesPerRow);
+ const imageSpacing =
+ (availableWidth - imageSize * imagesPerRow) / (imagesPerRow + 1);
+ const rowSpacing =
+ rows > 1 ? (availableHeight - imageSize * rows) / (rows + 1) : 0;
+
+ // 加载图片
+ let boxImage: any = null;
+ try {
+ const imagePath = `/mathPages/shared/assets/${config.folder}/${imageIndex}.png`;
+ boxImage = await getImage(canvas, imagePath);
+ } catch (error) {
+ console.error(`加载${config.folder}/${imageIndex}图片失败:`, error);
+ return;
+ }
+
+ // 绘制图片
+ for (let i = 0; i < count; i++) {
+ const row = Math.floor(i / imagesPerRow);
+ const col = i % imagesPerRow;
+
+ const imageX =
+ boxX +
+ horizontalPadding +
+ imageSpacing +
+ col * (imageSize + imageSpacing);
+ const imageY =
+ boxY +
+ verticalPadding +
+ // 这两段代码是用于计算每一张图片在Y轴方向上的实际位置(imageY 变量)
+ // 第一行:如果有多于一行,行间用 rowSpacing,否则单行时图片整体垂直居中排列。
+ (rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) +
+ // 第二行:加上具体第 row 行的纵向偏移(每张图片的高度加上行间距,再乘以行号)。
+ row * (imageSize + (rows > 1 ? rowSpacing : 0));
+
+ if (boxImage) {
+ // 计算图片高度(等比例缩放)
+ // @ts-ignore - 微信小程序图片对象有 width 和 height 属性
+ const scaledHeight =
+ (boxImage.height / boxImage.width) * imageSize;
+
+ ctx.drawImage(
+ boxImage,
+ imageX,
+ imageY,
+ imageSize,
+ scaledHeight,
+ );
+ }
+ }
+ }
+
+ /**
+ * 绘制圆角矩形(虚线边框)
+ */
+ private drawRoundedRectInternal(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ radius: number,
+ isDashed: boolean = false,
+ ) {
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.quadraticCurveTo(
+ x + width,
+ y + height,
+ x + width - radius,
+ y + height,
+ );
+ ctx.lineTo(x + radius, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+
+ if (isDashed) {
+ ctx.setLineDash([6 / 3, 6 / 3]); // 约2px的虚线
+ } else {
+ ctx.setLineDash([]);
+ }
+ ctx.stroke();
+ ctx.setLineDash([]); // 重置为实线
+ }
}
export default NumberDecomposeDraw;
diff --git a/miniprogram/mathPages/shared/service/numberFindDraw.ts b/miniprogram/mathPages/shared/service/numberFindDraw.ts
index cffb7b0..e820827 100644
--- a/miniprogram/mathPages/shared/service/numberFindDraw.ts
+++ b/miniprogram/mathPages/shared/service/numberFindDraw.ts
@@ -1,6 +1,5 @@
import { BaseDrawService } from '../../../service/baseDraw';
import { drawNumberPreview } from './numberPreviewDraw';
-import { drawNumberContent } from './numberContentDraw';
import { drawNumberWriteContent } from './numberWriteDraw';
/**
@@ -26,20 +25,11 @@ class NumberFindDraw extends BaseDrawService {
return;
}
- this.setPrintConfig();
this.selectedNumber = selectedNumber;
- this.clear();
- this.setPaper();
-
- // 绘制Header
- if (this.headerType !== 'minimal') {
- await this.drawHeader();
- } else {
- this.drawMiniHeader();
- }
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
// 绘制预览区域
- this.drawDivider();
await drawNumberPreview({
canvas: this.canvas,
ctx: this.ctx,
@@ -63,7 +53,7 @@ class NumberFindDraw extends BaseDrawService {
});
} else {
// 默认类型(number-find):绘制数字涂色内容
- drawNumberContent({
+ this.drawNumberContent({
ctx: this.ctx,
selectedNumber: this.selectedNumber,
canvasWidth: this.canvasWidth,
@@ -71,6 +61,75 @@ class NumberFindDraw extends BaseDrawService {
});
}
}
+
+ /**
+ * 绘制数字涂色内容区域
+ */
+ private drawNumberContent(params: {
+ ctx: RenderingContext;
+ selectedNumber: number;
+ canvasWidth: number;
+ startY: number;
+ }): void {
+ const { ctx, selectedNumber, canvasWidth, startY } = params;
+ const rows = 6;
+ const radius = 26; // 约26.67
+ const fontSize = 24; // 24
+
+ const startYPos = startY + 10 + radius; // 约47.33
+ const startX1 = 100 + radius; // 约112.22
+ const startX2 = 60 + radius; // 约75.56
+ const spaceWidthFirst = (canvasWidth - startX1 * 2) / (5 - 1);
+ const spaceWidthSecond = (canvasWidth - startX2 * 2) / (6 - 1);
+ const spaceHeight = 18; // 18
+
+ ctx.moveTo(startX1, startYPos);
+ for (let i = 0; i < rows; i++) {
+ const cols = i % 2 === 0 ? 5 : 6;
+
+ for (let j = 0; j < cols; j++) {
+ const y = startYPos + i * (spaceHeight + radius * 2);
+ const x =
+ i % 2 === 0
+ ? startX1 + j * spaceWidthFirst
+ : startX2 + j * spaceWidthSecond;
+
+ // 随机生成1-10的数字,但确保包含选中的数字
+ let number: number;
+ if (i === 0 && j === 0) {
+ // 第一个位置固定为选中的数字
+ number = selectedNumber;
+ } else {
+ // 其他位置随机,但增加选中数字出现的概率
+ const random = Math.random();
+ if (random < 0.3) {
+ // 30%概率是选中的数字
+ number = selectedNumber;
+ } else {
+ // 70%概率是其他随机数字
+ number = Math.floor(Math.random() * 10) + 1;
+ }
+ }
+
+ // 绘制圆形背景(线宽除以3)
+ ctx.fillStyle = '#fff';
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 1; // 约1.33
+ ctx.beginPath();
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.stroke();
+ ctx.closePath();
+
+ // 绘制数字(字体大小除以3)
+ ctx.fillStyle = '#333';
+ ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
+ ctx.textBaseline = 'middle';
+ ctx.textAlign = 'center';
+ ctx.fillText(String(number), x, y, radius * 2);
+ }
+ }
+ }
}
export default NumberFindDraw;
diff --git a/miniprogram/mathPages/shared/service/numberPreviewDraw.ts b/miniprogram/mathPages/shared/service/numberPreviewDraw.ts
index ec39752..d7ba6b6 100644
--- a/miniprogram/mathPages/shared/service/numberPreviewDraw.ts
+++ b/miniprogram/mathPages/shared/service/numberPreviewDraw.ts
@@ -52,7 +52,7 @@ export async function drawNumberPreview({
// 加载并绘制 animal-number 图片
try {
- const animalImagePath = `/mathPages/assets/animal-number/animal-number-${selectedNumber}.png`;
+ const animalImagePath = `/mathPages/shared/assets/animal-number/animal-number-${selectedNumber}.png`;
const animalImage = await getImage(canvas, animalImagePath);
ctx.drawImage(
animalImage,
@@ -79,7 +79,7 @@ export async function drawNumberPreview({
ctx.strokeRect(boxX, fingerBoxY, boxWidth, boxHeight);
try {
- const fingerImagePath = `/mathPages/assets/finger/finger-${selectedNumber}.png`;
+ const fingerImagePath = `/mathPages/shared/assets/finger/finger-${selectedNumber}.png`;
const fingerImage = await getImage(canvas, fingerImagePath);
// 计算图片在框内的位置(居中显示)
const imagePadding = 10;
diff --git a/miniprogram/mathPages/shared/service/numberSortDraw.ts b/miniprogram/mathPages/shared/service/numberSortDraw.ts
new file mode 100644
index 0000000..a79e16c
--- /dev/null
+++ b/miniprogram/mathPages/shared/service/numberSortDraw.ts
@@ -0,0 +1,177 @@
+/**
+ * 数字排序绘制服务
+ */
+
+import { BaseDrawService } from '../../../service/baseDraw';
+
+/**
+ * 数字排序数据
+ */
+export interface NumberSortData {
+ /** 6组题目数据 */
+ groups: Array<{
+ /** 4个乱序的数字及其颜色 */
+ numbers: Array<{
+ number: number;
+ color: string;
+ angle?: number; // 倾斜角度(弧度),-30度到+30度
+ }>;
+ }>;
+}
+
+/**
+ * 数字排序绘制服务
+ */
+class NumberSortDraw extends BaseDrawService {
+ sortData: NumberSortData | null = null;
+
+ constructor(
+ canvas: Canvas,
+ ctx: RenderingContext,
+ options?: Record,
+ ) {
+ super(canvas, ctx, options);
+ }
+
+ /**
+ * 绘制数字排序内容
+ */
+ async draw(sortData: NumberSortData) {
+ if (!sortData || !sortData.groups) {
+ return;
+ }
+
+ this.sortData = sortData;
+ this.prepareDraw();
+ await this.drawHeaderAndDivider();
+ this.drawContent({
+ ctx: this.ctx,
+ sortData: this.sortData,
+ canvasWidth: this.canvasWidth,
+ startY: this.currentY,
+ });
+ }
+
+ /**
+ * 绘制内容区域
+ */
+ private drawContent(params: {
+ ctx: RenderingContext;
+ sortData: NumberSortData;
+ canvasWidth: number;
+ startY: number;
+ }): void {
+ const { ctx, sortData, canvasWidth } = params;
+ let { startY } = params;
+
+ startY = startY + 25;
+
+ const margin = 40; // 左右边距
+ const rowSpacing = 60; // 行间距
+ const colSpacing = 40; // 列间距
+ const boxSpacing = 10; // 格子之间的间距
+
+ // 数字区域参数
+ const numberSize = 120; // 数字大小(字体大小)
+ const numberAreaHeight = 120; // 数字区域高度(增大以容纳更大的数字)
+ const numberOverlapSpacing = 35; // 数字之间的间距(用于重叠效果)
+
+ // 格子参数
+ const boxWidth = 40; // 格子宽度
+ const boxHeight = 50; // 格子高度(高比宽长)
+ const boxBorderWidth = 1; // 边框宽度
+ const boxBorderColor = '#999'; // 边框颜色
+
+ // 4个格子的总宽度
+ const boxesTotalWidth = 4 * boxWidth + 3 * boxSpacing;
+
+ // 每组的总宽度(以格子的总宽度为准)
+ const groupWidth = boxesTotalWidth;
+
+ // 计算总宽度(两列 + 一列间距)
+ const totalWidth = 2 * groupWidth + colSpacing;
+ const groupsStartX =
+ margin + (canvasWidth - margin * 2 - totalWidth) / 2;
+
+ // 每组的总高度(数字区域 + 格子区域)
+ const groupHeight = numberAreaHeight + boxHeight;
+
+ // 绘制6个组(三行两列)
+ for (let row = 0; row < 3; row++) {
+ for (let col = 0; col < 2; col++) {
+ const groupIndex = row * 2 + col;
+ const group = sortData.groups[groupIndex];
+ if (!group) continue;
+
+ const groupX = groupsStartX + col * (groupWidth + colSpacing);
+ const groupY = startY + row * (groupHeight + rowSpacing);
+
+ // 格子的起始X坐标(居中)
+ const boxesStartX = groupX;
+ const boxesY = groupY + numberAreaHeight;
+
+ // 计算数字组的中心位置(与格子中心对齐)
+ const boxesCenterX = boxesStartX + boxesTotalWidth / 2;
+
+ // 计算数字组的总宽度(从第一个数字中心到最后一个数字中心的距离)
+ const numbersGroupWidth = 3 * numberOverlapSpacing;
+
+ // 第一个数字的中心位置(数字组居中对齐)
+ const firstNumberCenterX = boxesCenterX - numbersGroupWidth / 2;
+
+ // 绘制4个数字(上边),有重叠效果
+ for (let i = 0; i < group.numbers.length; i++) {
+ const numberItem = group.numbers[i];
+ // 数字中心位置(有重叠)
+ const numberX =
+ firstNumberCenterX + i * numberOverlapSpacing;
+ const numberY = groupY + numberAreaHeight / 2;
+
+ // 获取倾斜角度(如果存在,否则使用0)
+ const angle = numberItem.angle || 0;
+
+ // 保存上下文状态
+ ctx.save();
+
+ // 移动到数字位置
+ ctx.translate(numberX, numberY);
+ // 旋转(角度转换为弧度)
+ ctx.rotate(angle);
+
+ // 绘制数字(带边线)
+ const text = numberItem.number.toString();
+ ctx.font = `bold ${numberSize}px Arial`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ // 先绘制边线
+ ctx.strokeStyle = '#141414';
+ ctx.lineWidth = 3;
+ ctx.strokeText(text, 0, 0);
+
+ // 再绘制填充
+ ctx.fillStyle = numberItem.color;
+ ctx.fillText(text, 0, 0);
+
+ // 恢复上下文状态
+ ctx.restore();
+ }
+
+ // 绘制4个长方形格子(下边)
+
+ for (let i = 0; i < 4; i++) {
+ const boxX = boxesStartX + i * (boxWidth + boxSpacing);
+
+ // 绘制格子边框
+ ctx.strokeStyle = boxBorderColor;
+ ctx.lineWidth = boxBorderWidth;
+ ctx.beginPath();
+ ctx.rect(boxX, boxesY, boxWidth, boxHeight);
+ ctx.stroke();
+ }
+ }
+ }
+ }
+}
+
+export default NumberSortDraw;
diff --git a/miniprogram/service/baseDraw.ts b/miniprogram/service/baseDraw.ts
index b4867d3..60c9296 100644
--- a/miniprogram/service/baseDraw.ts
+++ b/miniprogram/service/baseDraw.ts
@@ -397,4 +397,31 @@ export class BaseDrawService {
ctx.stroke();
}
}
+
+ /**
+ * 准备绘制(公共初始化逻辑)
+ * 执行:setPrintConfig -> clear -> setPaper
+ * 子类可以在调用此方法前后执行自定义逻辑(如数据验证、异步资源加载等)
+ */
+ prepareDraw() {
+ this.setPrintConfig();
+ this.clear();
+ this.setPaper();
+ }
+
+ /**
+ * 绘制 Header 和 Divider(公共绘制逻辑)
+ * 根据 headerType 自动选择绘制完整 Header 或迷你 Header,然后绘制分割线
+ */
+ async drawHeaderAndDivider() {
+ // 绘制Header
+ if (this.headerType !== 'minimal') {
+ await this.drawHeader();
+ } else {
+ this.drawMiniHeader();
+ }
+
+ // 绘制内容区域分割线
+ this.drawDivider();
+ }
}
diff --git a/project.private.config.json b/project.private.config.json
index c74b149..96d53fe 100644
--- a/project.private.config.json
+++ b/project.private.config.json
@@ -23,12 +23,19 @@
"condition": {
"miniprogram": {
"list": [
+ {
+ "name": "mathPages/numberSort/numberSort",
+ "pathName": "mathPages/numberSort/numberSort",
+ "query": "id=number-sort",
+ "scene": null,
+ "launchMode": "default"
+ },
{
"name": "focusPages/codeConnect/codeConnect",
"pathName": "focusPages/codeConnect/codeConnect",
"query": "id=code-connect",
- "scene": null,
- "launchMode": "default"
+ "launchMode": "default",
+ "scene": null
},
{
"name": "focusPages/gridReasoning/gridReasoning",