/** * 数一数比大小绘制服务 */ import { BaseDrawService } from '../../../service/baseDraw'; import { getImage } from '../../../utils/index'; /** * 数一数比大小数据 */ export interface CompareData { problems: Array<{ leftCount: number; rightCount: number; leftImageIndex: number; rightImageIndex: number; imageType: 'fruits' | 'twelve-animals'; }>; } /** * 数一数比大小绘制服务 */ class CompareDraw extends BaseDrawService { compareData: CompareData | null = null; constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { super(canvas, ctx, options); this.compareData = null; } /** * 绘制数一数比大小内容 */ async draw(compareData: CompareData) { if (!compareData || !compareData.problems) { return; } this.compareData = compareData; this.prepareDraw(); await this.drawHeaderAndDivider(); await this.drawContent({ canvas: this.canvas, ctx: this.ctx, compareData: this.compareData, canvasWidth: this.canvasWidth, startY: this.currentY, }); } /** * 绘制内容区域 */ private async drawContent(params: { canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; compareData: CompareData; canvasWidth: number; startY: number; }): Promise { const { canvas, ctx, compareData, canvasWidth, startY } = params; const { problems } = compareData; // 布局参数 const leftMargin = 30; const rightMargin = 30; const topMargin = 12; const rowSpacing = 116; // 行之间的间距 const columnSpacing = 40; // 两列之间的间距(可调节) // 计算每列的宽度(减去边距和列间距) const availableWidth = canvasWidth - leftMargin - rightMargin - columnSpacing; const columnWidth = availableWidth / 2; // 框的尺寸 const boxWidth = 100; const boxHeight = 100; const borderRadius = 12; const circleRadius = 18; // 圆圈半径 // 框与圆圈之间的间距 const boxToCircleSpacing = 12; // 计算左侧列和右侧列的起始X位置 const leftColumnStartX = leftMargin; const rightColumnStartX = leftMargin + columnWidth + columnSpacing; // 计算每列内部的布局(左框 | 圆圈 | 右框,居中) const totalWidthPerColumn = boxWidth + boxToCircleSpacing + circleRadius * 2 + boxToCircleSpacing + boxWidth; const columnPadding = (columnWidth - totalWidthPerColumn) / 2; let currentY = startY + topMargin; // 固定绘制6行 const totalRows = 6; for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) { const rowY = currentY + rowIndex * rowSpacing; // 绘制左侧列 const leftProblemIndex = rowIndex * 2; if (leftProblemIndex < problems.length) { await this.drawCompareProblem( ctx, canvas, problems[leftProblemIndex], leftColumnStartX + columnPadding, rowY, boxWidth, boxHeight, borderRadius, circleRadius, boxToCircleSpacing, ); } // 绘制右侧列 const rightProblemIndex = rowIndex * 2 + 1; if (rightProblemIndex < problems.length) { await this.drawCompareProblem( ctx, canvas, problems[rightProblemIndex], rightColumnStartX + columnPadding, rowY, boxWidth, boxHeight, borderRadius, circleRadius, boxToCircleSpacing, ); } } } /** * 绘制一个比较问题(左框 | 圆圈 | 右框) */ private async drawCompareProblem( ctx: RenderingContext, canvas: WechatMiniprogram.Canvas, problem: { leftCount: number; rightCount: number; leftImageIndex: number; rightImageIndex: number; imageType: 'fruits' | 'twelve-animals'; }, startX: number, startY: number, boxWidth: number, boxHeight: number, borderRadius: number, circleRadius: number, boxToCircleSpacing: number, ) { const { leftCount, rightCount, leftImageIndex, rightImageIndex, imageType, } = problem; // 图片配置 const imageConfig = { 'twelve-animals': { folder: 'twelve-animals', maxIndex: 12, }, fruits: { folder: 'fruits', maxIndex: 22, }, }; const config = imageConfig[imageType] || imageConfig['twelve-animals']; // 计算左框位置 const leftBoxX = startX; const leftBoxY = startY; // 计算圆圈位置 const circleX = leftBoxX + boxWidth + boxToCircleSpacing + circleRadius; const circleY = startY + boxHeight / 2; // 计算右框位置 const rightBoxX = circleX + circleRadius + boxToCircleSpacing; const rightBoxY = startY; // 绘制左框 ctx.strokeStyle = '#999'; ctx.lineWidth = 1; this.drawRoundedRectInternal( ctx, leftBoxX, leftBoxY, boxWidth, boxHeight, borderRadius, true, ); // 绘制左框内的图片 await this.drawImagesInBox( ctx, canvas, leftCount, leftImageIndex, config.folder, leftBoxX, leftBoxY, boxWidth, boxHeight, ); // 绘制实心圆圈 ctx.fillStyle = '#fff'; ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(circleX, circleY, circleRadius, 0, Math.PI * 2); ctx.fill(); // 先填充白色 ctx.stroke(); // 再绘制边框 // 绘制右框 ctx.strokeStyle = '#999'; ctx.lineWidth = 1; this.drawRoundedRectInternal( ctx, rightBoxX, rightBoxY, boxWidth, boxHeight, borderRadius, true, ); // 绘制右框内的图片 await this.drawImagesInBox( ctx, canvas, rightCount, rightImageIndex, config.folder, rightBoxX, rightBoxY, boxWidth, boxHeight, ); } /** * 在框内绘制多张图片 */ private async drawImagesInBox( ctx: RenderingContext, canvas: WechatMiniprogram.Canvas, count: number, imageIndex: number, folder: string, boxX: number, boxY: number, boxWidth: number, boxHeight: number, ) { const padding = 8; // 框内边距 const availableWidth = boxWidth - padding * 2; const availableHeight = boxHeight - padding * 2; // 根据数量确定每行的图片数和图片大小 let imagesPerRow: number; let imageSize: number; if (count <= 4) { imagesPerRow = count <= 2 ? count : 2; imageSize = Math.min(availableWidth / imagesPerRow, availableHeight / 2) - 4; } else if (count <= 6) { imagesPerRow = 3; imageSize = Math.min(availableWidth / 3, availableHeight / 2) - 4; } else { imagesPerRow = 3; imageSize = Math.min(availableWidth / 3, availableHeight / 3) - 4; } const rows = Math.ceil(count / imagesPerRow); const imageSpacing = (availableWidth - imageSize * imagesPerRow) / (imagesPerRow + 1); const rowSpacing = rows > 1 ? (availableHeight - imageSize * rows) / (rows + 1) : 0; // 加载图片 let boxImage: any = null; try { const imagePath = `/mathPages/shared/assets/${folder}/${imageIndex}.png`; boxImage = await getImage(canvas, imagePath); } catch (error) { console.error(`加载${folder}/${imageIndex}图片失败:`, error); return; } // 绘制图片 for (let i = 0; i < count; i++) { const row = Math.floor(i / imagesPerRow); const col = i % imagesPerRow; const imageX = boxX + padding + imageSpacing + col * (imageSize + imageSpacing); const imageY = boxY + padding + (rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) + row * (imageSize + (rows > 1 ? rowSpacing : 0)); if (boxImage) { // 计算图片高度(等比例缩放) // @ts-ignore - 微信小程序图片对象有 width 和 height 属性 const scaledHeight = (boxImage.height / boxImage.width) * imageSize; ctx.drawImage( boxImage, imageX, imageY, imageSize, scaledHeight, ); } } } /** * 绘制圆角矩形(虚线边框) */ private drawRoundedRectInternal( ctx: RenderingContext, x: number, y: number, width: number, height: number, radius: number, isDashed: boolean = true, ) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo( x + width, y + height, x + width - radius, y + height, ); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); if (isDashed) { ctx.setLineDash([6 / 3, 6 / 3]); // 约2px的虚线 } else { ctx.setLineDash([]); } ctx.stroke(); ctx.setLineDash([]); // 重置为实线 } } export default CompareDraw;