diff --git a/miniprogram/assets/imgs/doodle-footer.png b/miniprogram/assets/imgs/doodle-footer.png new file mode 100644 index 0000000..f82274a Binary files /dev/null and b/miniprogram/assets/imgs/doodle-footer.png differ diff --git a/miniprogram/assets/imgs/doodle-mini-code.jpg b/miniprogram/assets/imgs/doodle-mini-logo.jpg similarity index 100% rename from miniprogram/assets/imgs/doodle-mini-code.jpg rename to miniprogram/assets/imgs/doodle-mini-logo.jpg diff --git a/miniprogram/core/draw/backup/baseDraw.ts b/miniprogram/core/draw/backup/baseDraw.ts new file mode 100644 index 0000000..111f331 --- /dev/null +++ b/miniprogram/core/draw/backup/baseDraw.ts @@ -0,0 +1,531 @@ +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 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 } ] }