/** * 数物对应绘制服务 */ import { BaseDrawService } from '../../../service/baseDraw'; import { getImage } from '../../../utils/index'; /** * 数物对应数据 */ export interface NumberObjectMatchData { /** 上面区域的图片配置(4行4列,共16张) */ gridImages: Array<{ imageIndex: number; folder: 'fruits' | 'twelve-animals'; offsetX: number; // 随机左右偏移 offsetY: number; // 随机上下偏移 }>; /** 下面区域第一行的4张图片配置 */ bottomImages: Array<{ imageIndex: number; folder: 'fruits' | 'twelve-animals'; }>; /** 第二行要显示的数量(乱序排列) */ bottomNumbers: number[]; } /** * 数物对应绘制服务 */ class NumberObjectMatchDraw extends BaseDrawService { matchData: NumberObjectMatchData | null = null; constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { super(canvas, ctx, options); this.matchData = null; } /** * 绘制数物对应内容 */ async draw(matchData: NumberObjectMatchData) { if ( !matchData || !matchData.gridImages || !matchData.bottomImages || !matchData.bottomNumbers ) { return; } this.matchData = matchData; this.prepareDraw(); await this.drawHeaderAndDivider(); await this.drawContent({ canvas: this.canvas, ctx: this.ctx, matchData: this.matchData, canvasWidth: this.canvasWidth, startY: this.currentY, }); } /** * 绘制内容区域 */ private async drawContent(params: { canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; matchData: NumberObjectMatchData; canvasWidth: number; startY: number; }): Promise { const { canvas, ctx, matchData, canvasWidth } = params; let { startY } = params; startY = startY + 25; const margin = 0; // 左右边距 const imagePadding = 10; // 图片内边距 const imageSize = 75; // 图片大小 const cellSize = imageSize + imagePadding * 2; // 每个格子的大小 // ========== 绘制上面区域(4行4列) ========== const gridCols = 5; const gridRows = 4; const gridWidth = cellSize * gridCols; const gridStartX = margin + (canvasWidth - margin * 2 - gridWidth) / 2; // 居中 const gridStartY = startY; // 加载并绘制所有图片 const imagePromises = matchData.gridImages.map(async (item, index) => { const row = Math.floor(index / gridCols); const col = index % gridCols; const baseX = gridStartX + col * cellSize + cellSize / 2; const baseY = gridStartY + row * cellSize + cellSize / 2; // 加上随机偏移 const imageX = baseX + item.offsetX; const imageY = baseY + item.offsetY; // 加载图片 let image: any = null; try { const imagePath = `/mathPages/shared/assets/${item.folder}/${item.imageIndex}.png`; image = await getImage(canvas, imagePath); } catch (error) { console.error( `加载图片失败: ${item.folder}/${item.imageIndex}`, error, ); return; } if (image) { // 计算图片高度(等比例缩放) // @ts-ignore - 微信小程序图片对象有 width 和 height 属性 const scaledHeight = (image.height / image.width) * imageSize; // 绘制图片(居中) ctx.drawImage( image, imageX - imageSize / 2, imageY - scaledHeight / 2, imageSize, scaledHeight, ); } }); await Promise.all(imagePromises); // ========== 绘制虚线分割线 ========== const dividerY = gridStartY + gridRows * cellSize + 30; this.drawDashedDivider(dividerY, margin); // ========== 绘制下面区域 ========== const bottomStartY = dividerY + 30; const bottomImageSize = 50; // 下面图片大小 const bottomBoxSize = bottomImageSize + imagePadding * 2; // 图片框大小 const bottomSpacing = 40; // 图片间距 const bottomRowSpacing = 80; // 两行间距 // 计算下面区域的总宽度(4个图片框 + 3个间距) const bottomTotalWidth = bottomBoxSize * 4 + bottomSpacing * 3; const bottomStartX = margin + (canvasWidth - margin * 2 - bottomTotalWidth) / 2; // 居中 // 第一行:绘制图片框和图片 for (let i = 0; i < matchData.bottomImages.length; i++) { const item = matchData.bottomImages[i]; const boxX = bottomStartX + i * (bottomBoxSize + bottomSpacing); const boxY = bottomStartY; const imageX = boxX + imagePadding; const imageY = boxY + imagePadding; // 绘制方框(边线宽度2) ctx.strokeStyle = '#999'; ctx.lineWidth = 2; ctx.strokeRect(boxX, boxY, bottomBoxSize, bottomBoxSize); // 加载并绘制图片 let image: any = null; try { const imagePath = `/mathPages/shared/assets/${item.folder}/${item.imageIndex}.png`; image = await getImage(canvas, imagePath); } catch (error) { console.error( `加载图片失败: ${item.folder}/${item.imageIndex}`, error, ); continue; } if (image) { // 计算图片高度(等比例缩放) // @ts-ignore - 微信小程序图片对象有 width 和 height 属性 const scaledHeight = (image.height / image.width) * bottomImageSize; // 绘制图片(居中) const drawImageX = imageX + (bottomImageSize - bottomImageSize) / 2; const drawImageY = imageY + (bottomImageSize - scaledHeight) / 2; ctx.drawImage( image, drawImageX, drawImageY, bottomImageSize, scaledHeight, ); } } // 绘制第一行方框下方的圆点(间隔5) const dotRadius = 4; // 圆点半径 const dotSpacing = 10; // 圆点与方框的间距 const topDotY = bottomStartY + bottomBoxSize + dotSpacing; for (let i = 0; i < matchData.bottomImages.length; i++) { const boxX = bottomStartX + i * (bottomBoxSize + bottomSpacing); const dotX = boxX + bottomBoxSize / 2; // 方框正中间 // 绘制圆点 ctx.fillStyle = '#93D333'; ctx.beginPath(); ctx.arc(dotX, topDotY, dotRadius, 0, Math.PI * 2); ctx.fill(); } // 第二行:绘制数字输入框(4个方框) const numberBoxY = bottomStartY + bottomBoxSize + bottomRowSpacing; const numberBoxSize = bottomBoxSize; // 数字框大小和图片框一样 // 绘制第二行方框上方的圆点(间隔5) const bottomDotY = numberBoxY - dotSpacing; // 先绘制所有圆点 for (let i = 0; i < 4; i++) { const boxX = bottomStartX + i * (numberBoxSize + bottomSpacing); const dotX = boxX + numberBoxSize / 2; // 方框正中间 // 绘制圆点 ctx.fillStyle = '#93D333'; ctx.beginPath(); ctx.arc(dotX, bottomDotY, dotRadius, 0, Math.PI * 2); ctx.fill(); } // 再绘制方框和数字 for (let i = 0; i < 4; i++) { const boxX = bottomStartX + i * (numberBoxSize + bottomSpacing); // 绘制方框(边线宽度2) ctx.strokeStyle = '#999'; ctx.lineWidth = 2; ctx.strokeRect(boxX, numberBoxY, numberBoxSize, numberBoxSize); // 绘制数字(如果存在) if ( matchData.bottomNumbers && matchData.bottomNumbers[i] !== undefined ) { const number = matchData.bottomNumbers[i]; ctx.fillStyle = '#141414'; ctx.font = `bold ${48}px "Microsoft Yahei"`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText( String(number), boxX + numberBoxSize / 2, numberBoxY + numberBoxSize / 2, ); } } } } export default NumberObjectMatchDraw;