/** * 数一数选一选/填一填绘制服务 * 支持两种模式:'select'(选一选)和 'fill'(填一填) */ import { BaseDrawService } from '../../../service/baseDraw'; import { getImage } from '../../../utils/index'; import { getRandomNumberColor } from '../../../constants/colors'; /** * 数一数选一选/填一填数据 */ export interface CountingSelectData { problems: Array<{ count: number; imageIndex: number; imageType: 'fruits' | 'twelve-animals'; options?: number[]; // 选一选模式需要,填一填模式不需要 correctIndex?: number; // 选一选模式需要 }>; } /** * 数一数选一选/填一填绘制服务 */ class CountingSelectDraw extends BaseDrawService { countingData: CountingSelectData | null = null; mode: 'select' | 'fill'; constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { super(canvas, ctx, options); this.countingData = null; this.mode = 'select'; // 默认选一选模式 } /** * 绘制数一数选一选/填一填内容 */ async draw( countingData: CountingSelectData, mode: 'select' | 'fill' = 'select', ) { if (!countingData || !countingData.problems) { return; } this.countingData = countingData; this.mode = mode; this.prepareDraw(); await this.drawHeaderAndDivider(); await this.drawContent({ canvas: this.canvas, ctx: this.ctx, countingData: this.countingData, mode: this.mode, canvasWidth: this.canvasWidth, startY: this.currentY, }); } /** * 绘制内容区域 */ private async drawContent(params: { canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; countingData: CountingSelectData; mode: 'select' | 'fill'; canvasWidth: number; startY: number; }): Promise { const { canvas, ctx, countingData, mode, canvasWidth, startY } = params; const { problems } = countingData; // 布局参数 const leftMargin = 30; const rightMargin = 30; const topMargin = 20; const boxSpacing = 20; // 框之间的间距 // 计算每个框的尺寸 const availableWidth = canvasWidth - leftMargin - rightMargin; const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距 const boxHeight = 210; // 框的高度(增加) // 图片区域高度 const imageAreaHeight = 160; // 底部区域高度(选项表格或括号区域) const bottomAreaHeight = boxHeight - imageAreaHeight; // 选项表格相关(仅 select 模式使用) const optionCellWidth = boxWidth / 3; // 每列宽度(均匀分三列) let currentY = startY + topMargin; // 绘制3x3网格 for (let row = 0; row < 3; row++) { for (let col = 0; col < 3; col++) { const problemIndex = row * 3 + col; if (problemIndex >= problems.length) { continue; } const problem = problems[problemIndex]; // 计算框的位置 const boxX = leftMargin + col * (boxWidth + boxSpacing); const boxY = currentY + row * (boxHeight + boxSpacing); // 绘制一个完整的题目框 await this.drawProblemBox( ctx, canvas, problem, boxX, boxY, boxWidth, boxHeight, imageAreaHeight, bottomAreaHeight, optionCellWidth, mode, ); } } } /** * 绘制一个题目框(图片 + 选项表格 或 括号) */ private async drawProblemBox( ctx: RenderingContext, canvas: WechatMiniprogram.Canvas, problem: { count: number; imageIndex: number; imageType: 'fruits' | 'twelve-animals'; options?: number[]; correctIndex?: number; }, boxX: number, boxY: number, boxWidth: number, boxHeight: number, imageAreaHeight: number, bottomAreaHeight: number, optionCellWidth: number, mode: 'select' | 'fill', ) { const borderRadius = 12; // 绘制实线框 ctx.strokeStyle = '#333'; ctx.lineWidth = 1; this.drawRoundedRectInternal( ctx, boxX, boxY, boxWidth, boxHeight, borderRadius, false, ); // 图片区域 const imageAreaX = boxX; const imageAreaY = boxY; // 绘制图片 await this.drawImagesInBox( ctx, canvas, problem.count, problem.imageIndex, problem.imageType, imageAreaX, imageAreaY, boxWidth, imageAreaHeight, ); // 底部区域 const bottomAreaX = boxX; const bottomAreaY = boxY + imageAreaHeight; // 绘制底部区域的顶部横线(两种模式都需要) ctx.strokeStyle = '#333'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(bottomAreaX, bottomAreaY); ctx.lineTo(bottomAreaX + boxWidth, bottomAreaY); ctx.stroke(); if (mode === 'select' && problem.options) { // 选一选模式:绘制三个数字选项(表格样式) // 绘制三个单元格(均匀分三列) for (let i = 0; i < problem.options.length; i++) { const cellX = bottomAreaX + i * optionCellWidth; const cellY = bottomAreaY; // 绘制单元格边框(右边框,除了最后一个) if (i < problem.options.length - 1) { ctx.beginPath(); ctx.moveTo(cellX + optionCellWidth, cellY); ctx.lineTo( cellX + optionCellWidth, cellY + bottomAreaHeight, ); ctx.stroke(); } // 绘制数字(居中) const numberX = cellX + optionCellWidth / 2; const numberY = cellY + bottomAreaHeight / 2; ctx.fillStyle = getRandomNumberColor(); ctx.font = `bold ${28}px "Microsoft Yahei"`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(String(problem.options[i]), numberX, numberY); } } else if (mode === 'fill') { // 填一填模式:绘制括号 "( )" const bracketFontSize = 28; const bracketSpacing = 46; // 左右括号之间的间距(留空区域) // 计算括号位置(居中) const bracketY = bottomAreaY + bottomAreaHeight / 2; const centerX = boxX + boxWidth / 2; // 左括号 ctx.fillStyle = '#333'; ctx.font = `${bracketFontSize}px "Microsoft Yahei"`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const leftBracketX = centerX - bracketSpacing / 2; ctx.fillText('(', leftBracketX, bracketY); // 右括号 const rightBracketX = centerX + bracketSpacing / 2; ctx.fillText(')', rightBracketX, bracketY); } } /** * 在框内绘制多张图片 */ private async drawImagesInBox( ctx: RenderingContext, canvas: WechatMiniprogram.Canvas, count: number, imageIndex: number, imageType: 'fruits' | 'twelve-animals', boxX: number, boxY: number, boxWidth: number, boxHeight: number, ) { // 图片配置 const imageConfig = { 'twelve-animals': { folder: 'twelve-animals', maxIndex: 12, }, fruits: { folder: 'fruits', maxIndex: 22, }, }; const config = imageConfig[imageType] || imageConfig['twelve-animals']; 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/${config.folder}/${imageIndex}.png`; boxImage = await getImage(canvas, imagePath); } catch (error) { console.error(`加载${config.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 CountingSelectDraw;