diff --git a/miniprogram.zip b/miniprogram.zip new file mode 100644 index 0000000..39146c0 Binary files /dev/null and b/miniprogram.zip differ diff --git a/miniprogram/assets/imgs/checked.png b/miniprogram/assets/imgs/checked.png new file mode 100644 index 0000000..3cb1f8e Binary files /dev/null and b/miniprogram/assets/imgs/checked.png differ diff --git a/miniprogram/components/shape-card/shape-card.ts b/miniprogram/components/shape-card/shape-card.ts index ea46f99..0709d4d 100644 --- a/miniprogram/components/shape-card/shape-card.ts +++ b/miniprogram/components/shape-card/shape-card.ts @@ -3,7 +3,7 @@ Component({ * 组件的属性列表 */ properties: { - key: { + index: { type: Number, value: 0, }, @@ -73,17 +73,18 @@ Component({ }); }, onDelete() { - const { key } = this.data; - this.triggerEvent('onDelete', { key }); + const { index } = this.data; + this.triggerEvent('onDelete', { index }); }, onColorTap() { - const { key, fillColor } = this.data; - this.triggerEvent('onColorTap', { key, fillColor }); + const { index, fillColor } = this.data; + console.log('onColorTap', index, fillColor); + this.triggerEvent('onColorTap', { index, fillColor }); }, onShapeTap() { - const { key } = this.data; - this.triggerEvent('onShapeTap', { key }); + const { index } = this.data; + this.triggerEvent('onShapeTap', { index }); }, }, }); diff --git a/miniprogram/components/shape-picker/shape-picker.less b/miniprogram/components/shape-picker/shape-picker.less index 5878fef..7622e46 100644 --- a/miniprogram/components/shape-picker/shape-picker.less +++ b/miniprogram/components/shape-picker/shape-picker.less @@ -8,7 +8,7 @@ box-shadow: 0 3rpx 4rpx rgba(0, 0, 0, 0.15); display: flex; flex-direction: column; - padding: 36rpx 36rpx; + padding: 32rpx 24rpx; box-sizing: border-box; align-items: center; @@ -16,77 +16,67 @@ font-size: 32rpx; color: #141414; font-weight: bold; - margin-bottom: 30rpx; text-align: center; } .shape-options-wrapper { - padding: 28rpx 0; + margin: 34rpx 0; width: 100%; - height: 500rpx; + height: 670rpx; overflow-y: auto; } .shape-options { display: grid; - grid-template-columns: repeat(3, 186rpx); - gap: 12rpx; + grid-template-columns: repeat(3, 160rpx); + gap: 38rpx; width: 100%; justify-content: center; + padding: 6rpx 0; .shape-option { - width: 140rpx; + position: relative; + width: 160rpx; display: flex; flex-direction: column; align-items: center; transition: all 0.2s ease; - padding: 10rpx; - border-radius: 10rpx; - border: 1rpx solid transparent; + padding: 4rpx; + border-radius: 8rpx; + border: 1rpx solid #F2F2F2; + box-sizing: border-box; + box-shadow: 0px 8px 12px rgba(184, 210, 188, 0.2); + overflow: hidden; - .shape-svg-container { + .shape-svg { width: 140rpx; height: 140rpx; - display: flex; - align-items: center; - justify-content: center; - border: 1rpx solid #141414; - border-radius: 8rpx; - background-color: #fafafa; - margin-bottom: 8rpx; - position: relative; - - .shape-svg { - width: 140rpx; - height: 140rpx; - display: block; - } } .shape-name { font-size: 24rpx; color: #141414; text-align: center; - line-height: 1.2; + line-height: 40rpx; + height: 40rpx; + margin-bottom: 4rpx; } &.selected { - // transform: scale(1.1); - // border: 1px solid rgb(147, 211, 51); - // background-color: rgba(64, 150, 255, 0.05); - - .shape-svg-container { - border: 1px solid #93d333; - // border: 1px solid #4096ff; - // background-color: #fff; - // box-shadow: 0 0 6rpx 2rpx rgba(147, 211, 51, 0.2); - } + border: 1px solid #93d333; .shape-name { - color: rgb(147, 211, 51); - // font-weight: bold; + color: #93d333; } } + + .img-shape-selected { + position: absolute; + bottom: -2rpx; + right: -2rpx; + width: 48rpx; + height: 48rpx; + } } } } diff --git a/miniprogram/components/shape-picker/shape-picker.ts b/miniprogram/components/shape-picker/shape-picker.ts index f8a51ef..28c2ee2 100644 --- a/miniprogram/components/shape-picker/shape-picker.ts +++ b/miniprogram/components/shape-picker/shape-picker.ts @@ -10,13 +10,9 @@ Component({ type: Boolean, value: false, }, - selectedShape: { - type: String, - value: '', - }, - currentShapeKey: { - type: String, - value: '', + singleMode: { + type: Boolean, + value: false, }, }, /** @@ -24,7 +20,7 @@ Component({ */ data: { shapes: [] as (ShapeCard & { svgDataUrl: string })[], - selectedShapes: [] as string[], // 多选模式下选中的形状数组 + totalSelected: 0, }, lifetimes: { attached() { @@ -37,7 +33,6 @@ Component({ svgDataUrl: svgDataUri }; }); - console.log('shapesWithDataUrl---:', shapesWithDataUrl); this.setData({ shapes: shapesWithDataUrl }); @@ -53,28 +48,26 @@ Component({ onSelectShape(e: WechatMiniprogram.TouchEvent) { const { index } = e.currentTarget.dataset; - const { currentShapeKey, shapes } = this.data; - - if (currentShapeKey) { + const { singleMode, shapes } = this.data; + let newShapes = []; + if (singleMode) { // 单选模式:currentShapeKey 不为空时,只能选中一个形状 - this.setData({ - shapes: shapes.map((item, i) => ({ - ...item, - checked: i === index - })) - }); + newShapes = shapes.map((item, i) => ({ + ...item, + checked: i === index + })); } else { // 多选模式:currentShapeKey 为空时,支持多选,最多可选6个 - let shapesCopy = this.data.shapes.map(item => ({ ...item })); + newShapes = this.data.shapes.map(item => ({ ...item })); const targetIndex = index; // 统计当前已选中的数量 - const checkedCount = shapesCopy.filter(item => item.checked).length; + const checkedCount = newShapes.filter(item => item.checked).length; - if (shapesCopy[targetIndex].checked) { + if (newShapes[targetIndex].checked) { // 如果已选中,则取消选中 - shapesCopy[targetIndex].checked = false; + newShapes[targetIndex].checked = false; } else { if (checkedCount >= 6) { wx.showToast({ @@ -85,38 +78,20 @@ Component({ return; } // 如果未选中,则添加到选中列表 - shapesCopy[targetIndex].checked = true; + newShapes[targetIndex].checked = true; } - - this.setData({ - shapes: shapesCopy, - }); } + const totalSelected = newShapes.filter(item => item.checked).length; + this.setData({ + shapes: newShapes, + totalSelected, + }); }, onChange() { - const { selectedShape, selectedShapes, currentShapeKey } = this.data; - - if (currentShapeKey) { - // 单选模式:返回单个选中的形状 - this.triggerEvent('onChange', { shape: selectedShape, currentShapeKey }); - } else { - // 多选模式:返回选中的形状数组 - this.triggerEvent('onChange', { shapes: selectedShapes }); - } - }, - - // 获取选中状态的 CSS 类名 - getShapeClass(shapeId: string): string { - const { currentShapeKey, selectedShapes } = this.data; - - if (currentShapeKey) { - // 单选模式:检查是否等于当前选中的形状 - return shapeId === currentShapeKey ? 'selected single-mode' : ''; - } else { - // 多选模式:检查是否在选中列表中 - return selectedShapes.includes(shapeId) ? 'selected multi-mode' : ''; - } + const { shapes } = this.data; + const selectedShapes = shapes.filter(item => item.checked); + this.triggerEvent('onChange', { shapes: selectedShapes, singleMode: this.data.singleMode }); }, }, }); diff --git a/miniprogram/components/shape-picker/shape-picker.wxml b/miniprogram/components/shape-picker/shape-picker.wxml index a84ea49..91a20ba 100644 --- a/miniprogram/components/shape-picker/shape-picker.wxml +++ b/miniprogram/components/shape-picker/shape-picker.wxml @@ -4,7 +4,7 @@ custom-class="shape-picker-popup"> 选择形状{{ currentShapeKey ? '' : '(多选)' }}选择形状{{ singleMode ? '' : '(多选)' }} @@ -15,20 +15,16 @@ bindtap="onSelectShape" data-index="{{index}}" data-shape="{{item.id}}"> - - - - - - - + {{item.name}} + @@ -37,9 +33,8 @@ type="primary" bind:click="onChange" width="400rpx" - disabled="{{ currentShapeKey ? !selectedShape : selectedShapes.length === 0 }}"> - 确认选择{{ !currentShapeKey && selectedShapes.length > 0 ? '(' - + selectedShapes.length + '个)' : '' }} + disabled="{{ totalSelected === 0 }}"> + 确认选择{{ singleMode ? '' : '(' + totalSelected + '个)' }} diff --git a/miniprogram/constants/shapes.ts b/miniprogram/constants/shapes.ts index a8d1c4a..8386227 100644 --- a/miniprogram/constants/shapes.ts +++ b/miniprogram/constants/shapes.ts @@ -65,7 +65,7 @@ const SHAPES: ShapeCard[] = [ drawFunction: 'drawRectangle', }, { - id: 'rectangle', + id: 'parallelogram', name: '平行四边形', type: 'rect', // svg: '', @@ -74,7 +74,7 @@ const SHAPES: ShapeCard[] = [ drawFunction: 'drawParallelogram', }, { - id: 'parallelogram', + id: 'diamond', name: '菱形', type: 'rect', // svg: '', diff --git a/miniprogram/pages/index/index.ts b/miniprogram/pages/index/index.ts index 39f97e0..15fc29f 100644 --- a/miniprogram/pages/index/index.ts +++ b/miniprogram/pages/index/index.ts @@ -26,12 +26,12 @@ Page({ wx.getStorageSync('hasShowIntroduction') || false; // 判断是否在PC端开发工具上运行 - const systemInfo = wx.getDeviceInfo(); - if (systemInfo.platform === 'devtools') { - this.setData({ - isPCDevtool: true, - }); - } + // const systemInfo = wx.getDeviceInfo(); + // if (systemInfo.platform === 'devtools') { + // this.setData({ + // isPCDevtool: true, + // }); + // } if (!hasShowIntroduction) { this.setData({ diff --git a/miniprogram/pages/shape/index.json b/miniprogram/pages/shape/index.json index 685d1ea..212d13e 100644 --- a/miniprogram/pages/shape/index.json +++ b/miniprogram/pages/shape/index.json @@ -10,6 +10,7 @@ "van-popup": "@vant/weapp/popup/index", "toy-button": "../../ui/button/button", "shape-card": "../../components/shape-card/shape-card", - "shape-picker": "../../components/shape-picker/shape-picker" + "shape-picker": "../../components/shape-picker/shape-picker", + "color-picker": "../../components/color-picker/color-picker" } } diff --git a/miniprogram/pages/shape/index.less b/miniprogram/pages/shape/index.less index a7c37bf..744689c 100644 --- a/miniprogram/pages/shape/index.less +++ b/miniprogram/pages/shape/index.less @@ -42,4 +42,20 @@ page { row-gap: 24rpx; justify-content: flex-start; } +} + +.bottom-btn-box { + position: fixed; + left: 0; + bottom: 0; + width: 100%; + height: 120rpx; + display: flex; + justify-content: space-between; + align-items: center; + background-color: #fff; + box-shadow: 0px -3.9rpx 5.2rpx rgba(0, 0, 0, 0.15); + padding: 0 24rpx; + box-sizing: border-box; + border-radius: 16rpx 16rpx 0 0; } \ No newline at end of file diff --git a/miniprogram/pages/shape/index.ts b/miniprogram/pages/shape/index.ts index 487e040..1acf035 100644 --- a/miniprogram/pages/shape/index.ts +++ b/miniprogram/pages/shape/index.ts @@ -1,68 +1,173 @@ import { SHAPES, ShapeCard } from '../../constants/shapes'; import { WATER_COLORS, PAPER_SIZE } from '../../constants/colors'; - +import ShapeDrawService from '../../service/shapeDrawService'; +import { checkAndSaveImage } from '../../utils/saveImage'; Page({ + canvas: null as Canvas | null, + ctx: null as RenderingContext | null, + boxHeight: 0, + boxWidth: 0, + shapeDrawService: null as ShapeDrawService | null, + data: { shapeList: [] as ShapeCard[], showSelectShapePopup: false, - currentShapeKey: '', + currentShapeId: '', + showColorPopup: false, + currentIndex: 0, + currentColor: '', + isPCDevtool: false, }, onLoad() { + // 判断是否在PC端开发工具上运行 + // const systemInfo = wx.getDeviceInfo(); + // if (systemInfo.platform === 'devtools') { + // this.setData({ + // isPCDevtool: true, + // }); + // } + this.refreshShapeCard(); + }, + + onReady() { + const query = wx.createSelectorQuery(); + query + .select('#canvasWrapper') + .boundingClientRect((rect) => { + if (rect) { + const { width, height } = PAPER_SIZE['A4']; + const boxWidth = rect.width; + const boxHeight = boxWidth / (width / height); + + this.boxHeight = boxHeight; + this.boxWidth = boxWidth; + + this.initCanvas(boxWidth, boxHeight); + } + }) + .exec(); + }, + + initCanvas(boxWidth: number, boxHeight: number) { + wx.createSelectorQuery() + .select('#canvasContent') + .fields({ + node: true, + size: true, + }) + .exec((res) => { + if (res[0] && res[0].node) { + const canvas = res[0].node; + const ctx = canvas.getContext('2d'); + if (ctx) { + this.canvas = canvas; + this.ctx = ctx; + // const rect = res[0]; + this.shapeDrawService = new ShapeDrawService(canvas, ctx); + // shapeDrawService.draw(); + + this.setData({ boxWidth, boxHeight }); + this.drawCanvas(); + } + } + }); + }, + + /** 变更shapeList 并更新canvas */ + drawCanvas( + updatedShapeList?: ShapeCard[], + callback: () => void = () => { }, + ) { + if (updatedShapeList) { + this.setData({ shapeList: updatedShapeList }, () => { + this.shapeDrawService?.draw(updatedShapeList!); + callback(); + }); + } else { + const shapeList = this.data.shapeList; + this.shapeDrawService?.draw(shapeList); + } }, openSelectShapePopup(e: any) { const { key } = e.detail; this.setData({ showSelectShapePopup: true, - currentShapeKey: key + currentShapeId: key }); }, closeSelectShapePopup() { this.setData({ showSelectShapePopup: false, - currentShapeKey: '' + currentShapeId: '' }); }, onChangeShape(e: any) { - const { shape, currentShapeKey, shapes } = e.detail; - console.log('shape', shape); - console.log('shapes', shapes); - if (currentShapeKey) { - this.setData({ - showSelectShapePopup: false, - currentShapeKey: '', - shapeList: this.data.shapeList.map(item => { - if (item.id === currentShapeKey) { - return { ...item, fillColor: item.fillColor }; - } - return item; - }) - }); - } else { - // 随机从 WATER_COLORS.basic12 中获取不重复的颜色 - const colorList = WATER_COLORS.basic12.map(item => item.hex); - const colorsCopy = [...colorList]; - for (let i = colorsCopy.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [colorsCopy[i], colorsCopy[j]] = [colorsCopy[j], colorsCopy[i]]; - } - const selectedColors = colorsCopy.slice(0, shapes.length); + const { shapes } = e.detail; + const prevShapeList: ShapeCard[] = this.data.shapeList || []; + // 1. 保留已选图形的 fillColor,不变 + // 2. 新增的图形分配未被占用的颜色,且不重复 - this.setData({ - showSelectShapePopup: false, - currentShapeKey: '', - shapeList: shapes.map((item: ShapeCard, idx: number) => ({ - ...item, - fillColor: selectedColors[idx] || item.fillColor - })) - }); + // 获取所有可用颜色 + const colorList = WATER_COLORS.basic12.map(item => item.hex); + + // 记录已被使用的颜色(只考虑当前 shapeList 中的 fillColor) + const usedColors = prevShapeList.map(item => item.fillColor).filter(Boolean); + + // 新的 shapeList + const newShapeList: ShapeCard[] = []; + + // 记录已分配的颜色,避免重复 + const assignedColors = [...usedColors]; + + // 先将 shapes 转为 Map,方便查找 + const prevShapeMap = new Map(prevShapeList.map(item => [item.id, item])); + + // 随机打乱剩余可用颜色 + function shuffle(arr: string[]) { + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; } + + // 计算未被占用的颜色 + let availableColors = colorList.filter(color => !assignedColors.includes(color)); + availableColors = shuffle(availableColors); + + shapes.forEach((item: ShapeCard) => { + if (prevShapeMap.has(item.id)) { + // 已存在,保留原有 fillColor + newShapeList.push({ + ...item, + fillColor: prevShapeMap.get(item.id)!.fillColor + }); + } else { + // 新增,分配未被占用的颜色 + const color = availableColors.shift() || colorList[0]; + assignedColors.push(color); + newShapeList.push({ + ...item, + fillColor: color + }); + } + }); + + this.setData({ + showSelectShapePopup: false, + currentShapeId: '', + shapeList: newShapeList + }, () => { + // 绘制canvas + this.drawCanvas(); + }); }, /** * 随机生成6个图形 @@ -94,6 +199,95 @@ Page({ // 更新数据 this.setData({ shapeList + }, () => { + // 绘制canvas + this.drawCanvas(); }); - } -}); \ No newline at end of file + }, + onDelete(e: any) { + const { index } = e.detail; + const newShapeList = this.data.shapeList.filter((item, i) => i !== index); + this.setData({ + shapeList: newShapeList + }, () => { + // 绘制canvas + this.drawCanvas(); + }); + }, + onColorTap(e: any) { + const { index, fillColor } = e.detail; + console.log('shape Page onColorTap', index, fillColor); + this.setData({ + showColorPopup: true, + currentColor: fillColor, + currentIndex: index + }); + }, + onCloseColorPopup() { + this.setData({ + showColorPopup: false, + currentColor: '', + currentIndex: 0 + }); + }, + onChangeColor(e: any) { + const { color } = e.detail; + const { currentIndex } = this.data; + + this.setData({ + showColorPopup: false, + [`shapeList[${currentIndex}].fillColor`]: color + }, () => { + // 绘制canvas + this.drawCanvas(); + }) + }, + + // 测试ShapeDrawService + // testShapeDrawService() { + // if (!this.shapeDrawService) { + // wx.showToast({ + // title: 'Canvas未初始化', + // icon: 'none' + // }); + // return; + // } + + // // 使用当前选中的图形进行测试 + // const testShapes = this.data.shapeList.slice(0, 3); // 只测试前3个图形 + // if (testShapes.length === 0) { + // wx.showToast({ + // title: '请先选择图形', + // icon: 'none' + // }); + // return; + // } + + // this.shapeDrawService.draw(testShapes); + // wx.showToast({ + // title: '绘制完成', + // icon: 'success' + // }); + // }, + + onShareAppMessage() { + return { + title: '涂鸦丫-涂色|识字|画画|打印', + path: '/pages/index/index', + }; + }, + onShareTimeline() { + return { + title: '涂鸦丫-涂色|识字|画画|打印', + query: '/pages/index/index', + }; + }, + + /** 下载打印 */ + exportToPrint() { + if (this.canvas && this.data.shapeList.length > 0) { + checkAndSaveImage(this.canvas); + return; + } + }, +}); diff --git a/miniprogram/pages/shape/index.wxml b/miniprogram/pages/shape/index.wxml index 711414d..a0e98a4 100644 --- a/miniprogram/pages/shape/index.wxml +++ b/miniprogram/pages/shape/index.wxml @@ -24,7 +24,7 @@ wx:for-item="item" wx:for-index="index" wx:key="{{index}}" - key="{{index}}" + index="{{index}}" svg="{{ item.svg }}" fillColor="{{ item.fillColor }}" bind:onShapeTap="openSelectShapePopup" @@ -34,7 +34,49 @@ + + + + 预览打印效果 + + + + + + + + 分享 + + + 下载打印 + diff --git a/miniprogram/service/README.md b/miniprogram/service/README.md new file mode 100644 index 0000000..8290cc5 --- /dev/null +++ b/miniprogram/service/README.md @@ -0,0 +1,89 @@ +# ShapeDrawService 使用说明 + +## 概述 + +ShapeDrawService 是一个用于绘制图形涂色A4打印页的服务类,参考了 TextDrawService 的结构设计。 + +## 功能特性 + +- 支持最多6个图形的涂色页面绘制 +- 包含三个部分:标题区域、示例区域、练习区域 +- 自动避免图形重叠,保持适当间距 +- 智能边界检查:距离A4纸下边小于20px时自动停止绘制,左右边距100px,上边距距离分割线20px +- 使用Canvas API直接绘制图形,性能更好 +- 支持多种图形类型:三角形、四边形、多边形、圆形、椭圆、心形、五角星等 + +## 使用方法 + +### 1. 初始化 + +```typescript +import ShapeDrawService from '../../service/shapeDrawService'; + +// 在页面中初始化 +const canvas = res[0].node; +const ctx = canvas.getContext('2d'); +this.shapeDrawService = new ShapeDrawService(canvas, ctx); +``` + +### 2. 绘制页面 + +```typescript +// 准备图形数据 +const shapes: ShapeCard[] = [ + { + id: 'circle', + name: '圆形', + type: 'circle', + svg: '', + fillColor: '#ff0000', + }, + // ... 更多图形 +]; + +// 绘制页面 +this.shapeDrawService.draw(shapes); +``` + +### 3. 页面结构 + +绘制的页面包含以下三个部分: + +1. **标题区域**:显示应用名称、提示信息和主标题 +2. **示例区域**:显示选中的图形及其对应的颜色和名称 +3. **练习区域**:随机排列的图形轮廓,供用户涂色 + +## 支持的图形类型 + +- 三角形(钝角、直角、等腰) +- 四边形(正方形、长方形、平行四边形、菱形、梯形) +- 五边形、六边形 +- 五角星、心形 +- 圆形、椭圆、半圆、扇形、圆环 + +## 配置选项 + +可以通过构造函数传入配置选项: + +```typescript +const options = { + appName: '自定义应用名', + appHint: '自定义提示', + title: '自定义标题', + subTitle: '自定义副标题', +}; + +this.shapeDrawService = new ShapeDrawService(canvas, ctx, options); +``` + +## 注意事项 + +1. 最多支持6个图形同时绘制 +2. 图形会自动避免重叠,保持适当间距 +3. 练习区域的图形会随机旋转,增加趣味性 +4. 智能边界检查:自动确保图形不会超出A4纸边界(下边距20px,左右边距100px,上边距距离分割线20px) +5. 支持不同的打印头部类型(wechat、noLogoImage、LogoImage、minimal) + +## 示例 + +参考 `miniprogram/pages/shape/index.ts` 中的实现,展示了如何在页面中使用 ShapeDrawService。 diff --git a/miniprogram/service/drawShape.ts b/miniprogram/service/drawShape.ts new file mode 100644 index 0000000..7ceb6c2 --- /dev/null +++ b/miniprogram/service/drawShape.ts @@ -0,0 +1,385 @@ +import { ShapeCard } from '../constants/shapes'; + +// 绘制各种图形的辅助函数 + +// 绘制圆形 +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') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +// 绘制椭圆形 +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') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +// 绘制正方形 +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); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.strokeRect(x - size / 2, y - size / 2, size, size); +} + +// 绘制矩形 +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); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.strokeRect(x - width / 2, y - height / 2, width, height); +} + +// 绘制三角形(可指定类型:钝角、直角、锐角) +function drawTriangle(ctx: RenderingContext, x: number, y: number, size: number, type: string, fillColor: string) { + ctx.beginPath(); + + switch (type) { + case 'obtuse': + ctx.moveTo(x - size * 0.6, y - size * 0.4); + ctx.lineTo(x + size * 0.8, y + size * 0.6); + ctx.lineTo(x - size * 0.2, y + size * 0.4); + break; + case 'right': + ctx.moveTo(x - size * 0.4, y - size * 0.4); + ctx.lineTo(x - size * 0.4, y + size * 0.6); + ctx.lineTo(x + size * 0.6, y + size * 0.6); + break; + case 'acute': + default: + ctx.moveTo(x, y - size * 0.5); + ctx.lineTo(x - size * 0.5, y + size * 0.5); + ctx.lineTo(x + size * 0.5, y + size * 0.5); + break; + } + + ctx.closePath(); + if (fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +// 绘制平行四边形 +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); + ctx.lineTo(x + width / 2 + offset, y - height / 2); + ctx.lineTo(x + width / 2 - offset, y + height / 2); + ctx.lineTo(x - width / 2 - offset, y + height / 2); + ctx.closePath(); + if (fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +// 绘制菱形 +function drawDiamond(ctx: RenderingContext, x: number, y: number, size: number, fillColor: string) { + ctx.beginPath(); + ctx.moveTo(x, y - size); + ctx.lineTo(x + size, y); + ctx.lineTo(x, y + size); + ctx.lineTo(x - size, y); + ctx.closePath(); + if (fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +// 绘制梯形 +// 保证绘制的是等腰梯形:上下底居中,左右腰等长 +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 halfHeight = height / 2; + + // 上底中心点与下底中心点重合,左右对称 + ctx.beginPath(); + // 上底左点 + ctx.moveTo(x - topWidth / 2, y - halfHeight); + // 上底右点 + ctx.lineTo(x + topWidth / 2, y - halfHeight); + // 下底右点 + ctx.lineTo(x + bottomWidth / 2, y + halfHeight); + // 下底左点 + ctx.lineTo(x - bottomWidth / 2, y + halfHeight); + ctx.closePath(); + if (fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +// 绘制多边形 +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; + const px = x + Math.cos(angle) * radius; + const py = y + Math.sin(angle) * radius; + if (i === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + } + ctx.closePath(); + if (fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +// 绘制星形(五角星等) +// 按SVG 路径绘制五角星 +function drawStar(ctx: RenderingContext, x: number, y: number, radius: number, points: number, fillColor: string) { + // SVG原始点 + const svgPoints = [ + [50, 20], + [61, 42], + [85, 44], + [67, 60], + [71, 85], + [50, 75], + [29, 85], + [33, 60], + [15, 44], + [39, 42] + ]; + // SVG中心(50,50),最大半径约为35(从50,50到15,44的距离),SVG坐标范围大致为[15,85] + // 归一化到以(x, y)为中心,radius为最大半径 + const svgCenterX = 50; + const svgCenterY = 50; + 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; + if (idx === 0) ctx.moveTo(nx, ny); + else ctx.lineTo(nx, ny); + }); + ctx.closePath(); + if (fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +// 绘制爱心 +function drawHeart(ctx: RenderingContext, x: number, y: number, size: number, fillColor: string) { + // 放大爱心:将原本的缩放比例从60缩小为50,使爱心整体变大 + // 以SVG路径为参考,原始SVG中心为(50,50),宽高约为60x60 + // 这里size为整体缩放,x,y为中心点 + // SVG关键点:M50,35 C35,20 20,35 25,50 C30,65 50,80 50,80 C50,80 70,65 75,50 C80,35 65,20 50,35Z + // 归一化函数 + const scale = 40; // 原来是60,改为50,放大 + function tx(px: number) { + return x + (px - 50) / scale * size; + } + function ty(py: number) { + 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.closePath(); + if (fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +// 绘制半圆 +function drawSemicircle(ctx: RenderingContext, x: number, y: number, radius: number, fillColor: string) { + const verticalOffset = radius * 0.5; + y = y + verticalOffset; + ctx.beginPath(); + // ctx.moveTo(x, y + verticalOffset); + // 圆弧从左(180°,Math.PI)到右(0°),逆时针画半圆,圆弧在正上方 + ctx.arc(x, y, radius, Math.PI, 0, false); + ctx.closePath(); + if (fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +// 绘制扇形,圆弧在正上方 +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.closePath(); + if (fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill(); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); +} + +// 绘制圆环(修正避免中间出现一条线) +function drawRing(ctx: RenderingContext, x: number, y: number, radius: number, fillColor: string) { + const innerRadius = radius * 0.5; + ctx.save(); + ctx.beginPath(); + // 外圆,顺时针 + ctx.arc(x, y, radius, 0, Math.PI * 2, false); + // 关键:移动到内圆起点,避免连接线 + ctx.moveTo(x + innerRadius, y); + // 内圆,逆时针 + ctx.arc(x, y, innerRadius, 0, Math.PI * 2, true); + ctx.closePath(); + if (fillColor !== 'transparent') { + ctx.fillStyle = fillColor; + ctx.fill('evenodd'); + } + ctx.strokeStyle = '#141414'; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.restore(); +} +/** + * 根据图形类型绘制到Canvas + * @param ctx Canvas上下文 + * @param shape 图形对象 + * @param x 起始x坐标 + * @param y 起始y坐标 + * @param size 图形大小 + * @param fillColor 填充颜色 + */ +export function drawShape(ctx: RenderingContext, shape: ShapeCard, x: number, y: number, size: number, fillColor: string) { + ctx.save(); + ctx.translate(x, y); + + const radius = size / 2; + const shouldFill = fillColor !== 'transparent'; + + switch (shape.id) { + case 'circle': + drawCircle(ctx, 0, 0, radius, shouldFill ? fillColor : 'transparent'); + break; + case 'ellipse': + 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'); + break; + case 'rectangle': + 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'); + break; + case 'right-triangle': + drawTriangle(ctx, 0, 0, radius, 'right', shouldFill ? fillColor : 'transparent'); + break; + case 'acute-triangle': + 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'); + break; + case 'diamond': + drawDiamond(ctx, 0, 0, radius, shouldFill ? fillColor : 'transparent'); + break; + case 'trapezoid': + 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'); + break; + case 'hexagon': + drawPolygon(ctx, 0, 0, radius, 6, shouldFill ? fillColor : 'transparent'); + break; + case 'pentagram': + drawStar(ctx, 0, 0, radius, 5, shouldFill ? fillColor : 'transparent'); + break; + case 'heart': + drawHeart(ctx, 0, 0, radius, shouldFill ? fillColor : 'transparent'); + break; + case 'semicircle': + drawSemicircle(ctx, 0, 0, radius, shouldFill ? fillColor : 'transparent'); + break; + case 'sector': + 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'); + } + + ctx.restore(); +} diff --git a/miniprogram/service/shapeDrawService.ts b/miniprogram/service/shapeDrawService.ts new file mode 100644 index 0000000..12bd704 --- /dev/null +++ b/miniprogram/service/shapeDrawService.ts @@ -0,0 +1,371 @@ +import { PAPER_SIZE } from '../constants/colors'; +import { getMiniCodeImage, getImage } from '../utils/index'; +import { ShapeCard } from '../constants/shapes'; +import { drawShape } from './drawShape'; + +/** + * 计算图形在画布上的位置,避免重叠 + * @param canvasWidth 画布宽度 + * @param canvasHeight 画布高度 + * @param shapeCount 图形数量 + * @param shapeSize 图形大小 + * @returns 图形位置数组 + */ +function calculateShapePositions( + canvasWidth: number, + canvasHeight: number, + shapeCount: number, + shapeSize: number, +) { + const positions = []; + const padding = 80; + const minSpacing = shapeSize * 1.5; // 最小间距为图形大小的1.5倍 + + const availableWidth = canvasWidth - 2 * padding; + const availableHeight = canvasHeight - 2 * padding; + + // 计算网格布局 + const cols = Math.ceil(Math.sqrt(shapeCount)); + const rows = Math.ceil(shapeCount / cols); + + const cellWidth = availableWidth / cols; + const cellHeight = availableHeight / rows; + + for (let i = 0; i < shapeCount; i++) { + const row = Math.floor(i / cols); + 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; + + positions.push({ x, y }); + } + + return positions; +} + + +class ShapeDrawService { + headerType: PrintHeader; + canvas: WechatMiniprogram.Canvas; + ctx: RenderingContext; + options: Record; + paperSize: PaperSize; + currentX: number; + currentY: number; + shapes: ShapeCard[]; + + 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, + }; + this.currentX = 0; + this.currentY = 0; + this.shapes = []; + this.headerType = 'wechat'; // 默认值 + this.setPrintConfig(); + } + + setPrintConfig() { + const printConfig = getApp().getPrintConfig(); + this.headerType = printConfig.header; + this.options.appName = printConfig.appName; + } + + draw(shapes: ShapeCard[]) { + this.setPrintConfig(); + this.shapes = shapes.slice(0, 6); // 最多6个图形 + this.clear(); + this.setPaper(); + + if (this.headerType !== 'minimal') { + this.drawHeader(); + } else { + this.drawMiniHeader(); + } + + this.drawLegend(); + this.drawContent(); + } + + 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); + } + + 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); + } + + drawLegend() { + const { canvas, ctx, shapes } = this; + if (shapes.length <= 0) return; + + this.currentY = this.headerType === 'minimal' ? 200 : 304; + const shapeSize = 200; + const rectWidth = 180; + const rectHeight = 80; + // 固定图例的Y位置,不依赖shapeSize + const startY = this.currentY + 125; // 固定距离,不依赖shapeSize + const len = shapes.length; + const canvasWidth = canvas.width; + + // 计算示例图形的间距 + const totalWidth = len * shapeSize + (len - 1) * 40; + const startX = (canvasWidth - totalWidth) / 2 + shapeSize / 2; + + // 绘制所有图形 + shapes.forEach((shape: ShapeCard, index: number) => { + const x = startX + index * (shapeSize + 40); + const y = startY; + + // 绘制示例图形 + drawShape(ctx, shape, x, y, shapeSize, shape.fillColor); + + // // 绘制长方形 + // ctx.fillStyle = '#fff'; + // ctx.strokeStyle = '#000'; + // ctx.lineWidth = 4; + const rectangleX = x - rectWidth / 2; + const rectangleY = y + shapeSize / 2 + 10; + // ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight); + + // 绘制图形名称 + ctx.fillStyle = '#333'; + ctx.font = 'bold 36px "Microsoft Yahei"'; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + ctx.fillText( + shape.name, + rectangleX + rectWidth / 2, + rectangleY + rectHeight / 2, + rectWidth, + ); + }); + + this.currentY = this.headerType === 'minimal' ? 532 : 622; + this.drawLine(this.currentY); + } + + drawContent() { + const { canvas, ctx, shapes } = this; + if (shapes.length <= 0) return; + + const minY = this.headerType === 'minimal' ? 532 : 622; + const shapeSize = 180; + const minGap = 40; // 图形之间最小间距 + const bottomMargin = 20; + // 计算内容区起始Y坐标,保证在532或622以下 + const contentTop = minY + 20; // 距离minY线之后20px开始绘制 + const contentHeight = canvas.height - contentTop - bottomMargin; + + // 左右各留100像素边距 + const contentLeft = 100; + const contentWidth = canvas.width - shapeSize; + + // 计算一共要绘制多少个图形 + const repeatCount = 8; + const totalShapes = shapes.length * repeatCount; + + // 生成所有要绘制的图形(打乱顺序,保证随机性) + let allShapes: ShapeCard[] = []; + for (let i = 0; i < repeatCount; i++) { + allShapes = allShapes.concat(shapes); + } + // 洗牌算法彻底打乱 + for (let i = allShapes.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [allShapes[i], allShapes[j]] = [allShapes[j], allShapes[i]]; + } + + // 计算每行最多能放多少个图形(考虑最小间距和边距) + const maxPerRow = Math.floor((contentWidth + minGap) / (shapeSize + minGap)); + const rowCount = Math.ceil(totalShapes / maxPerRow); + + // 计算实际间距(保证左右边距为80,且间距不小于minGap) + const actualPerRow = Math.min(maxPerRow, totalShapes); + const actualGap = actualPerRow > 1 + ? Math.max(minGap, (contentWidth - actualPerRow * shapeSize) / (actualPerRow - 1)) + : 0; + + // 计算每行的Y坐标 - 固定上边距,不使用垂直居中 + const totalRows = rowCount; + let startY = contentTop; // 直接使用contentTop,不进行垂直居中计算 + + // 生成所有图形的位置 + let positions: { x: number, y: number }[] = []; + let shapeIdx = 0; + + const canvasHeight = this.canvas.height; + + for (let row = 0; row < totalRows; row++) { + // 本行实际要放多少个图形 + const shapesInThisRow = Math.min(actualPerRow, totalShapes - shapeIdx); + // 本行实际间距 + const gap = shapesInThisRow > 1 + ? Math.max(minGap, (contentWidth - shapesInThisRow * shapeSize) / (shapesInThisRow - 1)) + : 0; + // 本行起始X + let startX = contentLeft; + // 居中对齐 + if (shapesInThisRow > 1) { + const rowWidth = shapesInThisRow * shapeSize + (shapesInThisRow - 1) * gap; + startX = contentLeft + (contentWidth - rowWidth) / 2; + } else { + startX = contentLeft + (contentWidth - shapeSize) / 2; + } + + // 计算当前行的Y坐标 + const currentRowY = startY + row * (shapeSize + minGap) + shapeSize / 2; + + // 检查当前行是否会超出下边界 + if (currentRowY + shapeSize / 2 > canvasHeight - bottomMargin) { + break; // 停止生成更多行 + } + + for (let col = 0; col < shapesInThisRow; col++) { + positions.push({ + x: startX + col * (shapeSize + gap) + shapeSize / 2, + y: currentRowY + }); + shapeIdx++; + if (shapeIdx >= totalShapes) break; + } + + if (shapeIdx >= totalShapes) break; + } + + // 再次彻底打乱位置顺序,保证排列无规律 + for (let i = positions.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [positions[i], positions[j]] = [positions[j], positions[i]]; + } + + // 记录实际绘制的图形数量 + console.log(`绘制图形:计划${totalShapes}个,实际${positions.length}个,边界限制:距离下边${bottomMargin}px`); + + // 绘制所有图形 + positions.forEach((pos, index) => { + const shape = allShapes[index]; + + // 随机旋转角度 + const rotation = (Math.random() - 0.5) * 0.5; // ±0.25弧度 + + ctx.save(); + ctx.translate(pos.x, pos.y); + ctx.rotate(rotation); + + drawShape(ctx, shape, 0, 0, shapeSize, 'transparent'); + + ctx.restore(); + }); + } + + 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 ShapeDrawService; diff --git a/miniprogram/service/textDrawService.ts b/miniprogram/service/textDrawService.ts index d3da64f..4228284 100644 --- a/miniprogram/service/textDrawService.ts +++ b/miniprogram/service/textDrawService.ts @@ -50,7 +50,7 @@ function calculateCircleCenters( } class TextDrawService { - headerType: PrintHeader; + headerType: PrintHeader = 'wechat'; canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; options: Record;