diff --git a/miniprogram/pages/index/index.less b/miniprogram/pages/index/index.less index 0a7a67a..b8cc042 100644 --- a/miniprogram/pages/index/index.less +++ b/miniprogram/pages/index/index.less @@ -56,7 +56,7 @@ page { } // 模板选择区域 - /* .template-section { + .template-section { margin-top: 40rpx; .template-title { @@ -67,9 +67,9 @@ page { text-align: center; } - .template-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); + .template-list { + display: flex; + flex-direction: column; gap: 20rpx; } @@ -77,156 +77,60 @@ page { background: #FFFFFF; border: 2rpx solid #E8E8E8; border-radius: 16rpx; - padding: 16rpx; + padding: 24rpx; cursor: pointer; transition: all 0.2s ease-in-out; - position: relative; + display: flex; + align-items: center; + width: 100%; + box-sizing: border-box; &:active { - transform: scale(0.95); + transform: scale(0.98); } &.active { border-color: #93D333; box-shadow: 0 0 0 4rpx rgba(147, 211, 51, 0.2); + } - &::after { - content: '✓'; - position: absolute; - top: 12rpx; - right: 12rpx; - width: 24rpx; - height: 24rpx; - background: #93D333; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; + .template-checkbox { + width: 40rpx; + height: 40rpx; + border: 2rpx solid #E8E8E8; + border-radius: 8rpx; + display: flex; + align-items: center; + justify-content: center; + margin-right: 24rpx; + flex-shrink: 0; + transition: all 0.2s ease-in-out; + + .checkbox-icon { color: #FFFFFF; - font-size: 14rpx; + font-size: 24rpx; font-weight: bold; } } - .template-preview { - width: 100%; - height: 80rpx; - background: #F8F8F8; - border-radius: 12rpx; - margin-bottom: 12rpx; - display: flex; - align-items: center; - justify-content: center; - position: relative; - overflow: hidden; - - .template-preview-content { - display: flex; - align-items: center; - justify-content: center; - gap: 6rpx; - width: 100%; - height: 100%; - - .preview-word { - font-size: 20rpx; - font-weight: 600; - width: 24rpx; - height: 24rpx; - display: flex; - align-items: center; - justify-content: center; - border-radius: 6rpx; - background: rgba(255, 255, 255, 0.8); - } - - // 不同模板的布局样式 - &.grid-template { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4rpx; - } - - &.circle-template { - position: relative; - - .preview-word { - position: absolute; - - &:nth-child(1) { - top: 12rpx; - left: 50%; - transform: translateX(-50%); - } - - &:nth-child(2) { - right: 12rpx; - top: 50%; - transform: translateY(-50%); - } - - &:nth-child(3) { - bottom: 12rpx; - left: 50%; - transform: translateX(-50%); - } - - &:nth-child(4) { - left: 12rpx; - top: 50%; - transform: translateY(-50%); - } - } - } - - &.random-template { - position: relative; - - .preview-word { - position: absolute; - - &:nth-child(1) { - top: 10rpx; - left: 16rpx; - } - - &:nth-child(2) { - top: 24rpx; - right: 10rpx; - } - - &:nth-child(3) { - bottom: 18rpx; - left: 10rpx; - } - - &:nth-child(4) { - bottom: 10rpx; - right: 18rpx; - } - } - } - - &.line-template { - flex-direction: row; - gap: 8rpx; - } - } + &.active .template-checkbox { + background: #93D333; + border-color: #93D333; } .template-name { - font-size: 22rpx; - color: #666666; - text-align: center; + font-size: 28rpx; + color: #141414; font-weight: 500; + flex: 1; + } - .template-item.active & { - color: #93D333; - font-weight: 600; - } + &.active .template-name { + color: #93D333; + font-weight: 600; } } - } */ + } } diff --git a/miniprogram/pages/index/index.ts b/miniprogram/pages/index/index.ts index c1585d5..80be3fc 100644 --- a/miniprogram/pages/index/index.ts +++ b/miniprogram/pages/index/index.ts @@ -1,6 +1,10 @@ import { WATER_COLORS, PAPER_SIZE } from '../../constants/colors'; import { WORDS } from '../../constants/words'; -import TextDrawService from '../../service/textDrawService'; +import { + DrawServiceFactory, + IDrawService, + TemplateType, +} from '../../service/drawServiceFactory'; import { checkAndSaveImage } from '../../utils/saveImage'; Page({ @@ -8,7 +12,7 @@ Page({ ctx: null as RenderingContext | null, boxHeight: 0, boxWidth: 0, - textDrawService: null as TextDrawService | null, + drawService: null as IDrawService | null, data: { cardList: [] as CardList, @@ -18,7 +22,7 @@ Page({ currentColor: '', showIntroductionPopup: false, showSelectWordPopup: false, // 选择文字弹窗 - selectedTemplate: 'grid', // 选中的模板类型 + selectedTemplate: 'find', // 选中的模板类型 env: 'release', isPCDevtool: false, }, @@ -36,21 +40,24 @@ Page({ } if (!hasShowIntroduction) { - this.setData({ - cardList: [ - { color: '#FF0000', word: '涂' }, - { color: '#00FF00', word: '鸦' }, - { color: '#FF7F00', word: '丫' }, - { color: '#00FFFF', word: '欢' }, - { color: '#FFFF00', word: '迎' }, - { color: '#8A2BE2', word: '您' }, - ], - selectedWords: ['涂', '鸦', '丫', '欢', '迎', '您'], - showIntroductionPopup: true, - env: getApp().globalData.env || 'release', - }, () => { - this.drawCanvas(); - }); + this.setData( + { + cardList: [ + { color: '#FF0000', word: '涂' }, + { color: '#00FF00', word: '鸦' }, + { color: '#FF7F00', word: '丫' }, + { color: '#00FFFF', word: '欢' }, + { color: '#FFFF00', word: '迎' }, + { color: '#8A2BE2', word: '您' }, + ], + selectedWords: ['涂', '鸦', '丫', '欢', '迎', '您'], + showIntroductionPopup: true, + env: getApp().globalData.env || 'release', + }, + () => { + this.drawCanvas(); + }, + ); } else { this.setData({ env: getApp().globalData.env || 'release', @@ -88,13 +95,25 @@ Page({ if (ctx) { this.canvas = canvas; this.ctx = ctx; - this.textDrawService = new TextDrawService(canvas, ctx); + // 根据当前选中的模板类型创建对应的绘制服务 + const templateType = this.data + .selectedTemplate as TemplateType; + this.drawService = + DrawServiceFactory.create( + templateType, + canvas, + ctx, + ); this.setData({ boxWidth, boxHeight }); // 初始化完成后,如果有cardList内容则立即绘制 const currentCardList = this.data.cardList; if (currentCardList.length > 0) { - this.textDrawService.draw(currentCardList); + this.drawService + .draw(currentCardList) + .catch((err) => { + console.error('绘制失败:', err); + }); } } } @@ -104,35 +123,63 @@ Page({ .exec(); }, + /** + * 创建或更新绘制服务 + * 根据当前选中的模板类型创建对应的服务实例 + */ + createDrawService() { + if (this.canvas && this.ctx) { + const templateType = this.data.selectedTemplate as TemplateType; + this.drawService = DrawServiceFactory.create( + templateType, + this.canvas, + this.ctx, + ); + } + }, + /** 变更cardList 并更新canvas */ drawCanvas( updatedCardList?: Array<{ color: string; word: string }>, - callback: () => void = () => { }, + callback: () => void = () => {}, ) { if (updatedCardList) { - this.setData({ - cardList: updatedCardList, - selectedWords: updatedCardList.map((item) => item.word) - }, () => { - // 如果cardList有内容且canvas未初始化,先初始化canvas - if (!this.canvas) { - this.initCanvas(); - } - // 如果canvas已初始化且有内容,则绘制 - if (this.textDrawService && updatedCardList!.length > 0) { - this.textDrawService.draw(updatedCardList!); - } - callback(); - }); + this.setData( + { + cardList: updatedCardList, + selectedWords: updatedCardList.map((item) => item.word), + }, + () => { + // 如果cardList有内容且canvas未初始化,先初始化canvas + if (!this.canvas) { + this.initCanvas(); + } else { + // 如果canvas已初始化,确保绘制服务是最新的 + this.createDrawService(); + } + // 如果canvas已初始化且有内容,则绘制 + if (this.drawService && updatedCardList!.length > 0) { + this.drawService.draw(updatedCardList!).catch((err) => { + console.error('绘制失败:', err); + }); + } + callback(); + }, + ); } else { const currentCardList = this.data.cardList; // 如果cardList有内容且canvas未初始化,先初始化canvas if (!this.canvas) { this.initCanvas(); + } else { + // 如果canvas已初始化,确保绘制服务是最新的 + this.createDrawService(); } // 如果canvas已初始化且有内容,则绘制 - if (this.textDrawService && currentCardList.length > 0) { - this.textDrawService.draw(currentCardList); + if (this.drawService && currentCardList.length > 0) { + this.drawService.draw(currentCardList).catch((err) => { + console.error('绘制失败:', err); + }); } } }, @@ -199,7 +246,9 @@ Page({ color = colorHex; } else { // 否则随机选择一个可用颜色 - const randomIndex = Math.floor(Math.random() * shuffledColors.length); + const randomIndex = Math.floor( + Math.random() * shuffledColors.length, + ); color = shuffledColors[randomIndex]; // 从可用颜色中移除已使用的颜色 shuffledColors.splice(randomIndex, 1); @@ -207,7 +256,7 @@ Page({ return { word: char, - color: color + color: color, }; }); @@ -291,7 +340,7 @@ Page({ // 清空cardList时,重置canvas相关引用,因为DOM元素会被移除 this.canvas = null; this.ctx = null; - this.textDrawService = null; + this.drawService = null; this.drawCanvas([]); }, @@ -305,7 +354,8 @@ Page({ // 随机获取6个不重复的文字 const selectedWords: string[] = []; while (selectedWords.length < 6) { - const randomWord = allWords[Math.floor(Math.random() * allWords.length)]; + const randomWord = + allWords[Math.floor(Math.random() * allWords.length)]; if (!selectedWords.includes(randomWord)) { selectedWords.push(randomWord); } @@ -323,7 +373,7 @@ Page({ // 组合文字和颜色 const cardList = selectedWords.map((word, index) => ({ color: selectedColors[index], - word + word, })); this.drawCanvas(cardList); }, @@ -342,17 +392,17 @@ Page({ // 获取颜色字对应的16进制颜色值 getColorByWord(word: string): string | null { const colorMap: Record = { - '红': '#FF0000', - '蓝': '#0000FF', - '绿': '#00FF00', - '黄': '#FFFF00', - '黑': '#000000', - '白': '#FFFFFF', - '紫': '#800080', - '橙': '#FF7F00', - '粉': '#FF69B4', - '棕': '#A52A2A', - '灰': '#808080' + 红: '#FF0000', + 蓝: '#0000FF', + 绿: '#00FF00', + 黄: '#FFFF00', + 黑: '#000000', + 白: '#FFFFFF', + 紫: '#800080', + 橙: '#FF7F00', + 粉: '#FF69B4', + 棕: '#A52A2A', + 灰: '#808080', }; return colorMap[word] || null; }, @@ -365,7 +415,7 @@ Page({ const colorHex = this.getColorByWord(word); return { color: colorHex || colors[index].hex, - word + word, }; }); @@ -382,16 +432,20 @@ Page({ // 模板选择事件 onSelectTemplate(e: WechatMiniprogram.TouchEvent) { const { template } = e.currentTarget.dataset; - this.setData({ - selectedTemplate: template - }); - - // 这里后续可以添加根据模板类型更新canvas绘制效果的逻辑 - console.log('选择的模板类型:', template); - - // 如果有文字卡片,可以重新绘制canvas - if (this.data.cardList.length > 0) { - this.drawCanvas(); - } + this.setData( + { + selectedTemplate: template, + }, + () => { + // 模板切换后,重新创建绘制服务并重新绘制 + if (this.canvas && this.ctx) { + this.createDrawService(); + // 如果有文字卡片,重新绘制canvas + if (this.data.cardList.length > 0) { + this.drawCanvas(); + } + } + }, + ); }, }); diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index f7ad805..f9685f3 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -51,99 +51,37 @@ - + 预览打印效果 diff --git a/miniprogram/service/drawServiceFactory.ts b/miniprogram/service/drawServiceFactory.ts new file mode 100644 index 0000000..37c5492 --- /dev/null +++ b/miniprogram/service/drawServiceFactory.ts @@ -0,0 +1,47 @@ +import TextDrawService from './textDrawService'; +import FindWordDrawService from './findWordDrawService'; + +/** + * 绘制服务接口 + */ +export interface IDrawService { + draw(list: Array<{ color: string; word: string }>): Promise; + clear(): void; + setPaper(): void; +} + +/** + * 模板类型 + */ +export type TemplateType = 'grid' | 'find'; + +/** + * 绘制服务工厂类 + * 根据模板类型创建对应的绘制服务实例 + */ +export class DrawServiceFactory { + /** + * 创建绘制服务实例 + * @param templateType 模板类型 + * @param canvas Canvas 对象 + * @param ctx 渲染上下文 + * @param options 可选配置 + * @returns 绘制服务实例 + */ + static create( + templateType: TemplateType, + canvas: Canvas, + ctx: RenderingContext, + options?: Record, + ): IDrawService { + switch (templateType) { + case 'grid': + return new TextDrawService(canvas, ctx, options); + case 'find': + return new FindWordDrawService(canvas, ctx, options); + default: + // 默认使用网格模板 + return new TextDrawService(canvas, ctx, options); + } + } +} diff --git a/miniprogram/service/findWordDrawService.ts b/miniprogram/service/findWordDrawService.ts new file mode 100644 index 0000000..6db3159 --- /dev/null +++ b/miniprogram/service/findWordDrawService.ts @@ -0,0 +1,300 @@ +import { PAPER_SIZE } from '../constants/colors'; +import { + drawHeader as drawHeaderCommon, + drawMiniHeader as drawMiniHeaderCommon, +} from './headerDrawService'; +import { POSITION_TEMPLATES } from './findWordTemplate'; + +// ==================== Debug 开关 ==================== +// 设置为 true 时,其他文字会显示坐标系的索引值(第几个位置),方便手动调整坐标系 +const DEBUG = false; +// ==================================================== + +/** + * 从固定模板中获取位置 + * @param centerX 中心X坐标 + * @param centerY 中心Y坐标 + * @param count 需要的位置数量 + * @param padding 边距 + * @param canvasWidth 画布宽度 + * @param canvasHeight 画布高度 + * @param radius 字符圆半径 + * @returns 位置数组 + */ +/** + * 随机选择一个模板并返回模板的实际位置数量 + * @returns 模板索引和模板的实际位置数量 + */ +function selectTemplate(): { templateIndex: number } { + const templateIndex = Math.floor(Math.random() * POSITION_TEMPLATES.length); + return { + templateIndex, + }; +} + +/** + * 从指定模板中获取位置 + * @param templateIndex 模板索引 + * @param centerX 中心X坐标 + * @param centerY 中心Y坐标 + * @param padding 边距 + * @param canvasWidth 画布宽度 + * @param canvasHeight 画布高度 + * @param radius 字符圆半径 + * @returns 位置数组 + */ +function getPositionsFromTemplate( + templateIndex: number, + centerX: number, + centerY: number, +): Array<{ x: number; y: number }> { + const template = POSITION_TEMPLATES[templateIndex]; + + // 转换为绝对坐标并检查边界 + const positions: Array<{ x: number; y: number }> = []; + for (const pos of template) { + const x = centerX + pos.x; + const y = centerY + pos.y; + + positions.push({ x, y }); + } + return positions; +} + +class FindWordDrawService { + headerType: PrintHeader = 'wechat'; + canvas: WechatMiniprogram.Canvas; + ctx: RenderingContext; + options: Record; + paperSize: PaperSize; + currentX: number; + currentY: number; + colors: string[]; + characters: string[]; + debug: boolean = false; // Debug模式开关 + + constructor( + canvas: Canvas, + ctx: RenderingContext, + options?: Record, + ) { + options = options || {}; + this.canvas = canvas; + this.ctx = ctx; + this.paperSize = 'A4'; + this.options = { + appName: '涂鸦丫小程序', + appHint: '识字|识图|练字|打印', + title: '找一找 涂 色', + subTitle: '给文字涂上相同的颜色', + ...options, + }; + // 从options中读取debug参数,如果没有则使用全局DEBUG常量 + this.debug = options.debug === true || DEBUG; + this.currentX = 0; + this.currentY = 0; + this.colors = ['#000']; + this.characters = ['王']; + this.setPrintConfig(); + } + + setPrintConfig() { + const printConfig = getApp().getPrintConfig(); + this.headerType = printConfig.header; + this.options.appName = printConfig.appName; + } + + async draw(list: Array<{ color: string; word: string }>) { + this.setPrintConfig(); + + this.colors = list.map((item) => item.color || '#000'); + this.characters = list.map((item) => item.word || '日'); + this.clear(); + this.setPaper(); + if (this.headerType !== 'minimal') { + await this.drawHeader(); + } else { + this.drawMiniHeader(); + } + + // 找字模板没有 drawLegend 部分 + this.drawContent(); + } + + async drawHeader() { + this.currentX = 80; + this.currentY = 80; + await drawHeaderCommon({ + canvas: this.canvas, + ctx: this.ctx, + headerType: this.headerType, + options: { + appName: this.options.appName || '涂鸦丫小程序', + appHint: this.options.appHint || '识字|识图|练字|打印', + title: this.options.title || '找一找 涂 色', + subTitle: this.options.subTitle || '给文字涂上相同的颜色', + }, + onHeaderDrawn: (currentY) => { + this.currentY = currentY; + this.drawLine(this.currentY); + }, + }); + } + + drawMiniHeader() { + drawMiniHeaderCommon({ + canvas: this.canvas, + ctx: this.ctx, + options: { + appName: this.options.appName || '涂鸦丫小程序', + title: this.options.title || '找一找 涂 色', + }, + onHeaderDrawn: (currentY) => { + this.currentY = currentY; + this.drawLine(this.currentY); + }, + }); + } + + drawContent() { + const { canvas, ctx, characters, colors } = this; + if (characters.length <= 0) return; + + // 第一个字作为中心大字 + const firstChar = characters[0]; + const firstColor = colors[0]; + + // 计算内容区域 + const contentTop = this.currentY + 50; + const contentBottom = canvas.height - 80; + const contentHeight = contentBottom - contentTop; + + // 中心大字的参数 + const centerX = canvas.width / 2; + const centerY = contentTop + contentHeight / 2; // 内容区域垂直居中 + + // 普通圆的参数(和textDrawService一致) + const radius = 80; + const fontSize = 72; + + // 先选择模板,获取模板的实际位置数量 + const { templateIndex } = selectTemplate(); + // 从固定模板中获取位置 + const positions = getPositionsFromTemplate( + templateIndex, + centerX, + centerY, + ); + const targetCount = positions.length; // 使用模板的实际位置数量(中心字已单独绘制) + + // 生成所有要绘制的字符列表(包括第一个字和其他字) + const allChars: Array<{ char: string; color: string }> = []; + + // 如果只有一个字符,全部使用第一个字符 + if (characters.length === 1) { + for (let i = 0; i < targetCount; i++) { + allChars.push({ char: firstChar, color: firstColor }); + } + } else { + // 第一个字出现多次(根据总字符数决定,确保有足够的第一个字) + const firstCharCount = Math.max( + Math.floor(targetCount * 0.4), + Math.floor(characters.length * 2), + ); + for (let i = 0; i < firstCharCount; i++) { + allChars.push({ char: firstChar, color: firstColor }); + } + + // 其他字符各出现几次 + const remainingCount = targetCount - firstCharCount; + const otherCharCount = Math.max( + 1, + Math.floor(remainingCount / (characters.length - 1)), + ); + for (let i = 1; i < characters.length; i++) { + for (let j = 0; j < otherCharCount; j++) { + allChars.push({ char: characters[i], color: colors[i] }); + } + } + + // 如果生成的字符数量还不够,用第一个字符补齐 + while (allChars.length < targetCount) { + allChars.push({ char: firstChar, color: firstColor }); + } + } + + // 打乱顺序 + for (let i = allChars.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [allChars[i], allChars[j]] = [allChars[j], allChars[i]]; + } + + // 根据实际生成的位置数量来截取字符(确保数量匹配) + const charsToDraw = allChars.slice(0, positions.length); + + // 绘制中心大字(去掉圆圈border,只绘制文字) + ctx.fillStyle = '#333'; + ctx.font = `bold ${fontSize * 3}px "Microsoft Yahei"`; // 中心字是其他字的3倍 + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + ctx.fillText(firstChar, centerX, centerY); + + // 绘制四周的字符 + positions.forEach((pos, index) => { + const item = charsToDraw[index]; + const y = pos.y; // 已经是绝对坐标,不需要再加contentTop + + // 绘制圆 + ctx.fillStyle = '#fff'; + ctx.strokeStyle = '#000'; + ctx.lineWidth = 4; + ctx.beginPath(); + ctx.arc(pos.x, y, radius, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + ctx.closePath(); + + // 绘制字符 + ctx.fillStyle = '#333'; + ctx.font = `bold ${fontSize}px "Microsoft Yahei"`; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + // Debug模式:显示坐标系索引值,否则显示文字 + const displayText = this.debug ? String(index) : item.char; + ctx.fillText(displayText, pos.x, y, radius * 2); + }); + } + + drawLine(linY: number) { + const { canvas, ctx } = this; + + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(80, linY); + ctx.lineTo(canvas.width - 80, linY); + ctx.stroke(); + } + + setPaper() { + const { ctx, canvas } = this; + const { pixelRatio: dpr } = wx.getWindowInfo(); + let { width, height } = PAPER_SIZE[this.paperSize]; + width = width * dpr; + height = height * dpr; + + canvas.width = width; + canvas.height = height; + + this.clear(); + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, width, height); + } + + clear() { + const canvas = this.canvas; + this.ctx.clearRect(0, 0, canvas.width, canvas.height); + } +} + +export default FindWordDrawService; diff --git a/miniprogram/service/findWordTemplate.ts b/miniprogram/service/findWordTemplate.ts new file mode 100644 index 0000000..64c20de --- /dev/null +++ b/miniprogram/service/findWordTemplate.ts @@ -0,0 +1,223 @@ +export const POSITION_TEMPLATES: Array> = [ + // 模板1:椭圆形分布-上下更密集 28个位置,相邻间距约100-150px + [ + // 0-14号位置 + { x: -600, y: 0 }, // 0 - 左侧 + { x: -440, y: -240 }, // 1 + { x: -580, y: -430 }, // 2 + { x: -340, y: -600 }, // 3 + { x: -330, y: -880 }, // 4 + { x: -560, y: -780 }, // 5 + { x: -50, y: -890 }, // 6 + { x: -80, y: -670 }, // 7 - 上方 + { x: 180, y: -880 }, // 8 + { x: 200, y: -620 }, // 9 + { x: 440, y: -850 }, // 10 + { x: 600, y: -620 }, // 11 + { x: 420, y: -460 }, // 12 + { x: 500, y: -220 }, // 13 + { x: 600, y: 0 }, // 14 - 右侧 + // 15-28号位置:基于0-14号位置以x轴为镜像轴生成(y坐标取反,x坐标不变) + { x: 600, y: 0 }, // 15 - 14的镜像 + { x: 500, y: 220 }, // 16 - 13的镜像 + { x: 420, y: 460 }, // 17 - 12的镜像 + { x: 600, y: 620 }, // 18 - 11的镜像 + { x: 440, y: 850 }, // 19 - 10的镜像 + { x: 200, y: 620 }, // 20 - 9的镜像 + { x: 180, y: 880 }, // 21 - 8的镜像 + { x: -80, y: 670 }, // 22 - 7的镜像 + { x: -50, y: 890 }, // 23 - 6的镜像 + { x: -560, y: 780 }, // 24 - 5的镜像 + { x: -330, y: 880 }, // 25 - 4的镜像 + { x: -340, y: 600 }, // 26 - 3的镜像 + { x: -580, y: 430 }, // 27 - 2的镜像 + { x: -440, y: 240 }, // 28 - 1的镜像 + ], + // 模板2:基于模板1,在上下各增加一个坐标系,并适度调整原始位置 + [ + { x: -620, y: 0 }, // 0 - 左侧(向外扩展) + { x: -420, y: -200 }, // 1 + { x: -640, y: -350 }, // 2 + { x: -400, y: -480 }, // 3 + { x: -600, y: -600 }, // 4 + { x: -420, y: -740 }, // 5 + { x: -280, y: -910 }, // 6 + { x: -200, y: -680 }, // 7 + { x: -60, y: -890 }, // 8 + { x: -10, y: -660 }, // 9 - 上方 + { x: 180, y: -890 }, // 10 + { x: 200, y: -630 }, // 11 + { x: 400, y: -800 }, // 12 + { x: 590, y: -660 }, // 13 + { x: 410, y: -440 }, // 14 + { x: 680, y: -420 }, // 15 + { x: 570, y: -210 }, // 16 + { x: 670, y: 0 }, // 17 - 右侧 + // 18-35号位置:基于0-17号位置以x轴为镜像轴生成(y坐标取反,x坐标不变) + { x: 570, y: 210 }, // 18 - 16的镜像 + { x: 680, y: 420 }, // 19 - 15的镜像 + { x: 410, y: 440 }, // 20 - 14的镜像 + { x: 590, y: 660 }, // 21 - 13的镜像 + { x: 400, y: 800 }, // 22 - 12的镜像 + { x: 200, y: 630 }, // 23 - 11的镜像 + { x: 180, y: 890 }, // 24 - 10的镜像 + { x: -10, y: 660 }, // 25 - 9的镜像 + { x: -60, y: 890 }, // 26 - 8的镜像 + { x: -200, y: 680 }, // 27 - 7的镜像 + { x: -300, y: 930 }, // 28 - 6的镜像 + { x: -440, y: 780 }, // 29 - 5的镜像 + { x: -600, y: 600 }, // 30 - 4的镜像 + { x: -400, y: 480 }, // 31 - 3的镜像 + { x: -640, y: 350 }, // 32 - 2的镜像 + { x: -420, y: 200 }, // 33 - 1的镜像 + ], + // 模板3 + [ + // 0-15号位置 + { x: -660, y: -100 }, // 0 - 左侧 + { x: -500, y: -280 }, // 1 + { x: -660, y: -480 }, // 2 + { x: -660, y: -720 }, // 3 + { x: -440, y: -920 }, // 4 + { x: -250, y: -720 }, // 5 + { x: -380, y: -480 }, // 6 - 上方 + { x: -140, y: -480 }, // 7 + { x: 90, y: -480 }, // 8 + { x: 340, y: -480 }, // 9 + { x: 250, y: -720 }, // 10 + { x: 440, y: -920 }, // 11 + { x: 660, y: -720 }, // 12 + { x: 660, y: -480 }, // 13 + { x: 500, y: -280 }, // 14 + { x: 660, y: -100 }, // 15 - 右侧 + // 16-32号位置:基于0-15号位置以x轴为镜像轴生成(y坐标取反,x坐标不变) + { x: 660, y: 120 }, // 16 - 15的镜像 + { x: 500, y: 300 }, // 17 - 14的镜像 + { x: 680, y: 480 }, // 18 - 13的镜像 + { x: 680, y: 720 }, // 19 - 12的镜像 + { x: 500, y: 920 }, // 20 - 11的镜像 + { x: 250, y: 720 }, // 21 - 10的镜像 + { x: 340, y: 480 }, // 22 - 9的镜像 + { x: 90, y: 480 }, // 23 - 8的镜像 + { x: -140, y: 480 }, // 24 - 7的镜像 + { x: -380, y: 480 }, // 25 - 6的镜像 + { x: -250, y: 720 }, // 26 - 5的镜像 + { x: -500, y: 920 }, // 27 - 4的镜像 + { x: -660, y: 720 }, // 28 - 3的镜像 + { x: -680, y: 480 }, // 29 - 2的镜像 + { x: -500, y: 300 }, // 30 - 1的镜像 + { x: -660, y: 120 }, // 31 - 0的镜像 + ], + // 模板4:三角形和梯形 + [ + // 0-5号位置 + { x: -350, y: 0 }, // 0 - 顶点(不变) + { x: -500, y: 150 }, // 1 - 左侧上方45度 + { x: -650, y: 300 }, // 2 - 左侧上方45度(更远) + { x: -500, y: -150 }, // 3 - 左侧下方45度 + { x: -650, y: -300 }, // 4 - 左侧下方45度(更远) + { x: -650, y: 0 }, // 5 - Y=0,X和2、4相同 + + // 6-11号位置 + { x: 350, y: 0 }, // 6 - 顶点(不变) + { x: 500, y: 150 }, // 7 - 右侧上方45度 + { x: 650, y: 300 }, // 8 - 右侧上方45度(更远) + { x: 500, y: -150 }, // 9 - 右侧下方45度 + { x: 650, y: -300 }, // 10 - 右侧下方45度(更远) + { x: 650, y: 0 }, // 11 - Y=0,X和8、10相同 + + // 倒着的梯形 + { x: -440, y: -790 }, // 12 + { x: -320, y: -570 }, // 13 + { x: -200, y: -350 }, // 14 + { x: 0, y: -350 }, // 15 + { x: 200, y: -350 }, // 16 + { x: 320, y: -570 }, // 17 + { x: 440, y: -790 }, // 18 + { x: -150, y: -790 }, // 19 + { x: 150, y: -790 }, // 20 + + // 正着的梯形 + { x: -440, y: 790 }, // 21 + { x: -320, y: 570 }, // 22 + { x: -200, y: 350 }, // 23 + { x: 0, y: 350 }, // 24 + { x: 200, y: 350 }, // 25 + { x: 320, y: 570 }, // 26 + { x: 440, y: 790 }, // 27 + { x: 150, y: 790 }, // 28 + { x: -150, y: 790 }, // 29 + ], + // 模板5:4个正方形 + [ + // 0-7号位置 + { x: -250, y: -250 }, // 0 + { x: -250, y: -500 }, // 1 + { x: -250, y: -750 }, // 2 + { x: -500, y: -750 }, // 3 + { x: -750, y: -750 }, // 4 + { x: -750, y: -500 }, // 5 + { x: -750, y: -250 }, // 6 + { x: -500, y: -250 }, // 7 + + // 8-15号位置 + { x: 250, y: -250 }, // 8 + { x: 250, y: -500 }, // 9 + { x: 250, y: -750 }, // 10 + { x: 500, y: -750 }, // 11 + { x: 750, y: -750 }, // 12 + { x: 750, y: -500 }, // 13 + { x: 750, y: -250 }, // 14 + { x: 500, y: -250 }, // 15 + + // 16-23号位置 + { x: -250, y: 250 }, // 16 + { x: -250, y: 500 }, // 17 + { x: -250, y: 750 }, // 18 + { x: -500, y: 750 }, // 19 + { x: -750, y: 750 }, // 20 + { x: -750, y: 500 }, // 21 + { x: -750, y: 250 }, // 22 + { x: -500, y: 250 }, // 23 + + // 24-31号位置 + { x: 250, y: 250 }, // 24 + { x: 250, y: 500 }, // 25 + { x: 250, y: 750 }, // 26 + { x: 500, y: 750 }, // 27 + { x: 750, y: 750 }, // 28 + { x: 750, y: 500 }, // 29 + { x: 750, y: 250 }, // 30 + { x: 500, y: 250 }, // 31 + ], + // 模板6:4个十字形 + [ + // 0-4号位置 + { x: -500, y: -500 }, // 0 + { x: -250, y: -500 }, // 1 + { x: -500, y: -750 }, // 2 + { x: -750, y: -500 }, // 3 + { x: -500, y: -250 }, // 4 + + // 5-9号位置 + { x: 500, y: -500 }, // 5 + { x: 250, y: -500 }, // 6 + { x: 500, y: -750 }, // 7 + { x: 750, y: -500 }, // 8 + { x: 500, y: -250 }, // 9 + + // 10-14号位置 + { x: -500, y: 500 }, // 10 + { x: -250, y: 500 }, // 11 + { x: -500, y: 750 }, // 12 + { x: -750, y: 500 }, // 13 + { x: -500, y: 250 }, // 14 + + // 15-19号位置 + { x: 500, y: 500 }, // 15 + { x: 250, y: 500 }, // 16 + { x: 500, y: 750 }, // 17 + { x: 750, y: 500 }, // 18 + { x: 500, y: 250 }, // 19 + ], +]; diff --git a/miniprogram/service/headerDrawService.ts b/miniprogram/service/headerDrawService.ts new file mode 100644 index 0000000..981c487 --- /dev/null +++ b/miniprogram/service/headerDrawService.ts @@ -0,0 +1,126 @@ +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; + }; + onHeaderDrawn?: (currentY: number) => void; // 绘制完成后的回调,用于设置 currentY 和绘制分割线 +} + +/** + * 绘制迷你页眉(仅包含应用名称和标题,居中显示) + */ +export function drawMiniHeader({ + canvas, + ctx, + options, + onHeaderDrawn, +}: DrawMiniHeaderParams): void { + const { appName, title } = options; + const titleY = 120; + + ctx.font = 'bold 64px "Microsoft Yahei"'; + ctx.fillStyle = '#000'; + ctx.textAlign = 'center'; + ctx.fillText(appName + ' ' + title, canvas.width / 2, titleY); + + // 调用回调函数,让调用者设置 currentY 并绘制分割线 + if (onHeaderDrawn) { + onHeaderDrawn(200); + } +} diff --git a/miniprogram/service/shapeDrawService.ts b/miniprogram/service/shapeDrawService.ts index aae4b56..dd51a72 100644 --- a/miniprogram/service/shapeDrawService.ts +++ b/miniprogram/service/shapeDrawService.ts @@ -1,7 +1,10 @@ import { PAPER_SIZE } from '../constants/colors'; -import { getMiniCodeImage, getImage } from '../utils/index'; import { ShapeCard } from '../constants/shapes'; import { drawShape } from './drawShape'; +import { + drawHeader as drawHeaderCommon, + drawMiniHeader as drawMiniHeaderCommon, +} from './headerDrawService'; /** * 计算图形在画布上的位置,避免重叠 @@ -36,8 +39,16 @@ function calculateShapePositions( const col = i % cols; // 在单元格内随机位置 - const x = padding + col * cellWidth + (cellWidth - shapeSize) / 2 + (Math.random() - 0.5) * (cellWidth - shapeSize) * 0.3; - const y = padding + row * cellHeight + (cellHeight - shapeSize) / 2 + (Math.random() - 0.5) * (cellHeight - shapeSize) * 0.3; + const x = + padding + + col * cellWidth + + (cellWidth - shapeSize) / 2 + + (Math.random() - 0.5) * (cellWidth - shapeSize) * 0.3; + const y = + padding + + row * cellHeight + + (cellHeight - shapeSize) / 2 + + (Math.random() - 0.5) * (cellHeight - shapeSize) * 0.3; positions.push({ x, y }); } @@ -45,7 +56,6 @@ function calculateShapePositions( return positions; } - class ShapeDrawService { headerType: PrintHeader; canvas: WechatMiniprogram.Canvas; @@ -102,74 +112,38 @@ class ShapeDrawService { } async drawHeader() { - const { canvas, ctx } = this; - const { appName, appHint, title, subTitle } = this.options; - this.currentX = 80; this.currentY = 80; - let titleX = this.currentX + 200 + 48; - const titleY = 80; - - const logoX = 80; - const logoY = 60; - const logoWidth = 200; - const logoHeight = 200; - - switch (this.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); - - this.currentY = 304; - this.drawLine(this.currentY); + await drawHeaderCommon({ + canvas: this.canvas, + ctx: this.ctx, + headerType: this.headerType, + options: { + appName: this.options.appName || '涂鸦丫小程序', + appHint: this.options.appHint || '涂色|识字|画画|打印', + title: this.options.title || '找一找 涂 色', + subTitle: this.options.subTitle || '给图形涂上相同的颜色', + }, + onHeaderDrawn: (currentY) => { + this.currentY = currentY; + this.drawLine(this.currentY); + }, + }); } drawMiniHeader() { - const { canvas, ctx } = this; - const { appName, title } = this.options; - const titleY = 120; - - ctx.font = 'bold 64px "Microsoft Yahei"'; - ctx.fillStyle = '#000'; - ctx.textAlign = 'center'; - ctx.fillText(appName + ' ' + title, canvas.width / 2, titleY); - this.currentY = 200; - this.drawLine(this.currentY); + drawMiniHeaderCommon({ + canvas: this.canvas, + ctx: this.ctx, + options: { + appName: this.options.appName || '涂鸦丫小程序', + title: this.options.title || '找一找 涂 色', + }, + onHeaderDrawn: (currentY) => { + this.currentY = currentY; + this.drawLine(this.currentY); + }, + }); } drawLegend() { @@ -245,7 +219,9 @@ class ShapeDrawService { // 每行最多能放多少个图形(考虑最小间距40) const minGap = 40; - const maxPerRow = Math.floor((contentWidth + minGap) / (shapeSize + minGap)); + const maxPerRow = Math.floor( + (contentWidth + minGap) / (shapeSize + minGap), + ); // 总共要绘制多少个图形(每行都填满) const totalShapes = maxPerRow * ROW_COUNT; @@ -263,14 +239,20 @@ class ShapeDrawService { } // 生成所有图形的位置 - let positions: { x: number, y: number }[] = []; + let positions: { x: number; y: number }[] = []; for (let row = 0; row < ROW_COUNT; row++) { // 本行起始Y - const y = contentTop + row * (shapeSize + VERTICAL_GAP) + shapeSize / 2; + const y = + contentTop + row * (shapeSize + VERTICAL_GAP) + shapeSize / 2; // 本行实际间距 - const gap = maxPerRow > 1 - ? Math.max(minGap, (contentWidth - maxPerRow * shapeSize) / (maxPerRow - 1)) - : 0; + const gap = + maxPerRow > 1 + ? Math.max( + minGap, + (contentWidth - maxPerRow * shapeSize) / + (maxPerRow - 1), + ) + : 0; // 本行起始X(水平居中) let startX = leftMargin; if (maxPerRow > 1) { @@ -282,7 +264,7 @@ class ShapeDrawService { for (let col = 0; col < maxPerRow; col++) { positions.push({ x: startX + col * (shapeSize + gap) + shapeSize / 2, - y: y + y: y, }); } } @@ -297,7 +279,9 @@ class ShapeDrawService { } // 记录实际绘制的图形数量 - console.log(`绘制图形:计划${totalShapes}个,实际${positions.length}个;每行${maxPerRow}个,共${ROW_COUNT}行;边距 左右=${leftMargin}/${rightMargin},顶部=${topGap}(基于legend底线)`); + console.log( + `绘制图形:计划${totalShapes}个,实际${positions.length}个;每行${maxPerRow}个,共${ROW_COUNT}行;边距 左右=${leftMargin}/${rightMargin},顶部=${topGap}(基于legend底线)`, + ); // 绘制所有图形 positions.forEach((pos, index) => { diff --git a/miniprogram/service/textDrawService.ts b/miniprogram/service/textDrawService.ts index 3f7cab1..5308350 100644 --- a/miniprogram/service/textDrawService.ts +++ b/miniprogram/service/textDrawService.ts @@ -1,6 +1,9 @@ // import { PAPER_SIZE } from './constant'; import { PAPER_SIZE } from '../constants/colors'; -import { getMiniCodeImage, getImage } from '../utils/index'; +import { + drawHeader as drawHeaderCommon, + drawMiniHeader as drawMiniHeaderCommon, +} from './headerDrawService'; /** * 计算示例区域圆的中心点 @@ -89,7 +92,7 @@ class TextDrawService { this.options.appName = printConfig.appName; } - draw(list: Array<{ color: string; word: string }>) { + async draw(list: Array<{ color: string; word: string }>) { this.setPrintConfig(); this.colors = list.map((item) => item.color || '#000'); @@ -97,7 +100,7 @@ class TextDrawService { this.clear(); this.setPaper(); if (this.headerType !== 'minimal') { - this.drawHeader(); + await this.drawHeader(); } else { this.drawMiniHeader(); } @@ -107,85 +110,40 @@ class TextDrawService { } async drawHeader() { - const { canvas, ctx } = this; - const { appName, appHint, title, subTitle } = this.options; - this.currentX = 80; this.currentY = 80; - let titleX = this.currentX + 200 + 48; - const titleY = 80; - - const logoX = 80; - const logoY = 60; - const logoWidth = 200; - const logoHeight = 200; - - switch (this.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.strokeStyle = '#888'; - // ctx.lineWidth = 2; - // ctx.beginPath(); - // ctx.moveTo(483, 70); - // ctx.lineTo(483, 70 + 140); - // ctx.stroke(); - - 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); - - this.currentY = 304; - this.drawLine(this.currentY); + await drawHeaderCommon({ + canvas: this.canvas, + ctx: this.ctx, + headerType: this.headerType, + options: { + appName: this.options.appName || '涂鸦丫小程序', + appHint: this.options.appHint || '识字|识图|练字|打印', + title: this.options.title || '找一找 涂 色', + subTitle: this.options.subTitle || '给文字涂上相同的颜色', + }, + onHeaderDrawn: (currentY) => { + this.currentY = currentY; + this.drawLine(this.currentY); + }, + }); } drawMiniHeader() { - const { canvas, ctx } = this; - const { appName, title } = this.options; - const titleY = 120; - - ctx.font = 'bold 64px "Microsoft Yahei"'; - ctx.fillStyle = '#000'; - ctx.textAlign = 'center'; - ctx.fillText(appName + ' ' + title, canvas.width / 2, titleY); - this.currentY = 200; - this.drawLine(this.currentY); + drawMiniHeaderCommon({ + canvas: this.canvas, + ctx: this.ctx, + options: { + appName: this.options.appName || '涂鸦丫小程序', + title: this.options.title || '找一找 涂 色', + }, + onHeaderDrawn: (currentY) => { + this.currentY = currentY; + this.drawLine(this.currentY); + }, + }); } - /** 绘制示例 * 矩形: x、y为左上角 * 圆形:x、y为圆心 diff --git a/miniprogram/service/wordDrawService.ts b/miniprogram/service/wordDrawService.ts index ced36f9..f3a62a1 100644 --- a/miniprogram/service/wordDrawService.ts +++ b/miniprogram/service/wordDrawService.ts @@ -1,6 +1,9 @@ import { PAPER_SIZE } from '../constants/colors'; -import { getMiniCodeImage, getImage } from '../utils/index'; import { CharacterItem } from '../types/characterType'; +import { + drawHeader as drawHeaderCommon, + drawMiniHeader as drawMiniHeaderCommon, +} from './headerDrawService'; /** * 仅支持 M/L/Z 的简单 SVG 路径解析与绘制 @@ -18,19 +21,21 @@ function drawSvgPathCommands({ pathD, offsetX, offsetY, - scale + scale, }: DrawSvgPathCommandsParams) { // console.log('drawSvgPathCommands 参数:', { pathD, offsetX, offsetY, scale }); // 精度处理函数:避免浮点数精度问题 const roundToPrecision = (num: number, precision: number = 2): number => { - return Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision); + return ( + Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision) + ); }; // 新的解析方法:按命令片段解析 // 使用正则表达式匹配从字母开头到下一个字母(或结尾)的片段 const commandRegex = /([MLZ])([^MLZ]*?)(?=[MLZ]|$)/g; - const commands: Array<{ cmd: string, coords: string }> = []; + const commands: Array<{ cmd: string; coords: string }> = []; let match; while ((match = commandRegex.exec(pathD)) !== null) { @@ -44,9 +49,15 @@ function drawSvgPathCommands({ let commandCount = 0; // 优化的坐标解析和绘制函数 - const parseAndDrawCoords = (coords: string, cmd: string, drawFunction: (x: number, y: number) => void) => { + const parseAndDrawCoords = ( + coords: string, + cmd: string, + drawFunction: (x: number, y: number) => void, + ) => { // 更严格的坐标解析:支持负数和小数 - const coordParts = coords.split(/\s+/).filter(part => part.trim() !== ''); + const coordParts = coords + .split(/\s+/) + .filter((part) => part.trim() !== ''); if (coordParts.length >= 2) { const x = parseFloat(coordParts[0]); @@ -60,21 +71,27 @@ function drawSvgPathCommands({ // console.log(`${cmd}: 绘制到 (${finalX}, ${finalY}) [原始: (${x}, ${y})]`); drawFunction(finalX, finalY); } else { - console.warn(`${cmd}: 坐标解析失败 - x=${x}, y=${y}, 原始坐标: "${coords}"`); + console.warn( + `${cmd}: 坐标解析失败 - x=${x}, y=${y}, 原始坐标: "${coords}"`, + ); } } else { - console.warn(`${cmd}: 坐标数量不足,需要2个,实际${coordParts.length}个,原始: "${coords}"`); + console.warn( + `${cmd}: 坐标数量不足,需要2个,实际${coordParts.length}个,原始: "${coords}"`, + ); } }; // 命令处理映射表,提高可读性和可扩展性 const commandHandlers: Record void> = { - 'M': (coords) => parseAndDrawCoords(coords, 'M', (x, y) => ctx.moveTo(x, y)), - 'L': (coords) => parseAndDrawCoords(coords, 'L', (x, y) => ctx.lineTo(x, y)), - 'Z': () => { + M: (coords) => + parseAndDrawCoords(coords, 'M', (x, y) => ctx.moveTo(x, y)), + L: (coords) => + parseAndDrawCoords(coords, 'L', (x, y) => ctx.lineTo(x, y)), + Z: () => { // console.log('Z: 闭合路径'); // 注意:不要在这里调用 closePath(),因为 drawStrokes 中会统一处理 - } + }, }; for (const { cmd, coords } of commands) { @@ -113,7 +130,7 @@ function drawStrokes({ uptoInclusive, fillStyle, strokeStyle, - lineWidth + lineWidth, }: DrawStrokesParams) { // 模板中使用 54x54 的视窗尺寸 const viewBoxSize = 54; @@ -152,7 +169,7 @@ function drawStrokes({ pathD: strokes[s], offsetX: contentOffsetX, offsetY: contentOffsetY, - scale: contentScale + scale: contentScale, }); ctx.fillStyle = fillStyle; ctx.strokeStyle = strokeStyle; @@ -178,7 +195,7 @@ function drawTianZiGrid({ y, size, lineColor = '#e0e0e0', - boldColor = '#cccccc' + boldColor = '#cccccc', }: DrawTianZiGridParams) { // 外框 ctx.strokeStyle = boldColor; @@ -289,83 +306,53 @@ class WordDrawService { // 清空页眉以下的所有内容 ctx.fillStyle = '#fff'; - ctx.fillRect(0, contentStartY, canvas.width, canvas.height - contentStartY); + ctx.fillRect( + 0, + contentStartY, + canvas.width, + canvas.height - contentStartY, + ); } async drawHeader() { - const { canvas, ctx } = this; - const { appName, appHint, title, subTitle } = this.options; - this.currentX = 80; this.currentY = 80; - let titleX = this.currentX + 200 + 48; - const titleY = 80; - - const logoX = 80; - const logoY = 60; - const logoWidth = 200; - const logoHeight = 200; - - switch (this.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); - - // 计算页眉的实际高度,包括分割线 - const headerHeight = Math.max(titleY + 64 + 48 + 48) + 0; // 64px字体 + 48px间距 + 48px字体 + 20px边距 - // this.currentY = headerHeight; - this.currentY += headerHeight; - this.drawDivider(this.currentY); + await drawHeaderCommon({ + canvas: this.canvas, + ctx: this.ctx, + headerType: this.headerType, + options: { + appName: this.options.appName || '涂鸦丫小程序', + appHint: this.options.appHint || '识字|识图|练字|打印', + title: this.options.title || '田字格 练 字 贴', + subTitle: this.options.subTitle || '按笔画临摹练习', + }, + onHeaderDrawn: () => { + // wordDrawService 使用不同的计算方式 + const titleY = 80; + const headerHeight = Math.max(titleY + 64 + 48 + 48) + 0; // 64px字体 + 48px间距 + 48px字体 + 20px边距 + this.currentY = 80 + headerHeight; + this.drawDivider(this.currentY); + }, + }); } drawMiniHeader() { - const { canvas, ctx } = this; - const { appName, title } = this.options; - const titleY = 120; - - ctx.font = 'bold 64px "Microsoft Yahei"'; - ctx.fillStyle = '#000'; - ctx.textAlign = 'center'; - ctx.fillText(appName + ' ' + title, canvas.width / 2, titleY); - - // 计算迷你页眉的实际高度 - const miniHeaderHeight = titleY + 64 + 20; // 64px字体 + 20px边距 - this.currentY = miniHeaderHeight; - this.drawDivider(this.currentY); + drawMiniHeaderCommon({ + canvas: this.canvas, + ctx: this.ctx, + options: { + appName: this.options.appName || '涂鸦丫小程序', + title: this.options.title || '田字格 练 字 贴', + }, + onHeaderDrawn: () => { + // wordDrawService 使用不同的计算方式 + const titleY = 120; + const miniHeaderHeight = titleY + 64 + 20; // 64px字体 + 20px边距 + this.currentY = miniHeaderHeight; + this.drawDivider(this.currentY); + }, + }); } /** @@ -373,10 +360,10 @@ class WordDrawService { */ /** * 绘制正文内容:包括田字格的排布和内容 - * + * * minGap 作用解释: * minGap(最小列间距)用于田字格水平方向(每一行格子之间的间距)的初始最小值。它保证多个田字格在一行内不会紧贴排布、而是有一个最小的间隔,整体看起来不会拥挤。后续还会结合画布实际剩余空间动态调整为更合适的实际间距 actualColGap。 - * + * * rowGap 作用解释: * rowGap(行间距)用于田字格竖直方向(每一行田字格之间的间隔)的初始最小值。它防止行与行的田字格上下贴得太紧,增加一定的可视呼吸空间。之后同样会结合剩余空间动态调整为实际间距 actualRowGap。 */ @@ -397,8 +384,10 @@ class WordDrawService { const { maxRow, maxCol } = this.getMaxGridLayout(); // 动态调整间距以充分利用空间(在允许的最小间距基础上弹性扩展以适配画布) - const actualRowGap = maxRow > 1 ? (contentHeight - maxRow * cellSize) / (maxRow - 1) : 0; - const actualColGap = maxCol > 1 ? (contentWidth - maxCol * cellSize) / (maxCol - 1) : 0; + const actualRowGap = + maxRow > 1 ? (contentHeight - maxRow * cellSize) / (maxRow - 1) : 0; + const actualColGap = + maxCol > 1 ? (contentWidth - maxCol * cellSize) / (maxCol - 1) : 0; // console.log(`布局信息: 行数=${rowNumber}, 列数=${columnNumber}, 行间距=${actualRowGap.toFixed(1)}, 列间距=${actualColGap.toFixed(1)}`); @@ -411,7 +400,7 @@ class WordDrawService { colGap: actualColGap, rowGap: actualRowGap, maxRow, - maxCol + maxCol, }); if (characterData && characterData.length > 0) { @@ -425,9 +414,9 @@ class WordDrawService { colGap: actualColGap, rowGap: actualRowGap, maxCol, - maxRow + maxRow, }); - }; + } } /** @@ -449,8 +438,14 @@ class WordDrawService { const minGap = 24; const rowGap = 36; - const maxCol = Math.max(1, Math.floor((contentWidth + minGap) / (cellSize + minGap))); - const maxRow = Math.max(1, Math.floor((contentHeight + rowGap) / (cellSize + rowGap))); + const maxCol = Math.max( + 1, + Math.floor((contentWidth + minGap) / (cellSize + minGap)), + ); + const maxRow = Math.max( + 1, + Math.floor((contentHeight + rowGap) / (cellSize + rowGap)), + ); return { maxRow, maxCol }; } @@ -466,7 +461,7 @@ class WordDrawService { colGap, rowGap, maxRow, - maxCol + maxCol, }: { ctx: RenderingContext; startX: number; @@ -474,8 +469,8 @@ class WordDrawService { cellSize: number; colGap: number; rowGap: number; - maxRow: number; // 行数 - maxCol: number; // 列数 + maxRow: number; // 行数 + maxCol: number; // 列数 }) { for (let row = 0; row < maxRow; row++) { for (let col = 0; col < maxCol; col++) { @@ -498,7 +493,7 @@ class WordDrawService { colGap, rowGap, maxCol, - maxRow + maxRow, }: { ctx: RenderingContext; characterData: CharacterItem[]; @@ -574,7 +569,7 @@ class WordDrawService { cellSize, colGap, rowGap, - rowIndex + rowIndex, }: { ctx: RenderingContext; strokes: string[]; @@ -599,9 +594,9 @@ class WordDrawService { offsetY: y, size: cellSize, uptoInclusive: strokes.length - 1, - fillStyle: 'rgb(0,0,0)', // 黑色填充 - strokeStyle: 'rgb(0,0,0)', // 黑色描边 - lineWidth: 4, // 较粗的线条 + fillStyle: 'rgb(0,0,0)', // 黑色填充 + strokeStyle: 'rgb(0,0,0)', // 黑色描边 + lineWidth: 4, // 较粗的线条 }); } @@ -618,7 +613,7 @@ class WordDrawService { colGap, rowGap, rowIndex, - columnIndex + columnIndex, }: { ctx: RenderingContext; strokes: string[]; @@ -646,7 +641,7 @@ class WordDrawService { uptoInclusive: strokeIndex, fillStyle: '#ccc', strokeStyle: '#ccc', - lineWidth: 3, // 中等粗细 + lineWidth: 3, // 中等粗细 }); } diff --git a/project.private.config.json b/project.private.config.json index 0a26dc5..84a222f 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -2,7 +2,7 @@ "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "projectname": "doodle-mini", "setting": { - "compileHotReLoad": true, + "compileHotReLoad": false, "urlCheck": true, "coverView": false, "lazyloadPlaceholderEnable": false,