/** * 加减法计算绘制服务 */ import { BaseDrawService } from '../../../service/baseDraw'; import { getImage } from '../../../utils/index'; /** * 加减法计算数据 */ export interface AdditionData { problems: Array<{ type: 'addition' | 'subtraction'; left: number; right: number; result: number; }>; } /** * 加减法计算绘制服务 */ class AdditionDraw extends BaseDrawService { calculationData: AdditionData | null = null; constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { super(canvas, ctx, options); this.calculationData = null; } /** * 绘制加减法计算内容 */ async draw( calculationData: AdditionData, _calculationType: string = 'addition-5', // 保留参数以保持接口兼容性 _imageType?: string, // 可选,不再使用,每个运算会随机选择图片类型 ) { if (!calculationData || !calculationData.problems) { return; } this.calculationData = calculationData; this.prepareDraw(); await this.drawHeaderAndDivider(); await this.drawContent({ canvas: this.canvas, ctx: this.ctx, problems: this.calculationData.problems, canvasWidth: this.canvasWidth, startY: this.currentY, }); } /** * 绘制内容区域 */ private async drawContent(params: { canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; problems: Array<{ type: 'addition' | 'subtraction'; left: number; right: number; result: number; }>; canvasWidth: number; startY: number; }): Promise { const { canvas, ctx, problems, canvasWidth, startY } = params; // 根据图片类型确定图片目录和最大索引 const imageConfig = { 'twelve-animals': { folder: 'twelve-animals', maxIndex: 12, }, fruits: { folder: 'fruits', maxIndex: 22, }, }; const leftMargin = 24; const rightMargin = 24; const itemSpacing = 140; // 调整左右框宽度比例:6:4 (约420:280) const leftBoxWidth = 334; // 左侧图片框固定宽度 const rightBoxWidth = 196; // 右侧答案框固定宽度 const boxHeight = 110; const borderRadius = 12; const imageSpacing = 0; // 图片之间的间距(水平方向) const rowSpacing = 4; // 第一行和第二行之间的间距(垂直方向) const padding = 15; // 框内边距 const startYPos = startY + 20; const plusSignSize = 20; // 加号大小(缩小) const plusSignSpacing = 15; // 加号左右间距(方便修改和调试) // 计算左侧和右侧的起始X位置 const leftBoxX = leftMargin; const rightBoxX = canvasWidth - rightMargin - rightBoxWidth; // 为每个题目随机选择图片类型和图片索引 const problemImageConfigs: Array<{ type: 'twelve-animals' | 'fruits'; imageIndex: number; }> = []; for (let i = 0; i < problems.length; i++) { // 随机选择图片类型 const imageTypes: ('twelve-animals' | 'fruits')[] = [ 'twelve-animals', 'fruits', ]; const randomType = imageTypes[Math.floor(Math.random() * imageTypes.length)]; const config = imageConfig[randomType]; // 为当前题目生成唯一的图片索引 const availableImageIndices = Array.from( { length: config.maxIndex }, (_, i) => i + 1, ); const randomIndex = Math.floor( Math.random() * availableImageIndices.length, ); const imageIndex = availableImageIndices[randomIndex]; problemImageConfigs.push({ type: randomType, imageIndex, }); } // 绘制每一行的内容 for (let i = 0; i < problems.length; i++) { const problem = problems[i]; const boxY = startYPos + i * itemSpacing; const imageConfigItem = problemImageConfigs[i]; const config = imageConfig[imageConfigItem.type]; // 根据图片类型获取图片大小 const imageSize = this.getImageSize(imageConfigItem.type); // 加载图片 let image: any = null; try { const imagePath = `/mathPages/shared/assets/${config.folder}/${imageConfigItem.imageIndex}.png`; image = await getImage(canvas, imagePath); } catch (error) { console.error('图片加载失败:', error); } // ========== 绘制左侧图片框 ========== ctx.strokeStyle = '#999'; ctx.lineWidth = 1; this.drawRoundedRectInternal( ctx, leftBoxX, boxY, leftBoxWidth, boxHeight, borderRadius, true, ); const leftBoxCenterY = boxY + boxHeight / 2; const leftBoxContentX = leftBoxX + padding; if (problem.type === 'addition') { // 加法:左侧图片 + 加号 + 右侧图片 const leftImageCount = problem.left; const rightImageCount = problem.right; /** * 根据数量计算每行最多展示的图片数 */ const getMaxImagesPerRow = (count: number): number => { if (count <= 6) return 3; if (count <= 8) return 4; return 5; // 8-10 }; const leftMaxPerRow = getMaxImagesPerRow(leftImageCount); const rightMaxPerRow = getMaxImagesPerRow(rightImageCount); const leftRows = Math.min( 2, Math.ceil(leftImageCount / leftMaxPerRow), ); const rightRows = Math.min( 2, Math.ceil(rightImageCount / rightMaxPerRow), ); // 计算左侧图片区域的宽度和高度 // 需要根据实际行数计算每行的最大宽度 const leftFirstRowCount = Math.min( leftImageCount, leftMaxPerRow, ); const leftSecondRowCount = Math.max( 0, Math.min(leftMaxPerRow, leftImageCount - leftMaxPerRow), ); const leftImagesWidth = Math.max( leftFirstRowCount * imageSize + (leftFirstRowCount - 1) * imageSpacing, leftSecondRowCount * imageSize + (leftSecondRowCount - 1) * imageSpacing, ) || imageSize; // 至少为 imageSize // 计算右侧图片区域的宽度和高度 const rightFirstRowCount = Math.min( rightImageCount, rightMaxPerRow, ); const rightSecondRowCount = Math.max( 0, Math.min(rightMaxPerRow, rightImageCount - rightMaxPerRow), ); const rightImagesWidth = Math.max( rightFirstRowCount * imageSize + (rightFirstRowCount - 1) * imageSpacing, rightSecondRowCount * imageSize + (rightSecondRowCount - 1) * imageSpacing, ) || imageSize; // 至少为 imageSize // 获取图片实际高度(用于垂直居中计算) let imageActualHeight = imageSize; if (image) { // @ts-ignore imageActualHeight = (image.height / image.width) * imageSize; } // 计算左侧图片区域的实际高度(考虑图片的实际高度和行间距) const leftImagesHeight = leftRows * imageActualHeight + (leftRows - 1) * rowSpacing; // 计算右侧图片区域的实际高度 const rightImagesHeight = rightRows * imageActualHeight + (rightRows - 1) * rowSpacing; // 计算加号位置(居中,包含左右间距) const totalContentWidth = leftImagesWidth + plusSignSpacing + plusSignSize + plusSignSpacing + rightImagesWidth; const availableWidth = leftBoxWidth - padding * 2; const contentStartX = leftBoxContentX + (availableWidth - totalContentWidth) / 2; const leftImagesStartX = contentStartX; // 垂直居中:使用图片实际高度计算 const leftImagesStartY = leftBoxCenterY - leftImagesHeight / 2; // 绘制左侧图片(最多两行) for (let j = 0; j < Math.min(leftImageCount, 10); j++) { const row = Math.floor(j / leftMaxPerRow); const col = j % leftMaxPerRow; // 如果超过两行,停止绘制 if (row >= 2) break; const imagesInThisRow = Math.min( leftMaxPerRow, leftImageCount - row * leftMaxPerRow, ); const rowWidth = imagesInThisRow * imageSize + (imagesInThisRow - 1) * imageSpacing; const rowStartX = leftImagesStartX + (leftImagesWidth - rowWidth) / 2; const imageX = rowStartX + col * (imageSize + imageSpacing); // 使用图片实际高度和行间距计算垂直位置 const imageY = leftImagesStartY + row * (imageActualHeight + rowSpacing); if (image) { // @ts-ignore const scaledHeight = (image.height / image.width) * imageSize; ctx.drawImage( image, imageX, imageY, imageSize, scaledHeight, ); } } // 绘制加号(左右有间距) const plusX = leftImagesStartX + leftImagesWidth + plusSignSpacing + plusSignSize / 2; const plusY = leftBoxCenterY; ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.beginPath(); // 横线 ctx.moveTo(plusX - plusSignSize / 2, plusY); ctx.lineTo(plusX + plusSignSize / 2, plusY); // 竖线 ctx.moveTo(plusX, plusY - plusSignSize / 2); ctx.lineTo(plusX, plusY + plusSignSize / 2); ctx.stroke(); const rightImagesStartX = plusX + plusSignSize / 2 + plusSignSpacing; // 垂直居中:使用图片实际高度计算 const rightImagesStartY = leftBoxCenterY - rightImagesHeight / 2; // 绘制右侧图片(最多两行) for (let j = 0; j < Math.min(rightImageCount, 10); j++) { const row = Math.floor(j / rightMaxPerRow); const col = j % rightMaxPerRow; // 如果超过两行,停止绘制 if (row >= 2) break; const imagesInThisRow = Math.min( rightMaxPerRow, rightImageCount - row * rightMaxPerRow, ); const rowWidth = imagesInThisRow * imageSize + (imagesInThisRow - 1) * imageSpacing; const rowStartX = rightImagesStartX + (rightImagesWidth - rowWidth) / 2; const imageX = rowStartX + col * (imageSize + imageSpacing); // 使用图片实际高度和行间距计算垂直位置 const imageY = rightImagesStartY + row * (imageActualHeight + rowSpacing); if (image) { // @ts-ignore const scaledHeight = (image.height / image.width) * imageSize; ctx.drawImage( image, imageX, imageY, imageSize, scaledHeight, ); } } } else { // 减法:展示总数量的图片,后几个加划线 const totalCount = problem.left; // 总数 const subtractCount = problem.right; // 要减去的数量 // 获取图片实际高度(用于垂直居中计算) let imageActualHeight = imageSize; if (image) { // @ts-ignore imageActualHeight = (image.height / image.width) * imageSize; } // 计算图片布局 const imagesPerRow = 5; const totalRows = Math.ceil(totalCount / imagesPerRow); // 使用图片实际高度和行间距计算总高度 const totalHeight = totalRows * imageActualHeight + (totalRows - 1) * rowSpacing; const imagesStartY = leftBoxCenterY - totalHeight / 2; // 绘制所有图片 for (let j = 0; j < totalCount; j++) { const row = Math.floor(j / imagesPerRow); const col = j % imagesPerRow; const imagesInThisRow = Math.min( imagesPerRow, totalCount - row * imagesPerRow, ); const rowWidth = imagesInThisRow * imageSize + (imagesInThisRow - 1) * imageSpacing; const rowStartX = leftBoxContentX + (leftBoxWidth - padding * 2 - rowWidth) / 2; const imageX = rowStartX + col * (imageSize + imageSpacing); // 使用图片实际高度和行间距计算垂直位置 const imageY = imagesStartY + row * (imageActualHeight + rowSpacing); if (image) { // @ts-ignore const scaledHeight = (image.height / image.width) * imageSize; ctx.drawImage( image, imageX, imageY, imageSize, scaledHeight, ); // 如果是后几个图片,绘制虚线删除线(表示减去) if (j >= totalCount - subtractCount) { ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.setLineDash([4, 3]); // 设置虚线样式 ctx.beginPath(); // 绘制第一条虚线对角线(从左上到右下) ctx.moveTo(imageX, imageY); ctx.lineTo( imageX + imageSize, imageY + scaledHeight, ); ctx.stroke(); ctx.setLineDash([]); // 重置为实线,防止影响后续绘制 } } } } // ========== 绘制右侧答案框 ========== this.drawAnswerBoxArea(ctx, { rightBoxX, boxY, rightBoxWidth, borderRadius, boxHeight, padding, problemType: problem.type, }); } } /** * 绘制圆角矩形(虚线或实线边框) */ 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([2, 2]); // 虚线 } else { ctx.setLineDash([]); // 实线 } ctx.stroke(); ctx.setLineDash([]); // 重置为实线 } /** * 绘制实线圆角方框(用于填写答案) */ private drawAnswerBox( ctx: RenderingContext, x: number, y: number, width: number, height: number, ) { const radius = 6; // 圆角大小 ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.setLineDash([]); // 实线 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(); ctx.stroke(); } /** * 绘制右侧答案框 */ private drawAnswerBoxArea( ctx: RenderingContext, params: { rightBoxX: number; boxY: number; rightBoxWidth: number; borderRadius: number; boxHeight: number; padding: number; problemType: 'addition' | 'subtraction'; }, ) { const { rightBoxX, boxY, rightBoxWidth, borderRadius, boxHeight, padding, problemType, } = params; const plusSignSize = 18; // 加号大小(缩小) const minusSignSize = 18; // 减号大小(缩小) const equalsSignSize = 18; // 等号大小(缩小) const spacing = 8; // 加号左右间距(方便修改和调试) const answerBoxWidth = 34; // 答案框宽度 const answerBoxHeight = 34; // 答案框高度 // 绘制右侧答案框边框 ctx.strokeStyle = '#999'; ctx.lineWidth = 1; this.drawRoundedRectInternal( ctx, rightBoxX, boxY, rightBoxWidth, boxHeight, borderRadius, true, ); const rightBoxCenterY = boxY + boxHeight / 2; const rightBoxAvailableWidth = rightBoxWidth - padding * 2; const answerStartY = rightBoxCenterY - answerBoxHeight / 2; // 根据问题类型确定运算符号大小 const operatorSignSize = problemType === 'addition' ? plusSignSize : minusSignSize; // 计算总宽度并水平居中(加法和减法逻辑相同) const totalAnswerWidth = answerBoxWidth + spacing + operatorSignSize + spacing + answerBoxWidth + spacing + equalsSignSize + spacing + answerBoxWidth; const answerStartX = rightBoxX + padding + (rightBoxAvailableWidth - totalAnswerWidth) / 2; // 计算各个元素的位置 const box1X = answerStartX; const operatorX = box1X + answerBoxWidth + spacing; const box2X = operatorX + plusSignSize + spacing; const equalsX = box2X + answerBoxWidth + spacing; const box3X = equalsX + equalsSignSize + spacing; // 绘制第一个方框(圆角) this.drawAnswerBox( ctx, box1X, answerStartY, answerBoxWidth, answerBoxHeight, ); // 绘制运算符号(加号或减号) ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.beginPath(); if (problemType === 'addition') { // 加号:横线和竖线 ctx.moveTo(operatorX, rightBoxCenterY); ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY); ctx.moveTo( operatorX + operatorSignSize / 2, rightBoxCenterY - operatorSignSize / 2, ); ctx.lineTo( operatorX + operatorSignSize / 2, rightBoxCenterY + operatorSignSize / 2, ); } else { // 减号:只有横线 ctx.moveTo(operatorX, rightBoxCenterY); ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY); } ctx.stroke(); // 绘制第二个方框(圆角) this.drawAnswerBox( ctx, box2X, answerStartY, answerBoxWidth, answerBoxHeight, ); // 绘制等号(缩短) ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(equalsX, rightBoxCenterY - 3); ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY - 3); ctx.moveTo(equalsX, rightBoxCenterY + 3); ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY + 3); ctx.stroke(); // 绘制第三个方框(答案,圆角) this.drawAnswerBox( ctx, box3X, answerStartY, answerBoxWidth, answerBoxHeight, ); } /** * 根据图片类型设置图片大小 */ private getImageSize(imageType: 'twelve-animals' | 'fruits'): number { return imageType === 'twelve-animals' ? 40 : 36; } } export default AdditionDraw;