/** * 数物对应绘制服务 */ 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, mode: string = 'match') { if (!matchData || !matchData.gridImages || !matchData.bottomImages) { return; } // 连线模式需要 bottomNumbers,填写模式不需要 if (mode === 'match' && !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, mode: mode, }); } /** * 绘制内容区域 */ private async drawContent(params: { canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; matchData: NumberObjectMatchData; canvasWidth: number; startY: number; mode?: string; // 'match' 连线 或 'fill' 填写 }): Promise { const { canvas, ctx, matchData, canvasWidth, mode = 'match' } = 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); // ========== 绘制下面区域 ========== if (mode === 'fill') { // 填写模式:两行四列网格 await this.drawFillMode({ canvas, ctx, matchData, canvasWidth, dividerY, margin, }); } else { // 连线模式:保持原有逻辑 await this.drawMatchMode({ canvas, ctx, matchData, canvasWidth, dividerY, margin, }); } } /** * 绘制填写模式 */ private async drawFillMode(params: { canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; matchData: NumberObjectMatchData; canvasWidth: number; dividerY: number; margin: number; }): Promise { const { canvas, ctx, matchData, canvasWidth, dividerY, margin } = params; const bottomStartY = dividerY + 30; const bottomImageSize = 75; // 下面图片大小 const imagePadding = 10; // 图片内边距 const bottomBoxSize = bottomImageSize + imagePadding * 2; // 图片框大小 const gridRows = 2; // 两行 const gridCols = 4; // 四列 const cellSize = bottomBoxSize; // 格子大小 // 计算网格总宽度 const gridWidth = cellSize * gridCols; const gridStartX = margin + (canvasWidth - margin * 2 - gridWidth) / 2; // 居中 // 绘制网格线 this.drawGridLines( gridStartX, bottomStartY, cellSize, cellSize, gridCols, gridRows, ); // 第一行:绘制图片 for (let i = 0; i < matchData.bottomImages.length; i++) { const item = matchData.bottomImages[i]; const col = i; const boxX = gridStartX + col * cellSize; const boxY = bottomStartY; const imageX = boxX + imagePadding; const imageY = boxY + imagePadding; // 加载并绘制图片 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, ); } } // 第二行:空着,让小朋友填写数字(只绘制网格线,不绘制内容) } /** * 绘制连线模式 */ private async drawMatchMode(params: { canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; matchData: NumberObjectMatchData; canvasWidth: number; dividerY: number; margin: number; }): Promise { const { canvas, ctx, matchData, canvasWidth, dividerY, margin } = params; const bottomStartY = dividerY + 30; const bottomImageSize = 50; // 下面图片大小 const imagePadding = 10; // 图片内边距 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;