From e74fd3709cbf23c6206ac504cd5450cd0fd2bedb Mon Sep 17 00:00:00 2001 From: R524809 Date: Fri, 10 Apr 2026 15:06:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=98=E5=88=B6=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=B1=BB=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/assets/imgs/doodle-footer.png | Bin 0 -> 59479 bytes ...dle-mini-code.jpg => doodle-mini-logo.jpg} | Bin miniprogram/core/draw/backup/baseDraw.ts | 531 ++++++++++++++++++ .../core/draw/{ => backup}/baseHeaderDraw.ts | 0 miniprogram/core/draw/baseDraw.ts | 119 ++-- miniprogram/core/draw/drawHeader.ts | 111 ++++ miniprogram/pages/copyBook/copyBook.ts | 6 +- miniprogram/service/drawServiceFactory.ts | 13 + miniprogram/service/drawShape.ts | 348 +++++++++--- miniprogram/service/findWordDrawService.ts | 20 +- miniprogram/service/findWordTemplate.ts | 4 + miniprogram/service/headerDrawService.ts | 132 ----- miniprogram/service/shapeDrawService.ts | 25 +- miniprogram/service/textDrawService.ts | 23 +- miniprogram/service/wordDrawService.ts | 24 +- miniprogram/utils/index.ts | 2 +- project.private.config.json | 11 +- 17 files changed, 1091 insertions(+), 278 deletions(-) create mode 100644 miniprogram/assets/imgs/doodle-footer.png rename miniprogram/assets/imgs/{doodle-mini-code.jpg => doodle-mini-logo.jpg} (100%) create mode 100644 miniprogram/core/draw/backup/baseDraw.ts rename miniprogram/core/draw/{ => backup}/baseHeaderDraw.ts (100%) create mode 100644 miniprogram/core/draw/drawHeader.ts delete mode 100644 miniprogram/service/headerDrawService.ts diff --git a/miniprogram/assets/imgs/doodle-footer.png b/miniprogram/assets/imgs/doodle-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..f82274addcef7858798fe77a11adc63ae5311729 GIT binary patch literal 59479 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

