/** * 数一数连一连绘制服务 */ import { BaseDrawService } from '../../../service/baseDraw'; import { getImage } from '../../../utils/index'; import { getRandomNumberColor } from '../../../constants/colors'; /** * 数一数连一连数据 */ export interface CountMatchData { leftNumbers: number[]; rightNumbers: number[]; // 打乱顺序后的数字数组 } /** * 数一数连一连绘制服务 */ class CountMatchDraw extends BaseDrawService { matchData: CountMatchData | null = null; constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { super(canvas, ctx, options); this.matchData = null; } /** * 绘制数一数连一连内容 */ async draw( matchData: CountMatchData, imageType: string = 'twelve-animals', ) { if (!matchData || !matchData.leftNumbers || !matchData.rightNumbers) { 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, imageType, }); } /** * 绘制内容区域 */ private async drawContent(params: { canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; matchData: CountMatchData; canvasWidth: number; startY: number; imageType?: string; // 'twelve-animals' 或 'fruits' }): Promise { const { canvas, ctx, matchData, canvasWidth, startY, imageType = 'twelve-animals', } = params; const { leftNumbers, rightNumbers } = matchData; // 根据图片类型确定图片目录和最大索引 const imageConfig = { 'twelve-animals': { folder: 'twelve-animals', maxIndex: 12, }, fruits: { folder: 'fruits', maxIndex: 22, }, }; const config = imageConfig[imageType as keyof typeof imageConfig] || imageConfig['twelve-animals']; const leftMargin = 24; const rightMargin = 24; const itemSpacing = 140; const LeftBoxWidth = 120; const rightBoxWidth = 250; const boxHeight = 110; const borderRadius = 12; const imageSpacing = 5; // 图片之间的水平间距(同一行内) const rowSpacing = 10; // 行之间的垂直间距(两行之间) const imagesPerRow = 5; // 每行最多5张图片 const maxImages = 10; // 最多10张图片 const padding = 10; // 框内边距 const startYPos = startY + 20; const linePointRadius = 8; const linePointSpacing = 12; /** * 根据图片数量动态计算图片宽度 * @param count 图片数量 * @returns 图片宽度 */ const getImageWidth = (count: number): number => { if (count <= 2) { return 90; } else if (count <= 3) { return 70; } else if (count <= 4) { return 55; } else { return 40; } }; // 计算左侧和右侧的起始X位置 const leftBoxX = leftMargin; const rightBoxX = canvasWidth - rightMargin - rightBoxWidth; // 绘制左侧数字框 for (let i = 0; i < leftNumbers.length; i++) { const boxY = startYPos + i * itemSpacing; const number = leftNumbers[i]; // 绘制虚线圆角矩形框 ctx.strokeStyle = '#999'; ctx.lineWidth = 1; this.drawRoundedRectInternal( ctx, leftBoxX, boxY, LeftBoxWidth, boxHeight, borderRadius, true, ); // 绘制数字(使用随机颜色) ctx.fillStyle = getRandomNumberColor(); ctx.font = `bold ${56}px "Microsoft Yahei"`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText( String(number), leftBoxX + LeftBoxWidth / 2, boxY + boxHeight / 2, ); // 绘制左侧框右侧的连线圆点(直径14,半径7) const leftCircleX = leftBoxX + LeftBoxWidth + linePointSpacing + linePointRadius; const leftCircleY = boxY + boxHeight / 2; ctx.fillStyle = '#93D333'; ctx.beginPath(); ctx.arc(leftCircleX, leftCircleY, linePointRadius, 0, Math.PI * 2); ctx.fill(); } // 为每个框生成唯一的图片索引,确保不同框使用不同的图片 const availableImageIndices = Array.from( { length: config.maxIndex }, (_, i) => i + 1, ); const boxImageIndices: number[] = []; for (let i = 0; i < rightNumbers.length; i++) { const randomIndex = Math.floor( Math.random() * availableImageIndices.length, ); const imageIndex = availableImageIndices.splice(randomIndex, 1)[0]; boxImageIndices.push(imageIndex); } // 绘制右侧图片框 for (let i = 0; i < rightNumbers.length; i++) { const boxY = startYPos + i * itemSpacing; const number = rightNumbers[i]; // 这个数字决定了要绘制多少张图片 const boxImageIndex = boxImageIndices[i]; // 这个框使用的图片索引(所有图片都一样) // 绘制虚线圆角矩形框 ctx.strokeStyle = '#999'; ctx.lineWidth = 1; this.drawRoundedRectInternal( ctx, rightBoxX, boxY, rightBoxWidth, boxHeight, borderRadius, true, ); // 计算要绘制的图片数量(最多10张) const imageCount = Math.min(number, maxImages); // 根据图片数量动态计算图片宽度 const imageWidth = getImageWidth(imageCount); // 加载当前框使用的图片(只加载一次,用于计算高度和绘制) let boxImage: any = null; try { const imagePath = `/mathPages/shared/assets/${config.folder}/${boxImageIndex}.png`; boxImage = await getImage(canvas, imagePath); } catch (error) { console.error( `加载${config.folder}/${boxImageIndex}图片失败:`, error, ); } // 计算图片高度(等比例缩放) let imageHeight = imageWidth; // 默认高度 if (boxImage) { // @ts-ignore - 微信小程序图片对象有 width 和 height 属性 imageHeight = (boxImage.height / boxImage.width) * imageWidth; } // 计算每行的图片数量 const imagesInFirstRow = Math.min(imageCount, imagesPerRow); const imagesInSecondRow = imageCount > imagesPerRow ? imageCount - imagesPerRow : 0; // 计算图片的总高度(考虑行数) const rowCount = imagesInSecondRow > 0 ? 2 : 1; const totalImageHeight = rowCount * imageHeight + (rowCount - 1) * rowSpacing; // 垂直居中:框顶部 + 内边距 + (框高度 - 上下内边距 - 图片总高度) / 2 const startImageY = boxY + padding + (boxHeight - padding * 2 - totalImageHeight) / 2; // 计算每行的起始X位置(用于居中) const firstRowImageCount = imagesInFirstRow; const firstRowWidth = firstRowImageCount * imageWidth + (firstRowImageCount - 1) * imageSpacing; const firstRowStartX = rightBoxX + padding + (rightBoxWidth - padding * 2 - firstRowWidth) / 2; const secondRowImageCount = imagesInSecondRow; const secondRowWidth = secondRowImageCount * imageWidth + (secondRowImageCount - 1) * imageSpacing; const secondRowStartX = rightBoxX + padding + (rightBoxWidth - padding * 2 - secondRowWidth) / 2; // 绘制图片(同一个框内使用同一张图片) for (let imgIndex = 0; imgIndex < imageCount; imgIndex++) { const row = Math.floor(imgIndex / imagesPerRow); const col = imgIndex % imagesPerRow; // 计算图片的X位置(根据行数选择不同的起始位置) const rowStartX = row === 0 ? firstRowStartX : secondRowStartX; const imageX = rowStartX + col * (imageWidth + imageSpacing); // 计算图片的Y位置 const imageY = startImageY + row * (imageHeight + rowSpacing); if (boxImage) { // 计算图片高度(等比例缩放) // @ts-ignore - 微信小程序图片对象有 width 和 height 属性 const scaledHeight = (boxImage.height / boxImage.width) * imageWidth; ctx.drawImage( boxImage, imageX, imageY, imageWidth, scaledHeight, ); } else { // 如果图片加载失败,绘制一个小圆点作为备用 ctx.fillStyle = '#ccc'; ctx.beginPath(); ctx.arc( imageX + imageWidth / 2, imageY + imageHeight / 2, imageWidth / 4, 0, Math.PI * 2, ); ctx.fill(); } } // 绘制右侧框左侧的连线圆点(直径14,半径7) const rightCircleX = rightBoxX - linePointSpacing - linePointRadius; // 间距15 + 半径linePointRadius const rightCircleY = boxY + boxHeight / 2; ctx.fillStyle = '#93D333'; ctx.beginPath(); ctx.arc( rightCircleX, rightCircleY, linePointRadius, 0, Math.PI * 2, ); ctx.fill(); } } /** * 绘制圆角矩形(虚线边框) */ 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 CountMatchDraw;