Files
doodle-mini/miniprogram/service/baseDraw.ts
T
2025-12-19 10:34:30 +08:00

385 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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间距
}
/**
* 绘制虚线分割线(逻辑像素,尺寸除以3)
* @param y 分割线的Y坐标
* @param margin 左右边距,默认为40
* @param color 线条颜色,默认为'#999'
* @param lineWidth 线条宽度,默认为1
*/
drawDashedDivider(
y: number,
margin: number = 40,
color: string = '#999',
lineWidth: number = 1,
) {
const { ctx, canvasWidth } = this;
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.setLineDash([4, 4]); // 虚线
ctx.beginPath();
ctx.moveTo(margin, y);
ctx.lineTo(canvasWidth - margin, y);
ctx.stroke();
ctx.setLineDash([]); // 重置为实线
}
/**
* 绘制网格线(逻辑像素,尺寸除以3)
* @param gridStartX 网格起始X坐标
* @param gridStartY 网格起始Y坐标
* @param cellWidth 每个格子的宽度
* @param cellHeight 每个格子的高度
* @param cols 列数
* @param rows 行数
*/
drawGridLines(
gridStartX: number,
gridStartY: number,
cellWidth: number,
cellHeight: number,
cols: number,
rows: number,
): void {
const { ctx } = this;
// 在函数内部计算网格总宽度和高度
const gridWidth = cellWidth * cols;
const gridHeight = cellHeight * rows;
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.setLineDash([]); // 实线
// 绘制垂直线
for (let i = 0; i <= cols; i++) {
const x = gridStartX + i * cellWidth;
ctx.beginPath();
ctx.moveTo(x, gridStartY);
ctx.lineTo(x, gridStartY + gridHeight);
ctx.stroke();
}
// 绘制水平线
for (let i = 0; i <= rows; i++) {
const y = gridStartY + i * cellHeight;
ctx.beginPath();
ctx.moveTo(gridStartX, y);
ctx.lineTo(gridStartX + gridWidth, y);
ctx.stroke();
}
}
/**
* 绘制圆角矩形框(逻辑像素,尺寸除以3)
* @param x 框的X坐标
* @param y 框的Y坐标
* @param width 框的宽度
* @param height 框的高度
* @param options 可选参数
* @param options.isDashed 是否为虚线,默认为false(实线)
* @param options.radius 圆角半径,默认为10
* @param options.color 线条颜色,默认为'#000'
* @param options.lineWidth 线条宽度,默认为1
*/
drawRoundedRect(
x: number,
y: number,
width: number,
height: number,
options?: {
isDashed?: boolean;
radius?: number;
color?: string;
lineWidth?: number;
},
): void {
const { ctx } = this;
const {
isDashed = false,
radius = 10,
color = '#000',
lineWidth = 1,
} = options || {};
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
// 设置虚线或实线
if (isDashed) {
ctx.setLineDash([4, 4]); // 虚线
} else {
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();
// 重置为实线(避免影响后续绘制)
ctx.setLineDash([]);
}
/**
* 绘制符号(使用路径绘制)
* @param x 符号中心X坐标
* @param y 符号中心Y坐标
* @param symbol 符号类型:'+', '-', '×', '✓'
* @param size 符号大小
*/
drawSymbol(x: number, y: number, symbol: string, size: number): void {
const { ctx } = this;
ctx.save();
ctx.translate(x, y);
const lineWidth = size * 0.15; // 线条宽度
const halfSize = size / 2;
const strokeLength = halfSize * 0.7; // 线条长度
ctx.strokeStyle = '#000';
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
switch (symbol) {
case '+':
// 加号:横线和竖线
ctx.beginPath();
// 横线
ctx.moveTo(-strokeLength, 0);
ctx.lineTo(strokeLength, 0);
// 竖线
ctx.moveTo(0, -strokeLength);
ctx.lineTo(0, strokeLength);
ctx.stroke();
break;
case '-':
// 减号:横线
ctx.beginPath();
ctx.moveTo(-strokeLength, 0);
ctx.lineTo(strokeLength, 0);
ctx.stroke();
break;
case '×':
// 乘号:两条斜线
ctx.beginPath();
// 左上到右下
ctx.moveTo(-strokeLength * 0.7, -strokeLength * 0.7);
ctx.lineTo(strokeLength * 0.7, strokeLength * 0.7);
// 右上到左下
ctx.moveTo(strokeLength * 0.7, -strokeLength * 0.7);
ctx.lineTo(-strokeLength * 0.7, strokeLength * 0.7);
ctx.stroke();
break;
case '✓':
// 对号:勾,整体更大,右侧的线更长
const checkScale = 1.2; // 对号整体放大1.2倍
ctx.beginPath();
const checkStartX = -strokeLength * 0.5 * checkScale;
const checkStartY = -strokeLength * 0.2 * checkScale;
const checkMidX = -strokeLength * 0.1 * checkScale;
const checkMidY = strokeLength * 0.3 * checkScale;
const checkEndX = strokeLength * 1.0 * checkScale; // 增加右侧长度
const checkEndY = -strokeLength * 0.4 * checkScale; // 稍微向上调整
ctx.moveTo(checkStartX, checkStartY);
ctx.lineTo(checkMidX, checkMidY);
ctx.lineTo(checkEndX, checkEndY);
ctx.stroke();
break;
default:
// 默认绘制加号
ctx.beginPath();
ctx.moveTo(-strokeLength, 0);
ctx.lineTo(strokeLength, 0);
ctx.moveTo(0, -strokeLength);
ctx.lineTo(0, strokeLength);
ctx.stroke();
}
ctx.restore();
}
/**
* 绘制圆点
*/
drawDot(
ctx: RenderingContext,
x: number,
y: number,
radius: number,
fillColor?: string,
) {
ctx.fillStyle = fillColor ?? '#93D333';
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
}