Merge branch 'feature-2.5.6' into feature-2.6.0
@@ -17,7 +17,8 @@
|
||||
"missingNumber/missingNumber",
|
||||
"compare/compare",
|
||||
"countingSelect/countingSelect",
|
||||
"numberDecompose/numberDecompose"
|
||||
"numberDecompose/numberDecompose",
|
||||
"numberSort/numberSort"
|
||||
],
|
||||
"independent": false
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,38 +1,6 @@
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">预览打印效果</text>
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<!-- 预览打印效果 -->
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
|
||||
<!-- 随机生成按钮 -->
|
||||
<view class="random-button-area">
|
||||
<toy-button
|
||||
class="random-button"
|
||||
type="primary"
|
||||
bind:click="onRandom"
|
||||
width="100%"
|
||||
height="80rpx"
|
||||
icon="refresh"
|
||||
icon-class-prefix="toy-icon">
|
||||
随机生成
|
||||
</toy-button>
|
||||
</view>
|
||||
<draw-ad type="focusDraw"></draw-ad>
|
||||
</view>
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
<math-bottom-buttons
|
||||
disabled="{{!hasContent}}"
|
||||
bind:share="onShareAppMessage"
|
||||
bind:export="exportToPrint" />
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
|
||||
@@ -1,38 +1,6 @@
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">预览打印效果</text>
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<!-- 预览打印效果 -->
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
|
||||
<!-- 随机生成按钮 -->
|
||||
<view class="random-button-area">
|
||||
<toy-button
|
||||
class="random-button"
|
||||
type="primary"
|
||||
bind:click="onRandom"
|
||||
width="100%"
|
||||
height="80rpx"
|
||||
icon="refresh"
|
||||
icon-class-prefix="toy-icon">
|
||||
随机生成
|
||||
</toy-button>
|
||||
</view>
|
||||
<draw-ad type="focusDraw"></draw-ad>
|
||||
</view>
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
<math-bottom-buttons
|
||||
disabled="{{!hasContent}}"
|
||||
bind:share="onShareAppMessage"
|
||||
bind:export="exportToPrint" />
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">预览打印效果</text>
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<!-- 预览打印效果 -->
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
|
||||
<!-- 类型选择器和随机生成按钮 -->
|
||||
<math-type-selector
|
||||
current-type-name="{{currentModeName}}"
|
||||
type-actions="{{typeActions}}"
|
||||
bind:select="onSelectType"
|
||||
bind:random="onRandom" />
|
||||
<draw-ad type="focusDraw"></draw-ad>
|
||||
</view>
|
||||
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
<math-bottom-buttons
|
||||
disabled="{{!hasContent}}"
|
||||
bind:share="onShareAppMessage"
|
||||
bind:export="exportToPrint" />
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{
|
||||
boxWidth: boxWidth,
|
||||
boxHeight: boxHeight,
|
||||
hasTypeSelector: true,
|
||||
currentModeName: currentModeName,
|
||||
typeActions: typeActions,
|
||||
adType: 'focusDraw',
|
||||
disabled: !hasContent,
|
||||
showShareDialog: showShareDialog
|
||||
}}" />
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">预览打印效果</text>
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<!-- 预览打印效果 -->
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
|
||||
<!-- 类型选择器和随机生成按钮 -->
|
||||
<math-type-selector
|
||||
current-type-name="{{currentOperatorTypeName}}"
|
||||
type-actions="{{operatorTypeActions}}"
|
||||
bind:select="onSelectType"
|
||||
bind:random="onRandom" />
|
||||
<draw-ad type="focusDraw"></draw-ad>
|
||||
</view>
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
<math-bottom-buttons
|
||||
disabled="{{!hasContent}}"
|
||||
bind:share="onShareAppMessage"
|
||||
bind:export="exportToPrint" />
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{
|
||||
boxWidth: boxWidth,
|
||||
boxHeight: boxHeight,
|
||||
hasTypeSelector: true,
|
||||
currentModeName: currentOperatorTypeName,
|
||||
typeActions: operatorTypeActions,
|
||||
adType: 'focusDraw',
|
||||
disabled: !hasContent,
|
||||
showShareDialog: showShareDialog
|
||||
}}" />
|
||||
|
||||
@@ -1,38 +1,6 @@
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">预览打印效果</text>
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<!-- 预览打印效果 -->
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
|
||||
<!-- 随机生成按钮 -->
|
||||
<view class="random-button-area">
|
||||
<toy-button
|
||||
class="random-button"
|
||||
type="primary"
|
||||
bind:click="onRandom"
|
||||
width="100%"
|
||||
height="80rpx"
|
||||
icon="refresh"
|
||||
icon-class-prefix="toy-icon">
|
||||
随机生成
|
||||
</toy-button>
|
||||
</view>
|
||||
<draw-ad type="focusDraw"></draw-ad>
|
||||
</view>
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
<math-bottom-buttons
|
||||
disabled="{{!hasContent}}"
|
||||
bind:share="onShareAppMessage"
|
||||
bind:export="exportToPrint" />
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
|
||||
@@ -1,38 +1,6 @@
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">预览打印效果</text>
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<!-- 预览打印效果 -->
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
|
||||
<!-- 随机生成按钮 -->
|
||||
<view class="random-button-area">
|
||||
<toy-button
|
||||
class="random-button"
|
||||
type="primary"
|
||||
bind:click="onRandom"
|
||||
width="100%"
|
||||
height="80rpx"
|
||||
icon="refresh"
|
||||
icon-class-prefix="toy-icon">
|
||||
随机生成
|
||||
</toy-button>
|
||||
</view>
|
||||
<draw-ad type="focusDraw"></draw-ad>
|
||||
</view>
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
<math-bottom-buttons
|
||||
disabled="{{!hasContent}}"
|
||||
bind:share="onShareAppMessage"
|
||||
bind:export="exportToPrint" />
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
|
||||
@@ -1,38 +1,6 @@
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">预览打印效果</text>
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<!-- 预览打印效果 -->
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
|
||||
<!-- 随机生成按钮 -->
|
||||
<view class="random-button-area">
|
||||
<toy-button
|
||||
class="random-button"
|
||||
type="primary"
|
||||
bind:click="onRandom"
|
||||
width="100%"
|
||||
height="80rpx"
|
||||
icon="refresh"
|
||||
icon-class-prefix="toy-icon">
|
||||
随机生成
|
||||
</toy-button>
|
||||
</view>
|
||||
<draw-ad type="focusDraw"></draw-ad>
|
||||
</view>
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
<math-bottom-buttons
|
||||
disabled="{{!hasContent}}"
|
||||
bind:share="onShareAppMessage"
|
||||
bind:export="exportToPrint" />
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<!--
|
||||
Canvas 页面模板
|
||||
用于统一管理 Canvas 绘制页面的通用结构
|
||||
|
||||
使用方式:
|
||||
<import src="../../templates/canvas-page-template.wxml" />
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
|
||||
参数说明:
|
||||
- boxWidth: Canvas 宽度
|
||||
- boxHeight: Canvas 高度
|
||||
- hasTypeSelector: 是否显示类型选择器(布尔值)
|
||||
- showTypeSelector: 条件显示类型选择器(可选,用于 countMatch、missingNumber 等页面)
|
||||
- currentModeName: 当前模式名称(类型选择器使用)
|
||||
- typeActions: 类型选项数组(类型选择器使用)
|
||||
- adType: 广告位类型('focusDraw' 或 'mathDraw')
|
||||
- disabled: 底部按钮是否禁用
|
||||
- showShareDialog: 是否显示分享弹窗
|
||||
-->
|
||||
<template name="canvasPageTemplate">
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">预览打印效果</text>
|
||||
|
||||
<!-- 预览打印效果 -->
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
|
||||
<!-- 类型选择器和随机生成按钮 -->
|
||||
<!-- 支持两种方式:hasTypeSelector(布尔值)或 showTypeSelector(条件显示) -->
|
||||
<math-type-selector
|
||||
wx:if="{{hasTypeSelector || showTypeSelector}}"
|
||||
current-type-name="{{currentModeName}}"
|
||||
type-actions="{{typeActions}}"
|
||||
bind:select="onSelectType"
|
||||
bind:random="onRandom" />
|
||||
|
||||
<!-- 随机生成按钮(当没有类型选择器时显示) -->
|
||||
<view
|
||||
class="random-button-area"
|
||||
wx:if="{{!hasTypeSelector && !showTypeSelector}}">
|
||||
<toy-button
|
||||
class="random-button"
|
||||
type="primary"
|
||||
bind:click="onRandom"
|
||||
width="100%"
|
||||
height="80rpx"
|
||||
icon="refresh"
|
||||
icon-class-prefix="toy-icon">
|
||||
随机生成
|
||||
</toy-button>
|
||||
</view>
|
||||
|
||||
<!-- 广告位 -->
|
||||
<draw-ad type="{{adType}}"></draw-ad>
|
||||
</view>
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<math-bottom-buttons
|
||||
disabled="{{disabled}}"
|
||||
bind:share="onShareAppMessage"
|
||||
bind:export="exportToPrint" />
|
||||
|
||||
<!-- 分享引导弹窗 -->
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"navigationBarTitleText": "数字排序",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../ui/button/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -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<string, any>,
|
||||
) => {
|
||||
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();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">预览打印效果</text>
|
||||
|
||||
<!-- 预览打印效果 -->
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
|
||||
<!-- 随机生成按钮 -->
|
||||
<view class="random-button-area">
|
||||
<toy-button
|
||||
class="random-button"
|
||||
type="primary"
|
||||
bind:click="onRandom"
|
||||
width="100%"
|
||||
height="80rpx"
|
||||
icon="refresh"
|
||||
icon-class-prefix="toy-icon">
|
||||
随机生成
|
||||
</toy-button>
|
||||
</view>
|
||||
<draw-ad type="mathDraw"></draw-ad>
|
||||
</view>
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
<math-bottom-buttons
|
||||
disabled="{{!hasContent}}"
|
||||
bind:share="onShareAppMessage"
|
||||
bind:export="exportToPrint" />
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@@ -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<void> {
|
||||
// 根据图片类型确定图片目录和最大索引
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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;
|
||||
|
||||
@@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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;
|
||||
|
||||
@@ -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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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;
|
||||
|
||||
@@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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;
|
||||
|
||||
@@ -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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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;
|
||||
|
||||
@@ -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<void> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, any>,
|
||||
) {
|
||||
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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||