feat:架构代码优化

This commit is contained in:
R524809
2025-12-11 13:02:44 +08:00
parent 2c32235568
commit 3c0e2af10d
89 changed files with 2774 additions and 2816 deletions
+172
View File
@@ -0,0 +1,172 @@
# Service 目录绘制服务改造完成总结
## 改造目标
将所有绘制服务统一使用 `baseDraw.ts``baseHeaderDraw.ts` 作为基类,提取公共部分并统一 Header 绘制。
## 完成的工作
### 1. ✅ 统一基础服务类
所有绘制服务现在都继承自 `BaseDrawService`(位于 `service/baseDraw.ts`):
-`TextDrawService` - 文字涂色服务
-`ShapeDrawService` - 图形涂色服务
-`FindWordDrawService` - 找字涂色服务
-`WordDrawService` - 田字格练字服务
### 2. ✅ 统一 Header 绘制
所有服务使用统一的 Header 绘制功能(位于 `service/baseHeaderDraw.ts`):
- `drawBaseHeader()` - 完整 Header(包含 Logo、应用名称、标题、副标题)
- `drawBaseMiniHeader()` - 迷你 Header(简化版本)
### 3. ✅ 统一使用逻辑像素
所有服务现在使用逻辑像素(尺寸除以3),与数学模块保持一致:
- Canvas 尺寸通过 `setPaper()` 统一处理 DPRdevice pixel ratio
- 所有绘制代码中的硬编码尺寸都已转换为逻辑像素
- 坐标计算统一使用 `this.canvasWidth``this.canvasHeight`
### 4. ✅ 清理重复代码
删除了以下重复方法,统一使用基类实现:
- `setPrintConfig()` - 统一在基类中实现
- `setPaper()` - 统一在基类中实现
- `clear()` - 统一在基类中实现
- `drawHeader()` / `drawMiniHeader()` - 统一在基类中实现
- `drawDivider()` - 统一在基类中实现
## 改造详情
### TextDrawService
**改造内容:**
- 继承 `BaseDrawService`
- 移除自定义的 `setPaper()`, `clear()`, `drawHeader()`, `drawMiniHeader()`
- 所有尺寸转换为逻辑像素:
- 半径:80 → 27
- 字体:72px → 24px, 48px → 16px
- 边距:80 → 27, 220 → 73
- 坐标:200 → 67, 304 → 101
**关键方法:**
- `drawLegend()` - 绘制示例区域
- `drawContent()` - 绘制内容区域
### ShapeDrawService
**改造内容:**
- 继承 `BaseDrawService`
- 移除自定义的绘制方法
- 所有尺寸转换为逻辑像素:
- 图形大小:200 → 67, 180 → 60
- 字体:36px → 12px
- 间距:120 → 40, 160 → 53
**关键方法:**
- `drawLegend()` - 绘制示例区域
- `drawContent()` - 绘制内容区域
### FindWordDrawService
**改造内容:**
- 继承 `BaseDrawService`
- 移除自定义的绘制方法
- 所有尺寸转换为逻辑像素:
- 半径:80 → 27
- 字体:72px → 24px
- 模板坐标转换:`pos.x / 3`, `pos.y / 3`
**关键方法:**
- `drawContent()` - 绘制找字内容(中心大字 + 四周字符)
**特殊处理:**
- `getPositionsFromTemplate()` 函数中增加了坐标转换逻辑,将模板中的原始像素坐标转换为逻辑像素
### WordDrawService
**改造内容:**
- 继承 `BaseDrawService`
- 移除自定义的绘制方法
- 所有尺寸转换为逻辑像素:
- 田字格大小:140 → 47
- 边距:120 → 40, 50 → 17
- 间距:24 → 8, 36 → 12
**关键方法:**
- `drawLayout()` - 绘制页眉和基础布局
- `drawContent()` - 绘制田字格和练字内容
- `drawPracticeContent()` - 绘制练习内容
- `getMaxGridLayout()` - 计算最大网格布局
## 架构优势
1.**代码复用**:公共逻辑集中在 `BaseDrawService` 基类中
2.**统一 Header**:所有服务使用相同的 Header 绘制逻辑
3.**统一像素标准**:所有服务使用逻辑像素,保持一致
4.**易于维护**:修改 Header 或基础功能只需修改一个地方
5.**类型安全**TypeScript 类型支持完整
## 注意事项
### 坐标转换
所有硬编码的尺寸值都已转换为逻辑像素(除以3):
```typescript
// 原始像素 → 逻辑像素
const radius = 27; // 80/3≈27
const fontSize = 24; // 72/3=24
const margin = 40; // 120/3=40
```
### 模板坐标转换
`FindWordDrawService` 中使用的 `POSITION_TEMPLATES` 坐标是基于原始像素的,需要在使用时转换:
```typescript
const x = centerX + pos.x / 3; // 转换为逻辑像素
const y = centerY + pos.y / 3; // 转换为逻辑像素
```
### Canvas 尺寸
所有服务现在统一使用:
- `this.canvasWidth` - 逻辑像素宽度
- `this.canvasHeight` - 逻辑像素高度
这些值在 `setPaper()` 中设置,已经处理了 DPR。
## 向后兼容性
-`drawServiceFactory.ts` 接口无需修改
- ✅ 所有服务的公共接口保持不变
- ✅ 只改变了内部实现方式
## 后续优化建议
1. 可以考虑将 `findWordTemplate.ts` 中的坐标模板也转换为逻辑像素,避免运行时转换
2. 可以考虑创建更多的基础绘制工具函数(如绘制表格、绘制网格等)
3. 可以考虑支持更多 Paper 尺寸(目前仅支持 A4)
## 测试建议
1. 验证所有绘制服务功能正常
2. 验证 Header 正确显示
3. 验证尺寸和比例正确
4. 验证不同 Header 类型(wechat, LogoImage, noLogoImage, minimal
5. 验证打印输出质量
+218
View File
@@ -0,0 +1,218 @@
# 绘制服务重构总结
## 重构目标
基于 `baseDraw.ts``baseHeaderDraw.ts` 统一所有绘制服务的公共部分,提取并统一 Header 绘制功能。
## 完成的工作
### 1. ✅ 统一基础服务类
所有绘制服务现在都继承自 `BaseDrawService`(位于 `service/baseDraw.ts`):
-`AdditionDraw` - 加减法计算
-`CompareDraw` - 数一数比大小
-`CountMatchDraw` - 数一数连一连
-`CountingSelectDraw` - 数一数选一选/填一填
-`MissingNumberDraw` - 填上缺少的数字
-`NumberColorDraw` - 按数字涂颜色
-`NumberDecomposeDraw` - 10以内数的分与合
-`NumberFindDraw` - 数字涂色(找数字、写数字)
### 2. ✅ 统一 Header 绘制
所有服务使用统一的 Header 绘制功能(位于 `service/baseHeaderDraw.ts`):
- `drawBaseHeader()` - 完整 Header(包含 Logo、应用名称、标题、副标题)
- `drawBaseMiniHeader()` - 迷你 Header(简化版本)
### 3. ✅ 修复相关导入
- ✅ 修复 `base/pageMixin.ts` 中的导入路径
-`../mathPages/service/baseMathDraw` 改为 `../service/baseDraw`
### 4. ✅ 清理旧文件
- ✅ 删除 `mathPages/shared/service/baseMathDraw.ts`(已被 `BaseDrawService` 替代)
- ✅ 删除 `mathPages/shared/service/mathHeaderDraw.ts`(已被 `baseHeaderDraw.ts` 替代)
## 架构设计
### 基础服务类 (`BaseDrawService`)
提供以下公共功能:
```typescript
class BaseDrawService {
// Canvas 和上下文
canvas: Canvas;
ctx: RenderingContext;
// 配置
options: Record<string, any>;
paperSize: PaperSize;
headerType: PrintHeader;
// 坐标管理
currentX: number;
currentY: number;
canvasWidth: number;
canvasHeight: number;
// 公共方法
setPrintConfig(); // 设置打印配置
setPaper(); // 设置 Paper 尺寸
clear(); // 清除画布
drawHeader(); // 绘制完整 Header
drawMiniHeader(); // 绘制迷你 Header
drawDivider(); // 绘制分割线
}
```
### Header 绘制 (`baseHeaderDraw.ts`)
提供统一的 Header 绘制函数:
- `drawBaseHeader()` - 支持多种 Header 类型:
- `wechat` - 微信小程序码(默认)
- `LogoImage` - Logo 图片
- `noLogoImage` - 无 Logo
- `minimal` - 极简模式
- `drawBaseMiniHeader()` - 迷你版本,居中显示
### 具体服务类
每个具体服务类:
1. **继承 `BaseDrawService`**
2. **实现 `draw()` 方法**,通常包含以下步骤:
```typescript
async draw(data: any) {
// 1. 设置配置
this.setPrintConfig();
// 2. 清除并设置 Paper
this.clear();
this.setPaper();
// 3. 绘制 Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
this.drawMiniHeader();
}
// 4. 绘制分割线
this.drawDivider();
// 5. 绘制内容(调用 ContentDraw 函数)
await drawXXXContent({
canvas: this.canvas,
ctx: this.ctx,
data: data,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
```
3. **使用 ContentDraw 函数**绘制具体内容(例如 `drawAdditionContent`
## 文件结构
```
service/
├── baseDraw.ts # 基础绘制服务类 ✅
├── baseHeaderDraw.ts # Header 绘制函数 ✅
└── ...
mathPages/shared/service/
├── additionDraw.ts # 加减法绘制服务 ✅
├── compareDraw.ts # 比较绘制服务 ✅
├── countMatchDraw.ts # 连线绘制服务 ✅
├── countingSelectDraw.ts # 选择绘制服务 ✅
├── missingNumberDraw.ts # 缺失数字绘制服务 ✅
├── numberColorDraw.ts # 数字涂色绘制服务 ✅
├── numberDecomposeDraw.ts # 分解绘制服务 ✅
├── numberFindDraw.ts # 数字查找绘制服务 ✅
├── additionContentDraw.ts # 加减法内容绘制
├── compareContentDraw.ts # 比较内容绘制
├── ... # 其他 ContentDraw 函数
└── (已删除)
├── baseMathDraw.ts # ❌ 已删除
└── mathHeaderDraw.ts # ❌ 已删除
```
## 使用示例
### 创建新的绘制服务
```typescript
import { BaseDrawService } from '../../../service/baseDraw';
import { drawMyContent } from './myContentDraw';
class MyDraw extends BaseDrawService {
myData: any;
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
this.myData = null;
}
async draw(myData: any) {
if (!myData) return;
this.setPrintConfig();
this.myData = myData;
this.clear();
this.setPaper();
// 绘制 Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
this.drawMiniHeader();
}
// 绘制内容
this.drawDivider();
await drawMyContent({
canvas: this.canvas,
ctx: this.ctx,
data: this.myData,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
}
export default MyDraw;
```
## 优势
1. ✅ **代码复用**:公共逻辑集中在基类中
2. ✅ **统一 Header**:所有服务使用相同的 Header 绘制逻辑
3. ✅ **易于维护**:修改 Header 只需修改一个地方
4. ✅ **类型安全**TypeScript 类型支持完整
5. ✅ **扩展性好**:新增服务只需继承基类并实现 `draw()` 方法
## 注意事项
1. **ContentDraw 函数**:具体内容绘制函数通常以 `drawXXXContent` 命名,接收参数包括 `canvas`、`ctx`、数据、`canvasWidth`、`startY` 等
2. **坐标管理**:使用 `this.currentY` 管理垂直坐标,ContentDraw 函数需要更新该值
3. **Header 类型**:通过 `this.headerType` 判断使用完整 Header 还是迷你 Header
4. **逻辑像素**:所有尺寸都已除以 3,使用逻辑像素
## 后续优化建议
1. 考虑将 ContentDraw 函数也提取到 `service/` 目录下的统一位置
2. 可以考虑创建更多的基础绘制工具函数(如绘制表格、绘制网格等)
3. 可以考虑支持更多 Paper 尺寸(目前仅支持 A4)
+149
View File
@@ -0,0 +1,149 @@
import { PAPER_SIZE } from '../constants/colors';
import { drawBaseHeader, drawBaseMiniHeader } from './baseHeaderDraw';
/**
* 基础绘制服务
* 包含Paper设置和Header绘制功能,可被所有绘制服务复用
*
* 提供功能:
* - Canvas 初始化和配置
* - Paper 尺寸设置(支持 A4 等标准尺寸)
* - Header 绘制(支持完整 Header 和迷你 Header
* - 分割线绘制
* - 打印配置管理
*/
export class BaseDrawService {
headerType: PrintHeader = 'wechat';
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: Record<string, any>;
paperSize: PaperSize;
currentX: number;
currentY: number;
canvasWidth: number; // 逻辑像素宽度
canvasHeight: number; // 逻辑像素高度
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
options = options || {};
const { appName, appHint } = getApp().getPrintConfig();
this.canvas = canvas;
this.ctx = ctx;
this.paperSize = 'A4';
this.options = {
appName,
appHint,
title: '看数字,涂一涂',
subTitle: '找一找下面相同的数字,涂上颜色',
...options,
};
this.currentX = 0;
this.currentY = 0;
this.canvasWidth = 0;
this.canvasHeight = 0;
this.setPrintConfig();
}
setPrintConfig() {
const printConfig = getApp().getPrintConfig();
this.headerType = printConfig.header;
this.options.appName = printConfig.appName;
}
/**
* 设置Paper(逻辑像素,尺寸除以3)
*/
setPaper() {
const { ctx, canvas } = this;
const { pixelRatio: dpr } = wx.getWindowInfo();
let { width, height } = PAPER_SIZE[this.paperSize];
this.canvasWidth = width;
this.canvasHeight = height;
// 设置 canvas 为物理像素尺寸(用于高分辨率显示)
const physicalWidth = width * dpr;
const physicalHeight = height * dpr;
canvas.width = physicalWidth;
canvas.height = physicalHeight;
// 重置 transform 并 scale 到逻辑像素
ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置 transform
ctx.scale(dpr, dpr); // scale 到逻辑像素,后续绘制都使用逻辑像素
this.clear();
ctx.fillStyle = '#fff';
// 使用逻辑像素尺寸填充
ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
}
/**
* 清除画布
*/
clear() {
const canvas = this.canvas;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
}
/**
* 绘制Header(逻辑像素,尺寸除以3)
*/
async drawHeader() {
this.currentX = 25;
this.currentY = 25;
await drawBaseHeader({
canvas: this.canvas,
ctx: this.ctx,
headerType: this.headerType,
options: {
appName: this.options.appName || '涂鸦丫小程序',
appHint: this.options.appHint || '识字|识图|练字|打印',
title: this.options.title || '看数字,涂一涂',
subTitle:
this.options.subTitle || '找一找下面相同的数字,涂上颜色',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
},
});
}
/**
* 绘制迷你Header(逻辑像素,尺寸除以3)
*/
drawMiniHeader() {
drawBaseMiniHeader({
ctx: this.ctx,
canvasWidth: this.canvasWidth,
options: {
appName: this.options.appName || '涂鸦丫小程序',
title: this.options.title || '看数字,涂一涂',
},
onHeaderDrawn: (currentY) => {
console.log('drawMiniHeader currentY', currentY);
this.currentY = currentY;
},
});
}
/**
* 绘制分割线(逻辑像素,尺寸除以3)
*/
drawDivider() {
const { ctx, canvasWidth } = this;
const dividerY = this.currentY;
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(24, dividerY);
ctx.lineTo(canvasWidth - 24, dividerY);
ctx.stroke();
this.currentY = dividerY + 10; // 分割线下方10px间距
}
}
+129
View File
@@ -0,0 +1,129 @@
import { getMiniCodeImage, getImage } from '../utils/index';
/**
* 绘制数学模块页眉的参数接口(尺寸除以3)
*/
interface drawBaseHeaderParams {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
headerType: PrintHeader;
options: {
appName: string;
appHint: string;
title: string;
subTitle: string;
};
onHeaderDrawn?: (currentY: number) => void;
}
/**
* 绘制数学模块完整页眉(尺寸除以3)
*/
export async function drawBaseHeader({
canvas,
ctx,
headerType,
options,
onHeaderDrawn,
}: drawBaseHeaderParams): Promise<void> {
const { appName, appHint, title, subTitle } = options;
let titleX = 108; // 约109.33
const titleY = 25; // 约26.67
const logoX = 24; // 约26.67
const logoY = 20; // 20
const logoWidth = 65; // 约66.67
const logoHeight = 65; // 约66.67
// 根据 headerType 绘制 Logo 或调整标题位置
switch (headerType) {
case 'LogoImage': {
const image = await getImage(
canvas,
'/assets/imgs/doodle-logo.png',
);
ctx.drawImage(image, logoX, logoY, logoWidth, logoHeight);
break;
}
case 'noLogoImage': {
titleX = 40;
break;
}
case 'minimal': {
titleX = 40;
break;
}
default: {
const image = await getMiniCodeImage(canvas);
ctx.drawImage(image, logoX, logoY, logoWidth, logoHeight);
break;
}
}
// 绘制应用名称(字体大小除以3
ctx.font = 'bold 22px "Microsoft Yahei"';
ctx.fillStyle = '#000';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(appName, titleX, titleY);
// 绘制应用提示(字体大小除以3
ctx.font = '16px "Microsoft Yahei"';
ctx.fillStyle = '#666';
ctx.fillText(appHint, titleX, 66);
// 绘制标题(字体大小除以3
ctx.font = 'bold 22px "Microsoft Yahei"';
ctx.fillStyle = '#000';
ctx.fillText(title, 280, titleY);
// 绘制副标题(字体大小除以3
ctx.font = '16px "Microsoft Yahei"';
ctx.fillStyle = '#666';
ctx.fillText(subTitle, 280, 66);
// 调用回调函数
if (onHeaderDrawn) {
onHeaderDrawn(104);
}
}
/**
* 绘制数学模块迷你页眉的参数接口(尺寸除以3)
*/
interface drawBaseMiniHeaderParams {
// canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: {
appName: string;
title: string;
};
canvasWidth: number; // 逻辑像素宽度(已除以3
onHeaderDrawn?: (currentY: number) => void;
}
/**
* 绘制数学模块迷你页眉(尺寸除以3)
*/
export function drawBaseMiniHeader({
ctx,
options,
canvasWidth,
onHeaderDrawn,
}: drawBaseMiniHeaderParams): void {
const { appName, title } = options;
const titleY = 46;
const centerX = canvasWidth / 2;
// 字体大小除以3
ctx.font = 'bold 24px "Microsoft Yahei"'; // 64/3
ctx.fillStyle = '#000';
ctx.textAlign = 'center';
ctx.fillText(appName + ' ' + title, centerX, titleY);
// 调用回调函数,传递除以3后的currentY
if (onHeaderDrawn) {
onHeaderDrawn(60); // 约66.67
}
}
+28 -113
View File
@@ -1,8 +1,4 @@
import { PAPER_SIZE } from '../constants/colors';
import {
drawHeader as drawHeaderCommon,
drawMiniHeader as drawMiniHeaderCommon,
} from './headerDrawService';
import { BaseDrawService } from './baseDraw';
import { POSITION_TEMPLATES } from './findWordTemplate';
// ==================== Debug 开关 ====================
@@ -35,13 +31,11 @@ function selectTemplate(): { templateIndex: number } {
/**
* 从指定模板中获取位置
* @param templateIndex 模板索引
* @param centerX 中心X坐标
* @param centerY 中心Y坐标
* @param padding 边距
* @param canvasWidth 画布宽度
* @param canvasHeight 画布高度
* @param radius 字符圆半径
* @returns 位置数组
* @param centerX 中心X坐标(逻辑像素)
* @param centerY 中心Y坐标(逻辑像素)
* @returns 位置数组(逻辑像素)
*
* 注意:POSITION_TEMPLATES 中的坐标是基于原始像素的,需要转换为逻辑像素(除以3)
*/
function getPositionsFromTemplate(
templateIndex: number,
@@ -50,25 +44,18 @@ function getPositionsFromTemplate(
): Array<{ x: number; y: number }> {
const template = POSITION_TEMPLATES[templateIndex];
// 转换为绝对坐标并检查边界
// 转换为绝对坐标(模板坐标除以3转换为逻辑像素)
const positions: Array<{ x: number; y: number }> = [];
for (const pos of template) {
const x = centerX + pos.x;
const y = centerY + pos.y;
const x = centerX + pos.x / 3; // 转换为逻辑像素
const y = centerY + pos.y / 3; // 转换为逻辑像素
positions.push({ x, y });
}
return positions;
}
class FindWordDrawService {
headerType: PrintHeader = 'wechat';
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: Record<string, any>;
paperSize: PaperSize;
currentX: number;
currentY: number;
class FindWordDrawService extends BaseDrawService {
colors: string[];
characters: string[];
debug: boolean = false; // Debug模式开关
@@ -79,29 +66,15 @@ class FindWordDrawService {
options?: Record<string, any>,
) {
options = options || {};
this.canvas = canvas;
this.ctx = ctx;
this.paperSize = 'A4';
this.options = {
appName: '涂鸦丫小程序',
appHint: '识字|识图|练字|打印',
super(canvas, ctx, {
title: '找一找 涂 色',
subTitle: '找出相同的文字涂色',
...options,
};
});
// 从options中读取debug参数,如果没有则使用全局DEBUG常量
this.debug = options.debug === true || DEBUG;
this.currentX = 0;
this.currentY = 0;
this.colors = ['#000'];
this.characters = ['王'];
this.setPrintConfig();
}
setPrintConfig() {
const printConfig = getApp().getPrintConfig();
this.headerType = printConfig.header;
this.options.appName = printConfig.appName;
}
async draw(list: Array<{ color: string; word: string }>) {
@@ -111,6 +84,8 @@ class FindWordDrawService {
this.characters = list.map((item) => item.word || '日');
this.clear();
this.setPaper();
// 绘制Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
@@ -118,64 +93,30 @@ class FindWordDrawService {
}
// 找字模板没有 drawLegend 部分
this.drawDivider();
this.drawContent();
}
async drawHeader() {
this.currentX = 80;
this.currentY = 80;
await drawHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
headerType: this.headerType,
options: {
appName: this.options.appName || '涂鸦丫小程序',
appHint: this.options.appHint || '识字|识图|练字|打印',
title: this.options.title || '找一找 涂 色',
subTitle: this.options.subTitle || '找出相同的文字涂色',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
this.drawLine(this.currentY);
},
});
}
drawMiniHeader() {
drawMiniHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
options: {
appName: this.options.appName || '涂鸦丫小程序',
title: this.options.title || '找一找 涂 色',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
this.drawLine(this.currentY);
},
});
}
drawContent() {
const { canvas, ctx, characters, colors } = this;
const { ctx, characters, colors } = this;
if (characters.length <= 0) return;
// 第一个字作为中心大字
const firstChar = characters[0];
const firstColor = colors[0];
// 计算内容区域
const contentTop = this.currentY + 50;
const contentBottom = canvas.height - 80;
// 计算内容区域(逻辑像素)
const contentTop = this.currentY + 17; // 50/3≈17
const contentBottom = this.canvasHeight - 27; // 80/3≈27
const contentHeight = contentBottom - contentTop;
// 中心大字的参数
const centerX = canvas.width / 2;
const centerY = contentTop + contentHeight / 2; // 内容区域垂直居中
const centerX = this.canvasWidth / 2;
const centerY = contentTop + contentHeight / 2;
// 普通圆的参数(和textDrawService一致
const radius = 80;
const fontSize = 72;
// 普通圆的参数(逻辑像素
const radius = 27; // 80/3≈27
const fontSize = 24; // 72/3=24
// 先选择模板,获取模板的实际位置数量
const { templateIndex } = selectTemplate();
@@ -242,12 +183,12 @@ class FindWordDrawService {
// 绘制四周的字符
positions.forEach((pos, index) => {
const item = charsToDraw[index];
const y = pos.y; // 已经是绝对坐标,不需要再加contentTop
const y = pos.y; // 已经是绝对坐标
// 绘制圆
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#000';
ctx.lineWidth = 4;
ctx.lineWidth = 1; // 4/3≈1
ctx.beginPath();
ctx.arc(pos.x, y, radius, 0, Math.PI * 2);
ctx.fill();
@@ -265,35 +206,9 @@ class FindWordDrawService {
});
}
// drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容
drawLine(linY: number) {
const { canvas, ctx } = this;
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(80, linY);
ctx.lineTo(canvas.width - 80, linY);
ctx.stroke();
}
setPaper() {
const { ctx, canvas } = this;
const { pixelRatio: dpr } = wx.getWindowInfo();
let { width, height } = PAPER_SIZE[this.paperSize];
width = width * dpr;
height = height * dpr;
canvas.width = width;
canvas.height = height;
this.clear();
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, width, height);
}
clear() {
const canvas = this.canvas;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.drawDivider();
}
}
+35 -181
View File
@@ -1,69 +1,8 @@
import { PAPER_SIZE } from '../constants/colors';
import { BaseDrawService } from './baseDraw';
import { ShapeCard } from '../constants/shapes';
import { drawShape } from './drawShape';
import {
drawHeader as drawHeaderCommon,
drawMiniHeader as drawMiniHeaderCommon,
} from './headerDrawService';
/**
* 计算图形在画布上的位置,避免重叠
* @param canvasWidth 画布宽度
* @param canvasHeight 画布高度
* @param shapeCount 图形数量
* @param shapeSize 图形大小
* @returns 图形位置数组
*/
function calculateShapePositions(
canvasWidth: number,
canvasHeight: number,
shapeCount: number,
shapeSize: number,
) {
const positions = [];
const padding = 80;
const minSpacing = shapeSize * 1.5; // 最小间距为图形大小的1.5倍
const availableWidth = canvasWidth - 2 * padding;
const availableHeight = canvasHeight - 2 * padding;
// 计算网格布局
const cols = Math.ceil(Math.sqrt(shapeCount));
const rows = Math.ceil(shapeCount / cols);
const cellWidth = availableWidth / cols;
const cellHeight = availableHeight / rows;
for (let i = 0; i < shapeCount; i++) {
const row = Math.floor(i / cols);
const col = i % cols;
// 在单元格内随机位置
const x =
padding +
col * cellWidth +
(cellWidth - shapeSize) / 2 +
(Math.random() - 0.5) * (cellWidth - shapeSize) * 0.3;
const y =
padding +
row * cellHeight +
(cellHeight - shapeSize) / 2 +
(Math.random() - 0.5) * (cellHeight - shapeSize) * 0.3;
positions.push({ x, y });
}
return positions;
}
class ShapeDrawService {
headerType: PrintHeader;
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: Record<string, any>;
paperSize: PaperSize;
currentX: number;
currentY: number;
class ShapeDrawService extends BaseDrawService {
shapes: ShapeCard[];
constructor(
@@ -72,116 +11,61 @@ class ShapeDrawService {
options?: Record<string, any>,
) {
options = options || {};
this.canvas = canvas;
this.ctx = ctx;
this.paperSize = 'A4';
this.options = {
appName: '涂鸦丫小程序',
appHint: '涂色|识字|画画|打印',
super(canvas, ctx, {
title: '找一找 涂 色',
subTitle: '给图形涂上相同的颜色',
...options,
};
this.currentX = 0;
this.currentY = 0;
});
this.shapes = [];
this.headerType = 'wechat'; // 默认值
this.setPrintConfig();
}
setPrintConfig() {
const printConfig = getApp().getPrintConfig();
this.headerType = printConfig.header;
this.options.appName = printConfig.appName;
}
draw(shapes: ShapeCard[]) {
async draw(shapes: ShapeCard[]) {
this.setPrintConfig();
this.shapes = shapes.slice(0, 6); // 最多6个图形
this.clear();
this.setPaper();
// 绘制Header
if (this.headerType !== 'minimal') {
this.drawHeader();
await this.drawHeader();
} else {
this.drawMiniHeader();
await this.drawMiniHeader();
}
this.drawDivider();
this.drawLegend();
this.drawContent();
}
async drawHeader() {
this.currentX = 80;
this.currentY = 80;
await drawHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
headerType: this.headerType,
options: {
appName: this.options.appName || '涂鸦丫小程序',
appHint: this.options.appHint || '涂色|识字|画画|打印',
title: this.options.title || '找一找 涂 色',
subTitle: this.options.subTitle || '给图形涂上相同的颜色',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
this.drawLine(this.currentY);
},
});
}
drawMiniHeader() {
drawMiniHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
options: {
appName: this.options.appName || '涂鸦丫小程序',
title: this.options.title || '找一找 涂 色',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
this.drawLine(this.currentY);
},
});
}
drawLegend() {
const { canvas, ctx, shapes } = this;
const { ctx, shapes } = this;
if (shapes.length <= 0) return;
this.currentY = this.headerType === 'minimal' ? 200 : 304;
const shapeSize = 200;
const rectWidth = 180;
const rectHeight = 80;
// 固定图例的Y位置,不依赖shapeSize
const startY = this.currentY + 125; // 固定距离,不依赖shapeSize
// 逻辑像素尺寸(原始尺寸除以3
this.currentY = this.headerType === 'minimal' ? 75 : 110; // 200/3≈67, 304/3≈101
const shapeSize = 67; // 200/3≈67
const rectWidth = 60; // 180/3=60
const rectHeight = 27; // 80/3≈27
const startY = this.currentY + 42; // 125/3≈42
const len = shapes.length;
const canvasWidth = canvas.width;
// 计算示例图形的间距
const totalWidth = len * shapeSize + (len - 1) * 40;
const startX = (canvasWidth - totalWidth) / 2 + shapeSize / 2;
const totalWidth = len * shapeSize + (len - 1) * 13; // 40/3≈13
const startX = (this.canvasWidth - totalWidth) / 2 + shapeSize / 2;
// 绘制所有图形
shapes.forEach((shape: ShapeCard, index: number) => {
const x = startX + index * (shapeSize + 40);
const x = startX + index * (shapeSize + 13); // 40/3≈13
const y = startY;
// 绘制示例图形
drawShape(ctx, shape, x, y, shapeSize, shape.fillColor);
// // 绘制长方形
// ctx.fillStyle = '#fff';
// ctx.strokeStyle = '#000';
// ctx.lineWidth = 4;
const rectangleX = x - rectWidth / 2;
const rectangleY = y + shapeSize / 2 + 10;
// ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight);
const rectangleY = y + shapeSize / 2 + 3; // 10/3≈3
// 绘制图形名称
ctx.fillStyle = '#333';
ctx.font = 'bold 36px "Microsoft Yahei"';
ctx.font = 'bold 12px "Microsoft Yahei"'; // 36/3=12
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(
@@ -192,33 +76,29 @@ class ShapeDrawService {
);
});
this.currentY = this.headerType === 'minimal' ? 532 : 622;
this.drawLine(this.currentY);
this.currentY = this.headerType === 'minimal' ? 180 : 220; // 532/3≈177, 622/3≈207
this.drawDivider();
}
drawContent() {
const { canvas, ctx, shapes } = this;
const { ctx, shapes } = this;
if (shapes.length <= 0) return;
// 固定可渲染的总行数
// 逻辑像素尺寸(原始尺寸除以3
const ROW_COUNT = 6;
// 每行之间的垂直间距
const VERTICAL_GAP = 120;
// 图形大小
const shapeSize = 180;
// 左右边距
const leftMargin = 160;
const rightMargin = 160;
// 以 drawLegend 画完后的分割线作为基准,内容区顶部间距 50
const VERTICAL_GAP = 40; // 120/3=40
const shapeSize = 60; // 180/3=60
const leftMargin = 53; // 160/3≈53
const rightMargin = 53; // 160/3≈53
const legendBottomY = this.currentY;
const topGap = 60;
const topGap = 20; // 60/3=20
const contentTop = legendBottomY + topGap;
// 可用宽度
const contentWidth = canvas.width - leftMargin - rightMargin;
const contentWidth = this.canvasWidth - leftMargin - rightMargin;
// 每行最多能放多少个图形(考虑最小间距40
const minGap = 40;
// 每行最多能放多少个图形(考虑最小间距,逻辑像素
const minGap = 13; // 40/3≈13
const maxPerRow = Math.floor(
(contentWidth + minGap) / (shapeSize + minGap),
);
@@ -300,35 +180,9 @@ class ShapeDrawService {
});
}
// drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容
drawLine(linY: number) {
const { canvas, ctx } = this;
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(80, linY);
ctx.lineTo(canvas.width - 80, linY);
ctx.stroke();
}
setPaper() {
const { ctx, canvas } = this;
const { pixelRatio: dpr } = wx.getWindowInfo();
let { width, height } = PAPER_SIZE[this.paperSize];
width = width * dpr;
height = height * dpr;
canvas.width = width;
canvas.height = height;
this.clear();
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, width, height);
}
clear() {
const canvas = this.canvas;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.drawDivider();
}
}
+54 -136
View File
@@ -1,16 +1,10 @@
// import { PAPER_SIZE } from './constant';
import { PAPER_SIZE } from '../constants/colors';
import {
drawHeader as drawHeaderCommon,
drawMiniHeader as drawMiniHeaderCommon,
} from './headerDrawService';
import { BaseDrawService } from './baseDraw';
/**
* 计算示例区域圆的中心点
* @param canvasWidth 画布宽度
* @param canvasWidth 画布宽度(逻辑像素)
* @param circleCount 圆的数量
* @param padding 圆的间距
* @param radius 圆的半径
* @param padding 圆的间距(逻辑像素)
* @param radius 圆的半径(逻辑像素)
* @returns 圆的中心点
*/
function calculateCircleCenters(
@@ -24,7 +18,6 @@ function calculateCircleCenters(
const centers = [];
if (circleCount === 1) {
// 单个圆圈直接居中
centers.push(canvasWidth / 2);
} else {
const requiredSpace = 2 * radius * circleCount;
@@ -32,7 +25,6 @@ function calculateCircleCenters(
(availableWidth - requiredSpace) / (circleCount - 1);
if (spaceBetween > maxSpacing) {
// 超过最大间距时,固定间距并居中对齐
const totalWidth = 2 * radius + (circleCount - 1) * maxSpacing;
const startX = (canvasWidth - totalWidth) / 2 + radius;
@@ -40,7 +32,6 @@ function calculateCircleCenters(
centers.push(startX + i * maxSpacing);
}
} else {
// 正常均匀分布
const startX = padding + radius;
for (let i = 0; i < circleCount; i++) {
@@ -52,14 +43,7 @@ function calculateCircleCenters(
return centers;
}
class TextDrawService {
headerType: PrintHeader = 'wechat';
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: Record<string, any>;
paperSize: PaperSize;
currentX: number;
currentY: number;
class TextDrawService extends BaseDrawService {
colors: string[];
characters: string[];
@@ -69,27 +53,13 @@ class TextDrawService {
options?: Record<string, any>,
) {
options = options || {};
this.canvas = canvas;
this.ctx = ctx;
this.paperSize = 'A4';
this.options = {
appName: '涂鸦丫小程序',
appHint: '识字|识图|练字|打印',
super(canvas, ctx, {
title: '找一找 涂 色',
subTitle: '给文字涂上相同的颜色',
...options,
};
this.currentX = 0;
this.currentY = 0;
});
this.colors = ['#000'];
this.characters = ['王'];
this.setPrintConfig();
}
setPrintConfig() {
const printConfig = getApp().getPrintConfig();
this.headerType = printConfig.header;
this.options.appName = printConfig.appName;
}
async draw(list: Array<{ color: string; word: string }>) {
@@ -99,82 +69,58 @@ class TextDrawService {
this.characters = list.map((item) => item.word || '日');
this.clear();
this.setPaper();
// 绘制Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
this.drawMiniHeader();
await this.drawMiniHeader();
}
this.drawDivider();
this.drawLegend();
this.drawContent();
}
async drawHeader() {
this.currentX = 80;
this.currentY = 80;
await drawHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
headerType: this.headerType,
options: {
appName: this.options.appName || '涂鸦丫小程序',
appHint: this.options.appHint || '识字|识图|练字|打印',
title: this.options.title || '找一找 涂 色',
subTitle: this.options.subTitle || '给文字涂上相同的颜色',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
this.drawLine(this.currentY);
},
});
}
drawMiniHeader() {
drawMiniHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
options: {
appName: this.options.appName || '涂鸦丫小程序',
title: this.options.title || '找一找 涂 色',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
this.drawLine(this.currentY);
},
});
}
/** 绘制示例
* 矩形: x、y为左上角
* 圆形:x、y为圆心
* 文字:x textAlign 为 start,文本左边缘对齐
* y textBaseline 为 alphabeticy 对应字母基线(类似左下角)
*
* 注意:所有尺寸已转换为逻辑像素(除以3)
* */
drawLegend() {
const { canvas, ctx, colors, characters } = this;
const { ctx, colors, characters } = this;
if (characters.length <= 0) return;
// const { colors, characters } = options;
this.currentY = this.headerType === 'minimal' ? 200 : 304;
const radius = 65;
const rectWidth = 180;
const rectHeight = 80;
const startX = 260 + radius; // 圆形的X坐标是圆心
const startY = this.currentY + 30 + radius; // 圆形的Y坐标是圆心
// 逻辑像素尺寸(原始尺寸除以3
this.currentY = this.headerType === 'minimal' ? 68 : 110; // 200/3≈67, 304/3≈101
const radius = 22; // 65/3≈22
const rectWidth = 60; // 180/3=60
const rectHeight = 27; // 80/3≈27
const startX = 87 + radius; // (260+65)/3≈108
const startY = this.currentY + 10 + radius; // (30+65)/3≈32
const len = characters.length || 4;
const canvasWidth = canvas.width;
const centers = calculateCircleCenters(canvasWidth, len, 220, 65);
/** 计算每个圆之间的间距 俩个60,一个是页面右侧边距,另一个是圆离右边距地边距*/
const spaceWidth = len > 3 ? (canvasWidth - startX * 2) / (len - 1) : 0;
const centers = calculateCircleCenters(
this.canvasWidth,
len,
73,
radius,
); // 220/3≈73
/** 计算每个圆之间的间距 */
const spaceWidth =
len > 3 ? (this.canvasWidth - startX * 2) / (len - 1) : 0;
ctx.moveTo(startX, startY);
colors.forEach((color: string, index: number) => {
const x = centers[index] || startX + index * spaceWidth; // 计算圆的X坐标
const y = startY; // 计算圆的Y坐标
const x = centers[index] || startX + index * spaceWidth;
const y = startY;
// 绘制圆
ctx.strokeStyle = '#000';
ctx.fillStyle = color;
ctx.lineWidth = 4;
ctx.lineWidth = 1; // 4/3≈1
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
@@ -182,16 +128,16 @@ class TextDrawService {
ctx.closePath();
// 绘制长方形
ctx.fillStyle = '#fff'; // 长方形背景色
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#000';
ctx.lineWidth = 4;
const rectangleX = x - rectWidth / 2; // 从圆形移动一半的长方形的长
const rectangleY = y + radius + 43; // 从圆形下移一个半径,再加上43的间距
ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight); // 绘制长方形
ctx.lineWidth = 1; // 4/3≈1
const rectangleX = x - rectWidth / 2;
const rectangleY = y + radius + 14; // 43/3≈14
ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight);
// 绘制字符
ctx.fillStyle = '#333';
ctx.font = 'bold 48px "Microsoft Yahei"';
ctx.font = 'bold 16px "Microsoft Yahei"'; // 48/3=16
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(
@@ -202,26 +148,25 @@ class TextDrawService {
);
});
this.currentY = this.headerType === 'minimal' ? 522 : 612; // 计算分割线的Y坐标,距离示例图20px
this.drawLine(this.currentY);
this.currentY = this.headerType === 'minimal' ? 178 : 230; // 522/3=174, 612/3=204
this.drawDivider();
}
drawContent() {
const { canvas, ctx, characters } = this;
const { ctx, characters } = this;
if (characters.length <= 0) return;
const len = characters.length;
const rows = this.headerType === 'minimal' ? 9 : 8;
const radius = 80;
const fontSize = 72;
const radius = 27; // 80/3≈27
const fontSize = 24; // 72/3=24
const startY = this.currentY + 62 + radius;
const startX1 = 310 + radius;
const startX2 = 200 + radius;
const canvasWidth = canvas.width;
const spaceWidthFrist = (canvasWidth - startX1 * 2) / (5 - 1);
const spaceWidthSecond = (canvasWidth - startX2 * 2) / (6 - 1);
const spaceHeight = 54;
const startY = this.currentY + 10 + radius; // 62/3≈21
const startX1 = 103 + radius; // 310/3≈103
const startX2 = 67 + radius; // 200/3≈67
const spaceWidthFrist = (this.canvasWidth - startX1 * 2) / (5 - 1);
const spaceWidthSecond = (this.canvasWidth - startX2 * 2) / (6 - 1);
const spaceHeight = 18; // 54/3=18
ctx.moveTo(startX1, startY);
for (let i = 0; i < rows; i++) {
@@ -238,7 +183,7 @@ class TextDrawService {
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#000';
ctx.lineWidth = 4;
ctx.lineWidth = 1; // 4/3≈1
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
@@ -254,36 +199,9 @@ class TextDrawService {
}
}
// drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容
drawLine(linY: number) {
const { canvas, ctx } = this;
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(80, linY);
ctx.lineTo(canvas.width - 80, linY);
ctx.stroke();
}
setPaper() {
const { ctx, canvas } = this;
const { pixelRatio: dpr } = wx.getWindowInfo();
// const { width, height } = { width: 595 * dpr, height: 842 * dpr };
let { width, height } = PAPER_SIZE[this.paperSize];
width = width * dpr;
height = height * dpr;
canvas.width = width;
canvas.height = height;
this.clear();
ctx.fillStyle = '#fff'; // 设置背景色为白色
ctx.fillRect(0, 0, width, height);
}
clear() {
const canvas = this.canvas;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.drawDivider();
}
}
+42 -134
View File
@@ -1,9 +1,5 @@
import { PAPER_SIZE } from '../constants/colors';
import { BaseDrawService } from './baseDraw';
import { CharacterItem } from '../types/characterType';
import {
drawHeader as drawHeaderCommon,
drawMiniHeader as drawMiniHeaderCommon,
} from './headerDrawService';
/**
* 仅支持 M/L/Z 的简单 SVG 路径解析与绘制
@@ -197,14 +193,14 @@ function drawTianZiGrid({
lineColor = '#e0e0e0',
boldColor = '#cccccc',
}: DrawTianZiGridParams) {
// 外框
// 外框(逻辑像素)
ctx.strokeStyle = boldColor;
ctx.lineWidth = 2;
ctx.lineWidth = 1; // 2/3≈1,逻辑像素
ctx.strokeRect(x - size / 2, y - size / 2, size, size);
// 中线
// 中线(逻辑像素)
ctx.strokeStyle = lineColor;
ctx.lineWidth = 1;
ctx.lineWidth = 1; // 逻辑像素
ctx.beginPath();
// 竖线
ctx.moveTo(x, y - size / 2);
@@ -215,7 +211,7 @@ function drawTianZiGrid({
ctx.stroke();
ctx.closePath();
// 对角线(淡)
// 对角线(淡,逻辑像素
ctx.strokeStyle = '#eeeeee';
ctx.lineWidth = 1;
ctx.beginPath();
@@ -227,40 +223,18 @@ function drawTianZiGrid({
ctx.closePath();
}
class WordDrawService {
headerType: PrintHeader = 'wechat';
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: Record<string, any>;
paperSize: PaperSize;
currentX: number;
currentY: number;
class WordDrawService extends BaseDrawService {
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
options = options || {};
this.canvas = canvas;
this.ctx = ctx;
this.paperSize = 'A4';
this.options = {
appName: '涂鸦丫小程序',
appHint: '识字|识图|练字|打印',
super(canvas, ctx, {
title: '田字格 练 字 贴',
subTitle: '按笔画临摹练习',
...options,
};
this.currentX = 0;
this.currentY = 0;
this.setPrintConfig();
}
setPrintConfig() {
const printConfig = getApp().getPrintConfig();
this.headerType = printConfig.header;
this.options.appName = printConfig.appName;
});
}
/**
@@ -271,12 +245,14 @@ class WordDrawService {
this.clear();
this.setPaper();
// 等待页眉绘制完成,确保 this.currentY 被正确设置
// 绘制Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
await this.drawMiniHeader();
this.drawMiniHeader();
}
this.drawDivider();
this.drawContentEmpty();
}
@@ -301,60 +277,19 @@ class WordDrawService {
* 清空内容区域(页眉以下的部分)
*/
private clearContentArea() {
const { ctx, canvas } = this;
const { ctx } = this;
const contentStartY = this.currentY;
// 清空页眉以下的所有内容
// 清空页眉以下的所有内容(使用逻辑像素)
ctx.fillStyle = '#fff';
ctx.fillRect(
0,
contentStartY,
canvas.width,
canvas.height - contentStartY,
this.canvasWidth,
this.canvasHeight - contentStartY,
);
}
async drawHeader() {
this.currentX = 80;
this.currentY = 80;
await drawHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
headerType: this.headerType,
options: {
appName: this.options.appName || '涂鸦丫小程序',
appHint: this.options.appHint || '识字|识图|练字|打印',
title: this.options.title || '田字格 练 字 贴',
subTitle: this.options.subTitle || '按笔画临摹练习',
},
onHeaderDrawn: () => {
// wordDrawService 使用不同的计算方式
const titleY = 80;
const headerHeight = Math.max(titleY + 64 + 48 + 48) + 0; // 64px字体 + 48px间距 + 48px字体 + 20px边距
this.currentY = 80 + headerHeight;
this.drawDivider(this.currentY);
},
});
}
drawMiniHeader() {
drawMiniHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
options: {
appName: this.options.appName || '涂鸦丫小程序',
title: this.options.title || '田字格 练 字 贴',
},
onHeaderDrawn: () => {
// wordDrawService 使用不同的计算方式
const titleY = 120;
const miniHeaderHeight = titleY + 64 + 20; // 64px字体 + 20px边距
this.currentY = miniHeaderHeight;
this.drawDivider(this.currentY);
},
});
}
/**
* 绘制正文内容:两阶段绘制 - 先绘制空田字格,再绘制练字内容
*/
@@ -368,18 +303,18 @@ class WordDrawService {
* rowGap(行间距)用于田字格竖直方向(每一行田字格之间的间隔)的初始最小值。它防止行与行的田字格上下贴得太紧,增加一定的可视呼吸空间。之后同样会结合剩余空间动态调整为实际间距 actualRowGap。
*/
drawContent(characterData: CharacterItem[] | null) {
const { canvas, ctx } = this;
const { ctx } = this;
// 布局参数
const topGap = 50; // 与页眉分割线的距离
const leftMargin = 120;
const rightMargin = 120;
const bottomMargin = 120;
// 布局参数(逻辑像素,原始尺寸除以3
const topGap = 17; // 50/3≈17
const leftMargin = 40; // 120/3=40
const rightMargin = 40; // 120/3=40
const bottomMargin = 40; // 120/3=40
const contentTop = this.currentY + topGap;
const contentWidth = canvas.width - leftMargin - rightMargin;
const contentHeight = canvas.height - contentTop - bottomMargin;
const contentWidth = this.canvasWidth - leftMargin - rightMargin;
const contentHeight = this.canvasHeight - contentTop - bottomMargin;
const cellSize = 140;
const cellSize = 47; // 140/3≈47
// 统一通过 getMaxGridLayout 获取最大行列数
const { maxRow, maxCol } = this.getMaxGridLayout();
@@ -423,20 +358,18 @@ class WordDrawService {
* 获取当前页面可绘制田字格的最大行数与列数(与绘制使用同一套计算规则)
*/
getMaxGridLayout(): { maxRow: number; maxCol: number } {
const { canvas } = this;
// 布局参数需与 drawContent 保持一致
const topGap = 50;
const leftMargin = 120;
const rightMargin = 120;
const bottomMargin = 120;
// 布局参数需与 drawContent 保持一致(逻辑像素)
const topGap = 17; // 50/3≈17
const leftMargin = 40; // 120/3=40
const rightMargin = 40; // 120/3=40
const bottomMargin = 40; // 120/3=40
const contentTop = this.currentY + topGap;
const contentWidth = canvas.width - leftMargin - rightMargin;
const contentHeight = canvas.height - contentTop - bottomMargin;
const contentWidth = this.canvasWidth - leftMargin - rightMargin;
const contentHeight = this.canvasHeight - contentTop - bottomMargin;
const cellSize = 140;
const minGap = 24;
const rowGap = 36;
const cellSize = 47; // 140/3≈47
const minGap = 8; // 24/3=8
const rowGap = 12; // 36/3=12
const maxCol = Math.max(
1,
@@ -596,7 +529,7 @@ class WordDrawService {
uptoInclusive: strokes.length - 1,
fillStyle: 'rgb(0,0,0)', // 黑色填充
strokeStyle: 'rgb(0,0,0)', // 黑色描边
lineWidth: 4, // 较粗的线条
lineWidth: 1, // 较粗的线条4/3≈1,逻辑像素)
});
}
@@ -631,7 +564,7 @@ class WordDrawService {
// console.log(`练习格 [${rowIndex}, ${columnIndex}] 坐标: (${x}, ${y}), 笔画: ${strokeIndex + 1}/${strokes.length}`);
// 绘制到指定笔画的汉字(色,中等粗细)
// 绘制到指定笔画的汉字(色,中等粗细)
drawStrokes({
ctx,
strokes,
@@ -641,38 +574,13 @@ class WordDrawService {
uptoInclusive: strokeIndex,
fillStyle: '#ccc',
strokeStyle: '#ccc',
lineWidth: 3, // 中等粗细
lineWidth: 1, // 中等粗细3/3=1,逻辑像素)
});
}
drawDivider(linY: number) {
const { canvas, ctx } = this;
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(80, linY);
ctx.lineTo(canvas.width - 80, linY);
ctx.stroke();
}
setPaper() {
const { ctx, canvas } = this;
const { pixelRatio: dpr } = wx.getWindowInfo();
let { width, height } = PAPER_SIZE[this.paperSize];
width = width * dpr;
height = height * dpr;
canvas.width = width;
canvas.height = height;
this.clear();
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, width, height);
}
clear() {
const canvas = this.canvas;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
// drawDivider 已在基类中实现,此方法保留以保持兼容
drawDivider(linY?: number) {
super.drawDivider();
}
}