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 ROW_COUNT = 6; // 每行之间的垂直间距 const VERTICAL_GAP = 120; // 图形大小 const shapeSize = 180; // 左右边距 const leftMargin = 160; const rightMargin = 160; // 以 drawLegend 画完后的分割线作为基准,内容区顶部间距 50 const legendBottomY = this.currentY; const topGap = 60; const contentTop = legendBottomY + topGap; // 可用宽度 const contentWidth = canvas.width - leftMargin - rightMargin; // 每行最多能放多少个图形(考虑最小间距40) const minGap = 40; const maxPerRow = Math.floor((contentWidth + minGap) / (shapeSize + minGap)); // 总共要绘制多少个图形(每行都填满) const totalShapes = maxPerRow * ROW_COUNT; // 生成所有要绘制的图形(打乱顺序,保证随机性) let allShapes: ShapeCard[] = []; for (let i = 0; i < Math.ceil(totalShapes / shapes.length); i++) { allShapes = allShapes.concat(shapes); } allShapes = allShapes.slice(0, totalShapes); // 洗牌算法彻底打乱 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]]; } // 生成所有图形的位置 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 gap = maxPerRow > 1 ? Math.max(minGap, (contentWidth - maxPerRow * shapeSize) / (maxPerRow - 1)) : 0; // 本行起始X(水平居中) let startX = leftMargin; if (maxPerRow > 1) { const rowWidth = maxPerRow * shapeSize + (maxPerRow - 1) * gap; startX = leftMargin + (contentWidth - rowWidth) / 2; } else { startX = leftMargin + (contentWidth - shapeSize) / 2; } for (let col = 0; col < maxPerRow; col++) { positions.push({ x: startX + col * (shapeSize + gap) + shapeSize / 2, y: y }); } } // 修正:只保留前 totalShapes 个位置,防止多出一行 positions = positions.slice(0, totalShapes); // 再次彻底打乱位置顺序,保证排列无规律 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}个;每行${maxPerRow}个,共${ROW_COUNT}行;边距 左右=${leftMargin}/${rightMargin},顶部=${topGap}(基于legend底线)`); // 绘制所有图形 positions.forEach((pos, index) => { const shape = allShapes[index]; // 随机旋转角度,范围为±90度(即最大180度) const rotation = (Math.random() - 0.5) * Math.PI; // ±90度 = ±π/2 弧度,Math.PI为180度 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;