import { getImage } from '../../../utils/index'; import { getRandomNumberColor } from '../../../constants/mathFunctions'; interface DrawNumberDecomposeContentParams { canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; decomposeData: { problems: Array<{ whole: number | null; part1: number | null; part2: number | null; imageIndex?: number; imageType?: 'fruits' | 'twelve-animals'; }>; mode: 'with-image' | 'decompose' | 'compose'; }; canvasWidth: number; startY: number; } /** * 绘制圆角矩形(虚线边框) */ function drawRoundedRect( ctx: RenderingContext, x: number, y: number, width: number, height: number, radius: number, isDashed: boolean = false, ) { 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 async function drawNumberDecomposeContent({ canvas, ctx, decomposeData, canvasWidth, startY, }: DrawNumberDecomposeContentParams): Promise { const { problems, mode } = decomposeData; if (mode === 'with-image') { await drawWithImageMode(canvas, ctx, problems, canvasWidth, startY); } else if (mode === 'decompose') { await drawDecomposeOrComposeMode( ctx, problems, canvasWidth, startY, false, ); } else if (mode === 'compose') { await drawDecomposeOrComposeMode( ctx, problems, canvasWidth, startY, true, ); } } /** * 有图片模式:一行三列,总共三行(9个题目) */ async function drawWithImageMode( canvas: WechatMiniprogram.Canvas, ctx: RenderingContext, problems: Array<{ whole: number | null; part1: number | null; part2: number | null; imageIndex?: number; imageType?: 'fruits' | 'twelve-animals'; }>, canvasWidth: number, startY: number, ) { const leftMargin = 30; const rightMargin = 30; const topMargin = 20; const boxSpacing = 20; const rowSpacing = 32; // 计算每个框的尺寸 const availableWidth = canvasWidth - leftMargin - rightMargin; const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距 const imageAreaHeight = 120; // 图片区域高度 const treeAreaHeight = 80; // 二叉树区域高度 let currentY = startY + topMargin; // 绘制3列3行 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 * (imageAreaHeight + treeAreaHeight + rowSpacing); // 绘制一个完整的题目框 await drawWithImageProblem( ctx, canvas, problem, boxX, boxY, boxWidth, imageAreaHeight, treeAreaHeight, ); } } } /** * 分模式/组合模式:一行3个,总共5行(15个题目) * @param isInverted true 为组合模式(倒置二叉树),false 为分模式(正常二叉树) */ async function drawDecomposeOrComposeMode( ctx: RenderingContext, problems: Array<{ whole: number | null; part1: number | null; part2: number | null; }>, canvasWidth: number, startY: number, isInverted: boolean, ) { const leftMargin = 30; const rightMargin = 30; const topMargin = 20; const boxSpacing = 15; const rowSpacing = 140; // 计算每个框的尺寸 const availableWidth = canvasWidth - leftMargin - rightMargin; const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距 const boxHeight = 80; // 二叉树区域高度 let currentY = startY + topMargin; // 绘制3列5行 for (let row = 0; row < 5; 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 * rowSpacing; // 绘制二叉树 drawTree(ctx, problem, boxX, boxY, boxWidth, boxHeight, isInverted); } } } /** * 绘制有图片模式的单个题目 */ async function drawWithImageProblem( ctx: RenderingContext, canvas: WechatMiniprogram.Canvas, problem: { whole: number | null; part1: number | null; part2: number | null; imageIndex?: number; imageType?: 'fruits' | 'twelve-animals'; }, boxX: number, boxY: number, boxWidth: number, imageAreaHeight: number, treeAreaHeight: number, ) { const borderRadius = 12; // 绘制实线框(只绘制图片区域的框) ctx.strokeStyle = '#333'; ctx.lineWidth = 1; drawRoundedRect( ctx, boxX, boxY, boxWidth, imageAreaHeight, borderRadius, false, ); // 图片区域(增加底部间距,避免与二叉树根节点重叠) const imageBottomPadding = 20; // 图片区域底部额外间距 const effectiveImageHeight = imageAreaHeight - imageBottomPadding; if (problem.imageIndex && problem.imageType && problem.whole) { await drawImagesInBox( ctx, canvas, problem.whole, problem.imageIndex, problem.imageType, boxX, boxY, boxWidth, effectiveImageHeight, ); } // 二叉树区域(增加与图片区域的间距) const treeTopSpacing = -5; // 二叉树区域顶部间距 const treeY = boxY + imageAreaHeight + treeTopSpacing; drawTree( ctx, problem, boxX, treeY, boxWidth, treeAreaHeight, false, // 不是倒置 ); } /** * 绘制二叉树结构 * @param isInverted 是否倒置(组合模式为true) */ function drawTree( ctx: RenderingContext, problem: { whole: number | null; part1: number | null; part2: number | null; }, boxX: number, boxY: number, boxWidth: number, boxHeight: number, isInverted: boolean, ) { const nodeRadius = 18; const nodeSpacing = 40; // 节点之间的水平间距 const verticalSpacing = 70; // 根节点和子节点的垂直间距 if (isInverted) { // 组合模式:倒置二叉树 // 顶部:两个子节点(part1, part2) // 底部:根节点(whole,需要填写) const centerX = boxX + boxWidth / 2; const topY = boxY + boxHeight / 2 - verticalSpacing / 2; const bottomY = boxY + boxHeight / 2 + verticalSpacing / 2; // 绘制两个子节点(顶部) const leftNodeX = centerX - nodeSpacing / 2 - nodeRadius; const rightNodeX = centerX + nodeSpacing / 2 + nodeRadius; // 左子节点 drawNode(ctx, leftNodeX, topY, nodeRadius, problem.part1); // 右子节点 drawNode(ctx, rightNodeX, topY, nodeRadius, problem.part2); // 绘制根节点(底部) drawNode(ctx, centerX, bottomY, nodeRadius, problem.whole); // 绘制连接线(从子节点到根节点) ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(leftNodeX, topY + nodeRadius); ctx.lineTo(centerX, bottomY - nodeRadius); ctx.stroke(); ctx.beginPath(); ctx.moveTo(rightNodeX, topY + nodeRadius); ctx.lineTo(centerX, bottomY - nodeRadius); ctx.stroke(); } else { // 分解模式:正常二叉树 // 顶部:根节点(whole) // 底部:两个子节点(part1, part2,其中一个需要填写) const centerX = boxX + boxWidth / 2; const topY = boxY + boxHeight / 2 - verticalSpacing / 2; const bottomY = boxY + boxHeight / 2 + verticalSpacing / 2; // 绘制根节点(顶部) drawNode(ctx, centerX, topY, nodeRadius, problem.whole); // 绘制两个子节点(底部) const leftNodeX = centerX - nodeSpacing / 2 - nodeRadius; const rightNodeX = centerX + nodeSpacing / 2 + nodeRadius; // 左子节点 drawNode(ctx, leftNodeX, bottomY, nodeRadius, problem.part1); // 右子节点 drawNode(ctx, rightNodeX, bottomY, nodeRadius, problem.part2); // 绘制连接线(从根节点到子节点) ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(centerX, topY + nodeRadius); ctx.lineTo(leftNodeX, bottomY - nodeRadius); ctx.stroke(); ctx.beginPath(); ctx.moveTo(centerX, topY + nodeRadius); ctx.lineTo(rightNodeX, bottomY - nodeRadius); ctx.stroke(); } } /** * 绘制节点(圆圈和数字) */ function drawNode( ctx: RenderingContext, x: number, y: number, radius: number, value: number | null, ) { // 绘制圆角正方形 const size = radius * 2; // 正方形边长 const cornerRadius = 8; // 圆角半径 const halfSize = size / 2; ctx.fillStyle = '#fff'; ctx.strokeStyle = '#333'; ctx.lineWidth = 2; // 绘制圆角矩形 ctx.beginPath(); ctx.moveTo(x - halfSize + cornerRadius, y - halfSize); ctx.lineTo(x + halfSize - cornerRadius, y - halfSize); ctx.quadraticCurveTo( x + halfSize, y - halfSize, x + halfSize, y - halfSize + cornerRadius, ); ctx.lineTo(x + halfSize, y + halfSize - cornerRadius); ctx.quadraticCurveTo( x + halfSize, y + halfSize, x + halfSize - cornerRadius, y + halfSize, ); ctx.lineTo(x - halfSize + cornerRadius, y + halfSize); ctx.quadraticCurveTo( x - halfSize, y + halfSize, x - halfSize, y + halfSize - cornerRadius, ); ctx.lineTo(x - halfSize, y - halfSize + cornerRadius); ctx.quadraticCurveTo( x - halfSize, y - halfSize, x - halfSize + cornerRadius, y - halfSize, ); ctx.closePath(); ctx.fill(); ctx.stroke(); // 如果有值,绘制数字 if (value !== null) { ctx.fillStyle = getRandomNumberColor(); ctx.font = `bold ${24}px "Microsoft Yahei"`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(String(value), x, y); } } /** * 在框内绘制多张图片 */ async function 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 horizontalPadding = 5; const verticalPadding = 10; const availableWidth = boxWidth - horizontalPadding * 2; const availableHeight = boxHeight - verticalPadding * 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/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 + horizontalPadding + imageSpacing + col * (imageSize + imageSpacing); const imageY = boxY + verticalPadding + // 这两段代码是用于计算每一张图片在Y轴方向上的实际位置(imageY 变量) // 第一行:如果有多于一行,行间用 rowSpacing,否则单行时图片整体垂直居中排列。 (rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) + // 第二行:加上具体第 row 行的纵向偏移(每张图片的高度加上行间距,再乘以行号)。 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); } } }