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; paperSize: PaperSize; currentX: number; currentY: number; canvasWidth: number; // 逻辑像素宽度 canvasHeight: number; // 逻辑像素高度 constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { 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(); } }