g 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 + 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 x1 起点X坐标 + * @param y1 起点Y坐标 + * @param x2 终点X坐标 + * @param y2 终点Y坐标 + * @param options 可选参数 + * @param options.isDashed 是否为虚线,默认为true(虚线) + * @param options.dashPattern 虚线模式,默认为[4, 4] + * @param options.color 线条颜色,默认为'#999' + * @param options.lineWidth 线条宽度,默认为1 + */ + drawLine( + x1: number, + y1: number, + x2: number, + y2: number, + options?: { + isDashed?: boolean; + dashPattern?: number[]; + color?: string; + lineWidth?: number; + }, + ): void { + const { ctx } = this; + const { + isDashed = true, + dashPattern = [4, 4], + color = '#999', + lineWidth = 1, + } = options || {}; + + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + + // 设置虚线或实线 + if (isDashed) { + ctx.setLineDash(dashPattern); + } else { + ctx.setLineDash([]); + } + + // 绘制线条 + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + 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 '=': + // 等号:两条横线 + const equalsSpacing = size * 0.15; // 两条线之间的间距 + ctx.beginPath(); + // 上横线 + ctx.moveTo(-strokeLength, -equalsSpacing); + ctx.lineTo(strokeLength, -equalsSpacing); + // 下横线 + ctx.moveTo(-strokeLength, equalsSpacing); + ctx.lineTo(strokeLength, equalsSpacing); + 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(); + } + + /** + * 绘制圆点 + * @param ctx 绘制上下文 + * @param x 圆点中心X坐标 + * @param y 圆点中心Y坐标 + * @param radius 圆点半径 + * @param fillColor 填充颜色,默认为 '#93D333' + * @param strokeColor 边线颜色,如果传入则绘制边线,宽度为1,默认不绘制 + */ + drawDot( + ctx: RenderingContext, + x: number, + y: number, + radius: number, + fillColor?: string, + strokeColor?: string, + ) { + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + + // 绘制填充 + ctx.fillStyle = fillColor ?? '#93D333'; + ctx.fill(); + + // 绘制边线(如果提供了边线颜色) + if (strokeColor) { + ctx.strokeStyle = strokeColor; + ctx.lineWidth = 1; + 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(); + } + + /** + * 绘制空白方框(边框1px,#999,无填充,不显示数字) + */ + drawBox( + ctx: RenderingContext, + x: number, + y: number, + width: number, + height: number, + color: string = '#999', + lineWidth: number = 1, + ) { + // 绘制方框边框(1px,#999,无填充) + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.setLineDash([]); + ctx.strokeRect(x, y, width, height); + } + + /** + * 绘制正方形方框(调用drawBox,减少参数) + * @param ctx 渲染上下文 + * @param x 方框左上角x + * @param y 方框左上角y + * @param size 方框边长 + * @param color 边框颜色(可选,默认为#999) + * @param lineWidth 线宽(可选,默认为1) + */ + drawSquareBox( + ctx: RenderingContext, + x: number, + y: number, + size: number, + color: string = '#999', + lineWidth: number = 1, + ) { + this.drawBox(ctx, x, y, size, size, color, lineWidth); + } +} diff --git a/miniprogram/core/draw/baseHeaderDraw.ts b/miniprogram/core/draw/backup/baseHeaderDraw.ts similarity index 100% rename from miniprogram/core/draw/baseHeaderDraw.ts rename to miniprogram/core/draw/backup/baseHeaderDraw.ts diff --git a/miniprogram/core/draw/baseDraw.ts b/miniprogram/core/draw/baseDraw.ts index 111f331..7fbff1e 100644 --- a/miniprogram/core/draw/baseDraw.ts +++ b/miniprogram/core/draw/baseDraw.ts @@ -1,5 +1,6 @@ import { PAPER_SIZE } from '../../constants/colors'; -import { drawBaseHeader, drawBaseMiniHeader } from './baseHeaderDraw'; +import { getImage } from '../../utils/index'; +import drawHeader from './drawHeader'; /** * 基础绘制服务 @@ -8,12 +9,14 @@ import { drawBaseHeader, drawBaseMiniHeader } from './baseHeaderDraw'; * 提供功能: * - Canvas 初始化和配置 * - Paper 尺寸设置(支持 A4 等标准尺寸) - * - Header 绘制(支持完整 Header 和迷你 Header) + * - Header 绘制(统一打印纸页眉) * - 分割线绘制 * - 打印配置管理 */ +const LINE_COLOR = 'rgba(50, 46, 37, 0.4)'; +const LINE_WIDTH = 3; + export class BaseDrawService { - headerType: PrintHeader = 'wechat'; canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; options: Record; @@ -22,6 +25,8 @@ export class BaseDrawService { currentY: number; canvasWidth: number; // 逻辑像素宽度 canvasHeight: number; // 逻辑像素高度 + /** 紧跟在 drawHeader 之后的第一次 drawDivider 不画线(页眉已含底部分割线) */ + private _suppressNextDividerLine = false; constructor( canvas: Canvas, @@ -37,20 +42,18 @@ export class BaseDrawService { appName, appHint, title: '看数字,涂一涂', - subTitle: '找一找下面相同的数字,涂上颜色', - ...options, }; this.currentX = 0; this.currentY = 0; this.canvasWidth = 0; this.canvasHeight = 0; + this._suppressNextDividerLine = false; this.setPrintConfig(); } setPrintConfig() { const printConfig = getApp().getPrintConfig(); - this.headerType = printConfig.header; this.options.appName = printConfig.appName; } @@ -93,51 +96,95 @@ export class BaseDrawService { * 绘制Header(逻辑像素,尺寸除以3) */ async drawHeader() { - this.currentX = 25; - this.currentY = 25; - await drawBaseHeader({ + this.currentX = 24; + this.currentY = 14; + await drawHeader({ canvas: this.canvas, ctx: this.ctx, - headerType: this.headerType, + canvasWidth: this.canvasWidth, options: { - appName: this.options.appName || '涂鸦丫小程序', - appHint: this.options.appHint || '识字|识图|练字|打印', title: this.options.title || '看数字,涂一涂', - subTitle: - this.options.subTitle || '找一找下面相同的数字,涂上颜色', }, - onHeaderDrawn: (currentY) => { + onHeaderDrawn: (currentY: number) => { this.currentY = currentY; + this._suppressNextDividerLine = true; }, }); } /** - * 绘制迷你Header(逻辑像素,尺寸除以3) + * 页脚:顶部分割线 + 居中 `doodle-footer.png`(小鸭与「涂鸦丫」品牌图)。 + * 图片底边与纸张底边留白对齐,便于「探头」视觉。请在整页正文绘制完成后调用,避免被内容覆盖。 */ - 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; - }, - }); + async drawPrintFooter(): Promise { + const { ctx, canvasWidth, canvasHeight } = this; + const margin = 24; + const bottomMargin = 2; + const lineGapAboveImg = 10; + const maxImgW = canvasWidth - margin * 2; + const maxImgH = 40; + + let dw = maxImgW; + let dh = maxImgH; + + try { + const image = (await getImage( + this.canvas, + '/assets/imgs/doodle-footer.png', + )) as WechatMiniprogram.Image; + const nw = image.width || 2; + const nh = image.height || 1; + const aspect = nw / nh; + dh = Math.min(maxImgH, maxImgW / aspect); + dw = dh * aspect; + if (dw > maxImgW) { + dw = maxImgW; + dh = dw / aspect; + } + + const imgX = (canvasWidth - dw) / 2; + const imgY = canvasHeight - bottomMargin - dh; + const lineY = imgY - lineGapAboveImg; + + ctx.save(); + ctx.strokeStyle = LINE_COLOR; + ctx.lineWidth = LINE_WIDTH; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(margin, lineY); + ctx.lineTo(canvasWidth - margin, lineY); + ctx.stroke(); + ctx.restore(); + + ctx.drawImage(image, imgX, imgY, dw, dh); + } catch { + const lineY = + canvasHeight - bottomMargin - maxImgH - lineGapAboveImg; + ctx.save(); + ctx.strokeStyle = LINE_COLOR; + ctx.lineWidth = LINE_WIDTH; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(margin, lineY); + ctx.lineTo(canvasWidth - margin, lineY); + ctx.stroke(); + ctx.restore(); + } } /** * 绘制分割线(逻辑像素,尺寸除以3) */ drawDivider() { + if (this._suppressNextDividerLine) { + this._suppressNextDividerLine = false; + return; + } + const { ctx, canvasWidth } = this; const dividerY = this.currentY; - ctx.strokeStyle = '#000'; + ctx.strokeStyle = LINE_COLOR; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(24, dividerY); @@ -475,18 +522,10 @@ export class BaseDrawService { } /** - * 绘制 Header 和 Divider(公共绘制逻辑) - * 根据 headerType 自动选择绘制完整 Header 或迷你 Header,然后绘制分割线 + * 绘制统一页眉;紧随其后的 drawDivider 会跳过首条分割线(页眉已含底部灰线)。 */ async drawHeaderAndDivider() { - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域分割线 + await this.drawHeader(); this.drawDivider(); } diff --git a/miniprogram/core/draw/drawHeader.ts b/miniprogram/core/draw/drawHeader.ts new file mode 100644 index 0000000..0d6f01e --- /dev/null +++ b/miniprogram/core/draw/drawHeader.ts @@ -0,0 +1,111 @@ +import { getMiniCodeImage } from '../../utils/index'; + +/** + * 统一打印纸页眉(与 UI 稿一致:左上 Logo、居中主标题、姓名/日期/得分、底部分割线) + */ +interface DrawBaseHeaderParams { + canvas: WechatMiniprogram.Canvas; + ctx: RenderingContext; + canvasWidth: number; + options: { + /** 练习纸主标题,如「数一数,连一连」 */ + title: string; + }; + onHeaderDrawn?: (currentY: number) => void; +} + +const MARGIN_X = 24; +const LOGO_SIZE = 68; +const LOGO_X = MARGIN_X; +const LOGO_Y = 14; +const TITLE_FONT = 'bold 20px "Microsoft Yahei"'; +const META_FONT = '14px "Microsoft Yahei"'; +const META_FONT_PX = 12; +const META_UNDERLINE_GAP = 3; +const TITLE_COLOR = '#322E25'; +const LINE_COLOR = 'rgba(50, 46, 37, 0.4)'; +const LINE_WIDTH = 3; +const META_COLOR = '#7C766A'; +const META_ROW_MARGIN_X = 130; + +function drawMetaField( + ctx: RenderingContext, + label: string, + x: number, + y: number, + underlineRight: number, +) { + ctx.font = META_FONT; + ctx.fillStyle = META_COLOR; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText(label, x, y); + const metrics = ctx.measureText(label); + const labelW = metrics.width; + // textBaseline=top 时 y 是字顶,不是垂直中心;下划线应从字底开始(约一行字高,与 META_FONT 字号一致) + const lineY = y + META_FONT_PX + META_UNDERLINE_GAP; + ctx.strokeStyle = '#E5DCC9'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x + labelW + 2, lineY); + ctx.lineTo(Math.max(x + labelW + 2, underlineRight), lineY); + ctx.stroke(); +} + +export default async function drawHeader({ + canvas, + ctx, + canvasWidth, + options, + onHeaderDrawn, +}: DrawBaseHeaderParams): Promise { + const { title } = options; + + const image = await getMiniCodeImage(canvas); + ctx.drawImage(image, LOGO_X, LOGO_Y, LOGO_SIZE, LOGO_SIZE); + + ctx.font = TITLE_FONT; + ctx.fillStyle = TITLE_COLOR; + ctx.textAlign = 'center'; + // top:y 为文本顶边;勿用 alphabetic,否则 y 是基线,大字会大量画在 y 上方导致裁出纸上沿 + ctx.textBaseline = 'top'; + const titleY = 22; + ctx.fillText(title, canvasWidth / 2, titleY); + + const metaRowY = 65; + const innerLeft = META_ROW_MARGIN_X; + const innerRight = canvasWidth - META_ROW_MARGIN_X; + const innerW = innerRight - innerLeft; + const colW = innerW / 3; + + drawMetaField(ctx, '姓名:', innerLeft + 4, metaRowY, innerLeft + colW - 8); + drawMetaField( + ctx, + '日期:', + innerLeft + colW + 4, + metaRowY, + innerLeft + 2 * colW - 8, + ); + drawMetaField( + ctx, + '得分:', + innerLeft + 2 * colW + 4, + metaRowY, + innerRight - 4, + ); + + ctx.save(); + const separatorY = 96; + ctx.strokeStyle = LINE_COLOR; + ctx.lineWidth = LINE_WIDTH; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(MARGIN_X, separatorY); + ctx.lineTo(canvasWidth - MARGIN_X, separatorY); + ctx.stroke(); + ctx.restore(); + const contentStartY = separatorY + 14; + if (onHeaderDrawn) { + onHeaderDrawn(contentStartY); + } +} diff --git a/miniprogram/pages/copyBook/copyBook.ts b/miniprogram/pages/copyBook/copyBook.ts index c849e7f..074a814 100644 --- a/miniprogram/pages/copyBook/copyBook.ts +++ b/miniprogram/pages/copyBook/copyBook.ts @@ -362,7 +362,7 @@ Page({ const { words } = this.data as any; if (!words || words.length === 0) { - this.wordDrawService.drawContentEmpty(); + await this.wordDrawService.drawContentEmpty(); return; } @@ -370,7 +370,7 @@ Page({ const supportedWords = this.getSupportedWords(words); if (supportedWords.length === 0) { wx.showToast({ title: '暂不支持这些汉字', icon: 'none' }); - this.wordDrawService.drawContentEmpty(); + await this.wordDrawService.drawContentEmpty(); return; } @@ -391,7 +391,7 @@ Page({ maxRow, maxCol, ); - this.wordDrawService.drawPracticeContent(characterData); + await this.wordDrawService.drawPracticeContent(characterData); }, /** 检查汉字是否支持,返回支持的汉字列表 */ diff --git a/miniprogram/service/drawServiceFactory.ts b/miniprogram/service/drawServiceFactory.ts index 37c5492..bdb7920 100644 --- a/miniprogram/service/drawServiceFactory.ts +++ b/miniprogram/service/drawServiceFactory.ts @@ -1,3 +1,16 @@ +/** + * 识字绘制服务工厂 + * 根据模板类型创建对应的绘制服务实例 + * + * 支持的模板类型: + * - grid:网格模板 + * - find:找字模板 + * + * 支持的绘制服务: + * - TextDrawService:文字涂色服务 + * - FindWordDrawService:找字涂色服务 + */ + import TextDrawService from './textDrawService'; import FindWordDrawService from './findWordDrawService'; diff --git a/miniprogram/service/drawShape.ts b/miniprogram/service/drawShape.ts index 23e707c..9edac17 100644 --- a/miniprogram/service/drawShape.ts +++ b/miniprogram/service/drawShape.ts @@ -1,9 +1,36 @@ +/** + * 图形涂色方法 + * 根据图形类型绘制到Canvas + * + * 支持的图形类型: + * - circle:圆形 + * - ellipse:椭圆 + * - square:正方形 + * - rectangle:矩形 + * - triangle:三角形 + * - parallelogram:平行四边形 + * - diamond:菱形 + * - trapezoid:梯形 + * - pentagon:五边形 + * - hexagon:六边形 + * - pentagram:五角星 + * - heart:心形 + * - semicircle:半圆 + * - sector:扇形 + * - ring:圆环 + */ import { ShapeCard } from '../constants/shapes'; // 绘制各种图形的辅助函数 // 绘制圆形 -function drawCircle(ctx: RenderingContext, x: number, y: number, radius: number, fillColor: string) { +function drawCircle( + ctx: RenderingContext, + x: number, + y: number, + radius: number, + fillColor: string, +) { ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); if (fillColor !== 'transparent') { @@ -16,7 +43,14 @@ function drawCircle(ctx: RenderingContext, x: number, y: number, radius: number, } // 绘制椭圆形 -function drawEllipse(ctx: RenderingContext, x: number, y: number, rx: number, ry: number, fillColor: string) { +function drawEllipse( + ctx: RenderingContext, + x: number, + y: number, + rx: number, + ry: number, + fillColor: string, +) { ctx.beginPath(); ctx.ellipse(x, y, rx, ry, 0, 0, Math.PI * 2); if (fillColor !== 'transparent') { @@ -29,7 +63,13 @@ function drawEllipse(ctx: RenderingContext, x: number, y: number, rx: number, ry } // 绘制正方形 -function drawSquare(ctx: RenderingContext, x: number, y: number, size: number, fillColor: string) { +function drawSquare( + ctx: RenderingContext, + x: number, + y: number, + size: number, + fillColor: string, +) { if (fillColor !== 'transparent') { ctx.fillStyle = fillColor; ctx.fillRect(x - size / 2, y - size / 2, size, size); @@ -40,7 +80,14 @@ function drawSquare(ctx: RenderingContext, x: number, y: number, size: number, f } // 绘制矩形 -function drawRectangle(ctx: RenderingContext, x: number, y: number, width: number, height: number, fillColor: string) { +function drawRectangle( + ctx: RenderingContext, + x: number, + y: number, + width: number, + height: number, + fillColor: string, +) { if (fillColor !== 'transparent') { ctx.fillStyle = fillColor; ctx.fillRect(x - width / 2, y - height / 2, width, height); @@ -51,7 +98,14 @@ function drawRectangle(ctx: RenderingContext, x: number, y: number, width: numbe } // 绘制三角形(可指定类型:钝角、直角、锐角) -function drawTriangle(ctx: RenderingContext, x: number, y: number, size: number, type: string, fillColor: string) { +function drawTriangle( + ctx: RenderingContext, + x: number, + y: number, + size: number, + type: string, + fillColor: string, +) { ctx.beginPath(); switch (type) { @@ -84,7 +138,14 @@ function drawTriangle(ctx: RenderingContext, x: number, y: number, size: number, } // 绘制平行四边形 -function drawParallelogram(ctx: RenderingContext, x: number, y: number, width: number, height: number, fillColor: string) { +function drawParallelogram( + ctx: RenderingContext, + x: number, + y: number, + width: number, + height: number, + fillColor: string, +) { const offset = width * 0.2; ctx.beginPath(); ctx.moveTo(x - width / 2 + offset, y - height / 2); @@ -102,7 +163,13 @@ function drawParallelogram(ctx: RenderingContext, x: number, y: number, width: n } // 绘制菱形 -function drawDiamond(ctx: RenderingContext, x: number, y: number, size: number, fillColor: string) { +function drawDiamond( + ctx: RenderingContext, + x: number, + y: number, + size: number, + fillColor: string, +) { // 参考SVG: // 以(x, y)为中心,size为半高,按SVG比例映射四个点 // SVG中心(50,50),顶点(50,15),右(70,50),下(50,85),左(30,50) @@ -117,10 +184,10 @@ function drawDiamond(ctx: RenderingContext, x: number, y: number, size: number, const leftX = x - size * 0.4; ctx.beginPath(); - ctx.moveTo(x, topY); // 顶点 - ctx.lineTo(rightX, y); // 右 - ctx.lineTo(x, bottomY); // 下 - ctx.lineTo(leftX, y); // 左 + ctx.moveTo(x, topY); // 顶点 + ctx.lineTo(rightX, y); // 右 + ctx.lineTo(x, bottomY); // 下 + ctx.lineTo(leftX, y); // 左 ctx.closePath(); if (fillColor !== 'transparent') { ctx.fillStyle = fillColor; @@ -133,10 +200,17 @@ function drawDiamond(ctx: RenderingContext, x: number, y: number, size: number, // 绘制梯形 // 保证绘制的是等腰梯形:上下底居中,左右腰等长 -function drawTrapezoid(ctx: RenderingContext, x: number, y: number, width: number, height: number, fillColor: string) { +function drawTrapezoid( + ctx: RenderingContext, + x: number, + y: number, + width: number, + height: number, + fillColor: string, +) { // topWidth 为上底宽度,width 为下底宽度 const topWidth = width * 0.6; // 上底 - const bottomWidth = width; // 下底 + const bottomWidth = width; // 下底 const halfHeight = height / 2; // 上底中心点与下底中心点重合,左右对称 @@ -160,7 +234,14 @@ function drawTrapezoid(ctx: RenderingContext, x: number, y: number, width: numbe } // 绘制多边形 -function drawPolygon(ctx: RenderingContext, x: number, y: number, radius: number, sides: number, fillColor: string) { +function drawPolygon( + ctx: RenderingContext, + x: number, + y: number, + radius: number, + sides: number, + fillColor: string, +) { ctx.beginPath(); for (let i = 0; i < sides; i++) { const angle = (i * 2 * Math.PI) / sides - Math.PI / 2; @@ -181,7 +262,14 @@ function drawPolygon(ctx: RenderingContext, x: number, y: number, radius: number // 绘制星形(五角星等) // 按SVG 路径绘制五角星 -function drawStar(ctx: RenderingContext, x: number, y: number, radius: number, points: number, fillColor: string) { +function drawStar( + ctx: RenderingContext, + x: number, + y: number, + radius: number, + points: number, + fillColor: string, +) { // SVG原始点 const svgPoints = [ [50, 20], @@ -193,7 +281,7 @@ function drawStar(ctx: RenderingContext, x: number, y: number, radius: number, p [29, 85], [33, 60], [15, 44], - [39, 42] + [39, 42], ]; // SVG中心(50,50),最大半径约为35(从50,50到15,44的距离),SVG坐标范围大致为[15,85] // 归一化到以(x, y)为中心,radius为最大半径 @@ -202,8 +290,8 @@ function drawStar(ctx: RenderingContext, x: number, y: number, radius: number, p const svgRadius = 35; // 50-15=35 ctx.beginPath(); svgPoints.forEach(([px, py], idx) => { - const nx = x + (px - svgCenterX) / svgRadius * radius; - const ny = y + (py - svgCenterY) / svgRadius * radius; + const nx = x + ((px - svgCenterX) / svgRadius) * radius; + const ny = y + ((py - svgCenterY) / svgRadius) * radius; if (idx === 0) ctx.moveTo(nx, ny); else ctx.lineTo(nx, ny); }); @@ -218,7 +306,13 @@ function drawStar(ctx: RenderingContext, x: number, y: number, radius: number, p } // 绘制爱心 -function drawHeart(ctx: RenderingContext, x: number, y: number, size: number, fillColor: string) { +function drawHeart( + ctx: RenderingContext, + x: number, + y: number, + size: number, + fillColor: string, +) { // 放大爱心:将原本的缩放比例从60缩小为50,使爱心整体变大 // 以SVG路径为参考,原始SVG中心为(50,50),宽高约为60x60 // 这里size为整体缩放,x,y为中心点 @@ -226,33 +320,17 @@ function drawHeart(ctx: RenderingContext, x: number, y: number, size: number, fi // 归一化函数 const scale = 40; // 原来是60,改为50,放大 function tx(px: number) { - return x + (px - 50) / scale * size; + return x + ((px - 50) / scale) * size; } function ty(py: number) { - return y + (py - 50) / scale * size; + return y + ((py - 50) / scale) * size; } ctx.beginPath(); ctx.moveTo(tx(50), ty(35)); - ctx.bezierCurveTo( - tx(35), ty(20), - tx(20), ty(35), - tx(25), ty(50) - ); - ctx.bezierCurveTo( - tx(30), ty(65), - tx(50), ty(80), - tx(50), ty(80) - ); - ctx.bezierCurveTo( - tx(50), ty(80), - tx(70), ty(65), - tx(75), ty(50) - ); - ctx.bezierCurveTo( - tx(80), ty(35), - tx(65), ty(20), - tx(50), ty(35) - ); + ctx.bezierCurveTo(tx(35), ty(20), tx(20), ty(35), tx(25), ty(50)); + ctx.bezierCurveTo(tx(30), ty(65), tx(50), ty(80), tx(50), ty(80)); + ctx.bezierCurveTo(tx(50), ty(80), tx(70), ty(65), tx(75), ty(50)); + ctx.bezierCurveTo(tx(80), ty(35), tx(65), ty(20), tx(50), ty(35)); ctx.closePath(); if (fillColor !== 'transparent') { ctx.fillStyle = fillColor; @@ -264,7 +342,13 @@ function drawHeart(ctx: RenderingContext, x: number, y: number, size: number, fi } // 绘制半圆 -function drawSemicircle(ctx: RenderingContext, x: number, y: number, radius: number, fillColor: string) { +function drawSemicircle( + ctx: RenderingContext, + x: number, + y: number, + radius: number, + fillColor: string, +) { const verticalOffset = radius * 0.5; y = y + verticalOffset; ctx.beginPath(); @@ -282,14 +366,27 @@ function drawSemicircle(ctx: RenderingContext, x: number, y: number, radius: num } // 绘制扇形,圆弧在正上方 -function drawSector(ctx: RenderingContext, x: number, y: number, radius: number, fillColor: string) { +function drawSector( + ctx: RenderingContext, + x: number, + y: number, + radius: number, + fillColor: string, +) { // 为了让扇形垂直方向居中,需要将整个扇形向下平移一定距离 // 扇形的质心大约在半径的 0.6 倍处(120°扇形),这里经验值调整 const verticalOffset = radius * 0.5; ctx.beginPath(); ctx.moveTo(x, y + verticalOffset); // 扇形圆弧从左上(-135°)到右上(-45°),即从 -3/4π 到 -1/4π,圆弧在正上方 - ctx.arc(x, y + verticalOffset, radius, -3 * Math.PI / 4, -Math.PI / 4, false); + ctx.arc( + x, + y + verticalOffset, + radius, + (-3 * Math.PI) / 4, + -Math.PI / 4, + false, + ); ctx.closePath(); if (fillColor !== 'transparent') { ctx.fillStyle = fillColor; @@ -301,7 +398,13 @@ function drawSector(ctx: RenderingContext, x: number, y: number, radius: number, } // 绘制圆环(修正避免中间出现一条线) -function drawRing(ctx: RenderingContext, x: number, y: number, radius: number, fillColor: string) { +function drawRing( + ctx: RenderingContext, + x: number, + y: number, + radius: number, + fillColor: string, +) { const innerRadius = radius * 0.5; ctx.save(); ctx.beginPath(); @@ -330,7 +433,14 @@ function drawRing(ctx: RenderingContext, x: number, y: number, radius: number, f * @param size 图形大小 * @param fillColor 填充颜色 */ -export function drawShape(ctx: RenderingContext, shape: ShapeCard, x: number, y: number, size: number, fillColor: string) { +export function drawShape( + ctx: RenderingContext, + shape: ShapeCard, + x: number, + y: number, + size: number, + fillColor: string, +) { ctx.save(); ctx.translate(x, y); @@ -339,59 +449,171 @@ export function drawShape(ctx: RenderingContext, shape: ShapeCard, x: number, y: switch (shape.id) { case 'circle': - drawCircle(ctx, 0, 0, radius, shouldFill ? fillColor : 'transparent'); + drawCircle( + ctx, + 0, + 0, + radius, + shouldFill ? fillColor : 'transparent', + ); break; case 'ellipse': - drawEllipse(ctx, 0, 0, radius * 0.8, radius * 0.5, shouldFill ? fillColor : 'transparent'); + drawEllipse( + ctx, + 0, + 0, + radius * 0.8, + radius * 0.5, + shouldFill ? fillColor : 'transparent', + ); break; case 'square': - drawSquare(ctx, 0, 0, radius * 1.6, shouldFill ? fillColor : 'transparent'); + drawSquare( + ctx, + 0, + 0, + radius * 1.6, + shouldFill ? fillColor : 'transparent', + ); break; case 'rectangle': - drawRectangle(ctx, 0, 0, radius * 1.8, radius * 1.2, shouldFill ? fillColor : 'transparent'); + drawRectangle( + ctx, + 0, + 0, + radius * 1.8, + radius * 1.2, + shouldFill ? fillColor : 'transparent', + ); break; case 'obtuse-triangle': - drawTriangle(ctx, 0, 0, radius, 'obtuse', shouldFill ? fillColor : 'transparent'); + drawTriangle( + ctx, + 0, + 0, + radius, + 'obtuse', + shouldFill ? fillColor : 'transparent', + ); break; case 'right-triangle': - drawTriangle(ctx, 0, 0, radius, 'right', shouldFill ? fillColor : 'transparent'); + drawTriangle( + ctx, + 0, + 0, + radius, + 'right', + shouldFill ? fillColor : 'transparent', + ); break; case 'acute-triangle': - drawTriangle(ctx, 0, 0, radius, 'acute', shouldFill ? fillColor : 'transparent'); + drawTriangle( + ctx, + 0, + 0, + radius, + 'acute', + shouldFill ? fillColor : 'transparent', + ); break; case 'parallelogram': - drawParallelogram(ctx, 0, 0, radius * 1.6, radius * 1.2, shouldFill ? fillColor : 'transparent'); + drawParallelogram( + ctx, + 0, + 0, + radius * 1.6, + radius * 1.2, + shouldFill ? fillColor : 'transparent', + ); break; case 'diamond': - drawDiamond(ctx, 0, 0, radius, shouldFill ? fillColor : 'transparent'); + drawDiamond( + ctx, + 0, + 0, + radius, + shouldFill ? fillColor : 'transparent', + ); break; case 'trapezoid': - drawTrapezoid(ctx, 0, 0, radius * 1.6, radius * 1.2, shouldFill ? fillColor : 'transparent'); + drawTrapezoid( + ctx, + 0, + 0, + radius * 1.6, + radius * 1.2, + shouldFill ? fillColor : 'transparent', + ); break; case 'pentagon': - drawPolygon(ctx, 0, 0, radius, 5, shouldFill ? fillColor : 'transparent'); + drawPolygon( + ctx, + 0, + 0, + radius, + 5, + shouldFill ? fillColor : 'transparent', + ); break; case 'hexagon': - drawPolygon(ctx, 0, 0, radius, 6, shouldFill ? fillColor : 'transparent'); + drawPolygon( + ctx, + 0, + 0, + radius, + 6, + shouldFill ? fillColor : 'transparent', + ); break; case 'pentagram': - drawStar(ctx, 0, 0, radius, 5, shouldFill ? fillColor : 'transparent'); + drawStar( + ctx, + 0, + 0, + radius, + 5, + shouldFill ? fillColor : 'transparent', + ); break; case 'heart': - drawHeart(ctx, 0, 0, radius, shouldFill ? fillColor : 'transparent'); + drawHeart( + ctx, + 0, + 0, + radius, + shouldFill ? fillColor : 'transparent', + ); break; case 'semicircle': - drawSemicircle(ctx, 0, 0, radius, shouldFill ? fillColor : 'transparent'); + drawSemicircle( + ctx, + 0, + 0, + radius, + shouldFill ? fillColor : 'transparent', + ); break; case 'sector': - drawSector(ctx, 0, 0, radius, shouldFill ? fillColor : 'transparent'); + drawSector( + ctx, + 0, + 0, + radius, + shouldFill ? fillColor : 'transparent', + ); break; case 'ring': drawRing(ctx, 0, 0, radius, shouldFill ? fillColor : 'transparent'); break; default: // 默认绘制圆形 - drawCircle(ctx, 0, 0, radius, shouldFill ? fillColor : 'transparent'); + drawCircle( + ctx, + 0, + 0, + radius, + shouldFill ? fillColor : 'transparent', + ); } ctx.restore(); diff --git a/miniprogram/service/findWordDrawService.ts b/miniprogram/service/findWordDrawService.ts index c4944de..9bbea55 100644 --- a/miniprogram/service/findWordDrawService.ts +++ b/miniprogram/service/findWordDrawService.ts @@ -1,3 +1,15 @@ +/** + * 找字涂色服务 + * 根据模板类型绘制到Canvas + * + * 支持的模板类型: + * - grid:网格模板 + * - find:找字模板 + * + * 支持的绘制服务: + * - TextDrawService:文字涂色服务 + * - FindWordDrawService:找字涂色服务 + */ import { BaseDrawService } from '../core/draw/baseDraw'; import { POSITION_TEMPLATES } from './findWordTemplate'; @@ -85,16 +97,12 @@ class FindWordDrawService extends BaseDrawService { this.clear(); this.setPaper(); - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } + await this.drawHeader(); // 找字模板没有 drawLegend 部分 this.drawDivider(); this.drawContent(); + await this.drawPrintFooter(); } drawContent() { diff --git a/miniprogram/service/findWordTemplate.ts b/miniprogram/service/findWordTemplate.ts index 64c20de..ac6263e 100644 --- a/miniprogram/service/findWordTemplate.ts +++ b/miniprogram/service/findWordTemplate.ts @@ -1,3 +1,7 @@ +/** + * 找字模板 + * 包含多个模板,每个模板包含多个位置,每个位置包含x和y坐标 + */ export const POSITION_TEMPLATES: Array> = [ // 模板1:椭圆形分布-上下更密集 28个位置,相邻间距约100-150px [ diff --git a/miniprogram/service/headerDrawService.ts b/miniprogram/service/headerDrawService.ts deleted file mode 100644 index a57e203..0000000 --- a/miniprogram/service/headerDrawService.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { getMiniCodeImage, getImage } from '../utils/index'; - -/** - * 绘制页眉的参数接口 - */ -interface DrawHeaderParams { - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - headerType: PrintHeader; - options: { - appName: string; - appHint: string; - title: string; - subTitle: string; - }; - onHeaderDrawn?: (currentY: number) => void; // 绘制完成后的回调,用于设置 currentY 和绘制分割线 -} - -/** - * 绘制完整页眉(包含 Logo、应用名称、提示、标题、副标题) - */ -export async function drawHeader({ - canvas, - ctx, - headerType, - options, - onHeaderDrawn, -}: DrawHeaderParams): Promise { - const { appName, appHint, title, subTitle } = options; - - let titleX = 80 + 200 + 48; // currentX + logoWidth + spacing - const titleY = 80; - - const logoX = 80; - const logoY = 60; - const logoWidth = 200; - const logoHeight = 200; - - // 根据 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 = 120; - break; - } - case 'minimal': { - titleX = 120; - break; - } - default: { - const image = await getMiniCodeImage(canvas); - ctx.drawImage(image, logoX, logoY, logoWidth, logoHeight); - break; - } - } - - // 绘制应用名称 - ctx.font = 'bold 64px "Microsoft Yahei"'; - ctx.fillStyle = '#000'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - ctx.fillText(appName, titleX, titleY); - - // 绘制应用提示 - ctx.font = '48px "Microsoft Yahei"'; - ctx.fillStyle = '#666'; - ctx.fillText(appHint, titleX, 186); - - // 绘制标题 - ctx.font = 'bold 64px "Microsoft Yahei"'; - ctx.fillStyle = '#000'; - ctx.fillText(title, 1015, titleY); - - // 绘制副标题 - ctx.font = '48px "Microsoft Yahei"'; - ctx.fillStyle = '#666'; - ctx.fillText(subTitle, 1015, 186); - - // 调用回调函数,让调用者设置 currentY 并绘制分割线 - if (onHeaderDrawn) { - onHeaderDrawn(304); - } -} - -/** - * 绘制迷你页眉的参数接口 - */ -interface DrawMiniHeaderParams { - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - options: { - appName: string; - title: string; - }; - canvasWidth?: number; // 逻辑像素宽度(可选,如果提供则使用,否则从 canvas.width 计算) - onHeaderDrawn?: (currentY: number) => void; // 绘制完成后的回调,用于设置 currentY 和绘制分割线 -} - -/** - * 绘制迷你页眉(仅包含应用名称和标题,居中显示) - */ -export function drawMiniHeader({ - canvas, - ctx, - options, - canvasWidth, - onHeaderDrawn, -}: DrawMiniHeaderParams): void { - const { appName, title } = options; - const titleY = 120; - - // 如果提供了逻辑宽度,使用它;否则使用 canvas.width(假设 ctx 未 scale) - const centerX = - canvasWidth !== undefined ? canvasWidth / 2 : canvas.width / 2; - - ctx.font = 'bold 64px "Microsoft Yahei"'; - ctx.fillStyle = '#000'; - ctx.textAlign = 'center'; - ctx.fillText(appName + ' ' + title, centerX, titleY); - - // 调用回调函数,让调用者设置 currentY 并绘制分割线 - if (onHeaderDrawn) { - onHeaderDrawn(200); - } -} diff --git a/miniprogram/service/shapeDrawService.ts b/miniprogram/service/shapeDrawService.ts index eb5105e..bfe0ed6 100644 --- a/miniprogram/service/shapeDrawService.ts +++ b/miniprogram/service/shapeDrawService.ts @@ -1,3 +1,14 @@ +/** + * 图形涂色服务 + * 根据图形类型绘制到Canvas + * + * 支持的图形类型: + * - circle:圆形 + * - ellipse:椭圆 + * - square:正方形 + * - rectangle:矩形 + * - triangle:三角形 + */ import { BaseDrawService } from '../core/draw/baseDraw'; import { ShapeCard } from '../constants/shapes'; import { drawShape } from './drawShape'; @@ -25,12 +36,7 @@ class ShapeDrawService extends BaseDrawService { this.clear(); this.setPaper(); - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - await this.drawMiniHeader(); - } + await this.drawHeader(); this.drawDivider(); this.drawLegend(); this.drawContent(); @@ -40,12 +46,11 @@ class ShapeDrawService extends BaseDrawService { const { ctx, shapes } = this; if (shapes.length <= 0) return; - // 逻辑像素尺寸(原始尺寸除以3) - this.currentY = this.headerType === 'minimal' ? 75 : 110; // 200/3≈67, 304/3≈101 + const legendTopY = this.currentY; 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 startY = legendTopY + 42; // 125/3≈42 const len = shapes.length; // 计算示例图形的间距 @@ -76,7 +81,7 @@ class ShapeDrawService extends BaseDrawService { ); }); - this.currentY = this.headerType === 'minimal' ? 180 : 220; // 532/3≈177, 622/3≈207 + this.currentY = startY + shapeSize / 2 + 3 + rectHeight + 28; this.drawDivider(); } diff --git a/miniprogram/service/textDrawService.ts b/miniprogram/service/textDrawService.ts index 1f3058f..49700f9 100644 --- a/miniprogram/service/textDrawService.ts +++ b/miniprogram/service/textDrawService.ts @@ -1,3 +1,8 @@ +/** + * 找一找涂色,文字涂色服务 + * 根据文字列表绘制到Canvas + */ + import { BaseDrawService } from '../core/draw/baseDraw'; /** * 计算示例区域圆的中心点 @@ -70,16 +75,11 @@ class TextDrawService extends BaseDrawService { this.clear(); this.setPaper(); - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - await this.drawMiniHeader(); - } - + await this.drawHeader(); this.drawDivider(); this.drawLegend(); this.drawContent(); + await this.drawPrintFooter(); } /** 绘制示例 @@ -94,13 +94,12 @@ class TextDrawService extends BaseDrawService { const { ctx, colors, characters } = this; if (characters.length <= 0) return; - // 逻辑像素尺寸(原始尺寸除以3) - this.currentY = this.headerType === 'minimal' ? 68 : 110; // 200/3≈67, 304/3≈101 + const legendTopY = this.currentY; 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 startY = legendTopY + 10 + radius; // (30+65)/3≈32 const len = characters.length || 4; const centers = calculateCircleCenters( this.canvasWidth, @@ -148,7 +147,7 @@ class TextDrawService extends BaseDrawService { ); }); - this.currentY = this.headerType === 'minimal' ? 178 : 230; // 522/3=174, 612/3=204 + this.currentY = startY + radius + 14 + rectHeight + 24; this.drawDivider(); } @@ -157,7 +156,7 @@ class TextDrawService extends BaseDrawService { if (characters.length <= 0) return; const len = characters.length; - const rows = this.headerType === 'minimal' ? 9 : 8; + const rows = 8; const radius = 27; // 80/3≈27 const fontSize = 24; // 72/3=24 diff --git a/miniprogram/service/wordDrawService.ts b/miniprogram/service/wordDrawService.ts index d0cc874..c79ee50 100644 --- a/miniprogram/service/wordDrawService.ts +++ b/miniprogram/service/wordDrawService.ts @@ -1,3 +1,13 @@ +/** + * 田字格练字服务 + * 根据汉字列表绘制到Canvas + * + * 支持的汉字类型: + * - character:汉字 + * - number:数字 + * - letter:字母 + * - symbol:符号 + */ import { BaseDrawService } from '../core/draw/baseDraw'; import { CharacterItem } from '../types/characterType'; @@ -255,32 +265,28 @@ class WordDrawService extends BaseDrawService { this.clear(); this.setPaper(); - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - + await this.drawHeader(); this.drawDivider(); - this.drawContentEmpty(); + await this.drawContentEmpty(); } async drawContentEmpty() { this.clearContentArea(); this.drawContent(null); + await this.drawPrintFooter(); } /** * 绘制田字格和练字内容 * @param wordsMap 形如 { "其": ["M.. L.. Z", ...], ... },不超过 10 个汉字 */ - drawPracticeContent(characters: CharacterItem[]) { + async drawPracticeContent(characters: CharacterItem[]) { // 清空内容区域(页眉以下的部分) this.clearContentArea(); // 绘制田字格和练字内容 this.drawContent(characters); + await this.drawPrintFooter(); } /** diff --git a/miniprogram/utils/index.ts b/miniprogram/utils/index.ts index 9a9abaa..41754bf 100644 --- a/miniprogram/utils/index.ts +++ b/miniprogram/utils/index.ts @@ -5,7 +5,7 @@ export * from './http'; export * from './getWordsSvgJson'; export async function getMiniCodeImage(canvas: Canvas) { - return getImage(canvas, '/assets/imgs/doodle-mini-code.jpg'); + return getImage(canvas, '/assets/imgs/doodle-mini-logo.jpg'); } export async function getImage(canvas: Canvas, path: string) { diff --git a/project.private.config.json b/project.private.config.json index d9b5798..e1e715b 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -23,12 +23,19 @@ "condition": { "miniprogram": { "list": [ + { + "name": "pages/index/index", + "pathName": "pages/index/index", + "query": "", + "scene": null, + "launchMode": "default" + }, { "name": "englishPages/letterTracing/letterTracing", "pathName": "englishPages/letterTracing/letterTracing", "query": "id=letter-tracing-single", - "scene": null, - "launchMode": "default" + "launchMode": "default", + "scene": null } ] }