From c7d55e1e2d8256bae64fd698457b8f16b42262d5 Mon Sep 17 00:00:00 2001 From: R524809 Date: Mon, 22 Dec 2025 11:05:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E7=B2=BE=E7=AE=80draw=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shared/service/codeConnectDraw.ts | 15 +- .../shared/service/colorPatternDraw.ts | 15 +- .../focusPages/shared/service/gridDraw.ts | 15 +- .../shared/service/gridReasoningDraw.ts | 15 +- .../shared/service/lineRecognitionDraw.ts | 15 +- .../shared/service/matchConnectDraw.ts | 15 +- .../shared/service/positionColoringDraw.ts | 15 +- .../shared/service/shapeSymbolDraw.ts | 15 +- .../shared/service/additionContentDraw.ts | 576 --------------- .../mathPages/shared/service/additionDraw.ts | 664 +++++++++++++++++- .../shared/service/compareContentDraw.ts | 344 --------- .../mathPages/shared/service/compareDraw.ts | 388 +++++++++- .../shared/service/countMatchContentDraw.ts | 294 -------- .../shared/service/countMatchDraw.ts | 342 ++++++++- .../service/countingSelectContentDraw.ts | 312 -------- .../shared/service/countingSelectDraw.ts | 383 +++++++++- .../service/missingNumberContentDraw.ts | 133 ---- .../shared/service/missingNumberDraw.ts | 175 ++++- .../shared/service/numberColorContentDraw.ts | 459 ------------ .../shared/service/numberColorDraw.ts | 500 ++++++++++++- .../shared/service/numberContentDraw.ts | 78 -- .../service/numberDecomposeContentDraw.ts | 504 ------------- .../shared/service/numberDecomposeDraw.ts | 577 ++++++++++++++- .../shared/service/numberFindDraw.ts | 85 ++- .../shared/service/numberSortDraw.ts | 15 +- miniprogram/service/baseDraw.ts | 27 + 26 files changed, 2938 insertions(+), 3038 deletions(-) delete mode 100644 miniprogram/mathPages/shared/service/additionContentDraw.ts delete mode 100644 miniprogram/mathPages/shared/service/compareContentDraw.ts delete mode 100644 miniprogram/mathPages/shared/service/countMatchContentDraw.ts delete mode 100644 miniprogram/mathPages/shared/service/countingSelectContentDraw.ts delete mode 100644 miniprogram/mathPages/shared/service/missingNumberContentDraw.ts delete mode 100644 miniprogram/mathPages/shared/service/numberColorContentDraw.ts delete mode 100644 miniprogram/mathPages/shared/service/numberContentDraw.ts delete mode 100644 miniprogram/mathPages/shared/service/numberDecomposeContentDraw.ts diff --git a/miniprogram/focusPages/shared/service/codeConnectDraw.ts b/miniprogram/focusPages/shared/service/codeConnectDraw.ts index 870be13..5ce4059 100644 --- a/miniprogram/focusPages/shared/service/codeConnectDraw.ts +++ b/miniprogram/focusPages/shared/service/codeConnectDraw.ts @@ -50,20 +50,9 @@ class CodeConnectDraw extends BaseDrawService { return; } - this.setPrintConfig(); this.codeData = codeData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域 - this.drawDivider(); + this.prepareDraw(); + await this.drawHeaderAndDivider(); await this.drawContent({ canvas: this.canvas, ctx: this.ctx, diff --git a/miniprogram/focusPages/shared/service/colorPatternDraw.ts b/miniprogram/focusPages/shared/service/colorPatternDraw.ts index 270a346..2438ddb 100644 --- a/miniprogram/focusPages/shared/service/colorPatternDraw.ts +++ b/miniprogram/focusPages/shared/service/colorPatternDraw.ts @@ -41,20 +41,9 @@ class ColorPatternDraw extends BaseDrawService { return; } - this.setPrintConfig(); this.gridData = gridData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域 - this.drawDivider(); + this.prepareDraw(); + await this.drawHeaderAndDivider(); this.drawContent({ ctx: this.ctx, gridData: this.gridData, diff --git a/miniprogram/focusPages/shared/service/gridDraw.ts b/miniprogram/focusPages/shared/service/gridDraw.ts index d7a273a..60b5fe8 100644 --- a/miniprogram/focusPages/shared/service/gridDraw.ts +++ b/miniprogram/focusPages/shared/service/gridDraw.ts @@ -51,20 +51,9 @@ class GridDraw extends BaseDrawService { return; } - this.setPrintConfig(); this.gridData = gridData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域(网格和填充) - this.drawDivider(); + this.prepareDraw(); + await this.drawHeaderAndDivider(); await this.drawGridContent({ canvas: this.canvas, ctx: this.ctx, diff --git a/miniprogram/focusPages/shared/service/gridReasoningDraw.ts b/miniprogram/focusPages/shared/service/gridReasoningDraw.ts index d9fea61..631cd1b 100644 --- a/miniprogram/focusPages/shared/service/gridReasoningDraw.ts +++ b/miniprogram/focusPages/shared/service/gridReasoningDraw.ts @@ -60,20 +60,9 @@ class GridReasoningDraw extends BaseDrawService { return; } - this.setPrintConfig(); this.gridData = gridData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域 - this.drawDivider(); + this.prepareDraw(); + await this.drawHeaderAndDivider(); await this.drawContent({ canvas: this.canvas, ctx: this.ctx, diff --git a/miniprogram/focusPages/shared/service/lineRecognitionDraw.ts b/miniprogram/focusPages/shared/service/lineRecognitionDraw.ts index e399fa9..675761c 100644 --- a/miniprogram/focusPages/shared/service/lineRecognitionDraw.ts +++ b/miniprogram/focusPages/shared/service/lineRecognitionDraw.ts @@ -83,20 +83,9 @@ class LineRecognitionDraw extends BaseDrawService { await this.loadSpiralLineImage(); } - this.setPrintConfig(); this.lineData = lineData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域 - this.drawDivider(); + this.prepareDraw(); + await this.drawHeaderAndDivider(); await this.drawContent({ canvas: this.canvas, ctx: this.ctx, diff --git a/miniprogram/focusPages/shared/service/matchConnectDraw.ts b/miniprogram/focusPages/shared/service/matchConnectDraw.ts index 6746759..86af9b0 100644 --- a/miniprogram/focusPages/shared/service/matchConnectDraw.ts +++ b/miniprogram/focusPages/shared/service/matchConnectDraw.ts @@ -39,20 +39,9 @@ class MatchConnectDraw extends BaseDrawService { return; } - this.setPrintConfig(); this.matchData = matchData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域 - this.drawDivider(); + this.prepareDraw(); + await this.drawHeaderAndDivider(); await this.drawContent({ canvas: this.canvas, ctx: this.ctx, diff --git a/miniprogram/focusPages/shared/service/positionColoringDraw.ts b/miniprogram/focusPages/shared/service/positionColoringDraw.ts index e0f50f3..c069f66 100644 --- a/miniprogram/focusPages/shared/service/positionColoringDraw.ts +++ b/miniprogram/focusPages/shared/service/positionColoringDraw.ts @@ -40,20 +40,9 @@ class PositionColoringDraw extends BaseDrawService { return; } - this.setPrintConfig(); this.gridData = gridData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域 - this.drawDivider(); + this.prepareDraw(); + await this.drawHeaderAndDivider(); await this.drawContent({ canvas: this.canvas, ctx: this.ctx, diff --git a/miniprogram/focusPages/shared/service/shapeSymbolDraw.ts b/miniprogram/focusPages/shared/service/shapeSymbolDraw.ts index f6a828d..955e6c4 100644 --- a/miniprogram/focusPages/shared/service/shapeSymbolDraw.ts +++ b/miniprogram/focusPages/shared/service/shapeSymbolDraw.ts @@ -45,20 +45,9 @@ class ShapeSymbolDraw extends BaseDrawService { return; } - this.setPrintConfig(); this.gridData = gridData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域 - this.drawDivider(); + this.prepareDraw(); + await this.drawHeaderAndDivider(); this.drawContent({ ctx: this.ctx, gridData: this.gridData, diff --git a/miniprogram/mathPages/shared/service/additionContentDraw.ts b/miniprogram/mathPages/shared/service/additionContentDraw.ts deleted file mode 100644 index 1a298d7..0000000 --- a/miniprogram/mathPages/shared/service/additionContentDraw.ts +++ /dev/null @@ -1,576 +0,0 @@ -import { getImage } from '../../../utils/index'; - -interface DrawAdditionContentParams { - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - problems: Array<{ - type: 'addition' | 'subtraction'; - left: number; - right: number; - result: number; - }>; - canvasWidth: number; - startY: number; - imageType?: string; // 'twelve-animals' 或 'fruits' -} - -/** - * 绘制圆角矩形(虚线或实线边框) - */ -function drawRoundedRect( - 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([]); // 重置为实线 -} - -/** - * 绘制实线圆角方框(用于填写答案) - */ -function 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(); -} - -/** - * 绘制右侧答案框 - */ -function 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; - drawRoundedRect( - 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; - - // 绘制第一个方框(圆角) - 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(); - - // 绘制第二个方框(圆角) - 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(); - - // 绘制第三个方框(答案,圆角) - drawAnswerBox(ctx, box3X, answerStartY, answerBoxWidth, answerBoxHeight); -} - -// 根据图片类型设置图片大小 -function getImageSize(imageType: 'twelve-animals' | 'fruits'): number { - return imageType === 'twelve-animals' ? 40 : 36; -} - -/** - * 绘制加减法计算内容区域 - * 左侧框:展示图片(加法:加号左右展示图片;减法:总数图片,后几个加划线) - * 右侧框:填写运算方式(实线方框 + 实线方框 = 实线方框 或 实线方框 - 实线方框 = 实线方框) - */ -export async function drawAdditionContent({ - canvas, - ctx, - problems, - canvasWidth, - startY, - imageType: _imageType = '', // 不再使用,每个运算会随机选择图片类型 -}: DrawAdditionContentParams): Promise { - // 根据图片类型确定图片目录和最大索引 - 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 = getImageSize(imageConfigItem.type); - - // 加载图片 - let image: any = null; - try { - const imagePath = `/mathPages/assets/${config.folder}/${imageConfigItem.imageIndex}.png`; - image = await getImage(canvas, imagePath); - } catch (error) { - console.error('图片加载失败:', error); - } - - // ========== 绘制左侧图片框 ========== - ctx.strokeStyle = '#999'; - ctx.lineWidth = 1; - drawRoundedRect( - 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.moveTo(imageX + imageSize, imageY); - // ctx.lineTo(imageX, imageY + scaledHeight); - ctx.stroke(); - ctx.setLineDash([]); // 重置为实线,防止影响后续绘制 - } - } - } - } - - // ========== 绘制右侧答案框 ========== - drawAnswerBoxArea(ctx, { - rightBoxX, - boxY, - rightBoxWidth, - borderRadius, - boxHeight, - padding, - problemType: problem.type, - }); - } -} diff --git a/miniprogram/mathPages/shared/service/additionDraw.ts b/miniprogram/mathPages/shared/service/additionDraw.ts index 41feefd..c90d73e 100644 --- a/miniprogram/mathPages/shared/service/additionDraw.ts +++ b/miniprogram/mathPages/shared/service/additionDraw.ts @@ -1,19 +1,27 @@ +/** + * 加减法计算绘制服务 + */ + import { BaseDrawService } from '../../../service/baseDraw'; -import { drawAdditionContent } from './additionContentDraw'; +import { getImage } from '../../../utils/index'; + +/** + * 加减法计算数据 + */ +export interface AdditionData { + problems: Array<{ + type: 'addition' | 'subtraction'; + left: number; + right: number; + result: number; + }>; +} /** * 加减法计算绘制服务 - * 组合使用基础绘制服务和内容区域绘制服务 */ class AdditionDraw extends BaseDrawService { - calculationData: { - problems: Array<{ - type: 'addition' | 'subtraction'; - left: number; - right: number; - result: number; - }>; - } | null; + calculationData: AdditionData | null = null; constructor( canvas: Canvas, @@ -24,15 +32,11 @@ class AdditionDraw extends BaseDrawService { this.calculationData = null; } + /** + * 绘制加减法计算内容 + */ async draw( - calculationData: { - problems: Array<{ - type: 'addition' | 'subtraction'; - left: number; - right: number; - result: number; - }>; - }, + calculationData: AdditionData, _calculationType: string = 'addition-5', // 保留参数以保持接口兼容性 _imageType?: string, // 可选,不再使用,每个运算会随机选择图片类型 ) { @@ -40,21 +44,10 @@ class AdditionDraw extends BaseDrawService { return; } - this.setPrintConfig(); this.calculationData = calculationData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域(加减法计算) - this.drawDivider(); - await drawAdditionContent({ + this.prepareDraw(); + await this.drawHeaderAndDivider(); + await this.drawContent({ canvas: this.canvas, ctx: this.ctx, problems: this.calculationData.problems, @@ -62,6 +55,613 @@ class AdditionDraw extends BaseDrawService { 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/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; diff --git a/miniprogram/mathPages/shared/service/compareContentDraw.ts b/miniprogram/mathPages/shared/service/compareContentDraw.ts deleted file mode 100644 index 80883b4..0000000 --- a/miniprogram/mathPages/shared/service/compareContentDraw.ts +++ /dev/null @@ -1,344 +0,0 @@ -import { getImage } from '../../../utils/index'; - -interface DrawCompareContentParams { - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - compareData: { - problems: Array<{ - leftCount: number; - rightCount: number; - leftImageIndex: number; - rightImageIndex: number; - imageType: 'fruits' | 'twelve-animals'; - }>; - }; - canvasWidth: number; - startY: number; -} - -/** - * 绘制圆角矩形(虚线边框) - */ -function drawRoundedRect( - 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 async function drawCompareContent({ - canvas, - ctx, - compareData, - canvasWidth, - startY, -}: DrawCompareContentParams): Promise { - const { problems } = compareData; - - // 布局参数 - const leftMargin = 30; - const rightMargin = 30; - const topMargin = 12; - const rowSpacing = 116; // 行之间的间距 - const columnSpacing = 40; // 两列之间的间距(可调节) - - // 计算每列的宽度(减去边距和列间距) - const availableWidth = - canvasWidth - leftMargin - rightMargin - columnSpacing; - const columnWidth = availableWidth / 2; - - // 框的尺寸 - const boxWidth = 100; - const boxHeight = 100; - const borderRadius = 12; - const circleRadius = 18; // 圆圈半径 - - // 框与圆圈之间的间距 - const boxToCircleSpacing = 12; - - // 计算左侧列和右侧列的起始X位置 - const leftColumnStartX = leftMargin; - const rightColumnStartX = leftMargin + columnWidth + columnSpacing; - - // 计算每列内部的布局(左框 | 圆圈 | 右框,居中) - const totalWidthPerColumn = - boxWidth + - boxToCircleSpacing + - circleRadius * 2 + - boxToCircleSpacing + - boxWidth; - const columnPadding = (columnWidth - totalWidthPerColumn) / 2; - - let currentY = startY + topMargin; - - // 计算分割线位置(两列中间) - // const dividerX = leftMargin + columnWidth + columnSpacing / 2; - - // 固定绘制6行 - const totalRows = 6; - for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) { - const rowY = currentY + rowIndex * rowSpacing; - // 绘制左侧列 - const leftProblemIndex = rowIndex * 2; - if (leftProblemIndex < problems.length) { - await drawCompareProblem( - ctx, - canvas, - problems[leftProblemIndex], - leftColumnStartX + columnPadding, - rowY, - boxWidth, - boxHeight, - borderRadius, - circleRadius, - boxToCircleSpacing, - ); - } - - // 绘制右侧列 - const rightProblemIndex = rowIndex * 2 + 1; - if (rightProblemIndex < problems.length) { - await drawCompareProblem( - ctx, - canvas, - problems[rightProblemIndex], - rightColumnStartX + columnPadding, - rowY, - boxWidth, - boxHeight, - borderRadius, - circleRadius, - boxToCircleSpacing, - ); - } - } - - // 绘制中间分割线(贯穿所有行) - // if (totalRows > 0) { - // ctx.strokeStyle = '#999'; - // ctx.lineWidth = 1; - // ctx.beginPath(); - // const dividerStartY = currentY; - // const dividerEndY = currentY + (totalRows - 1) * rowSpacing + boxHeight; - // ctx.moveTo(dividerX, dividerStartY); - // ctx.lineTo(dividerX, dividerEndY); - // ctx.stroke(); - // } -} - -/** - * 绘制一个比较问题(左框 | 圆圈 | 右框) - */ -async function drawCompareProblem( - ctx: RenderingContext, - canvas: WechatMiniprogram.Canvas, - problem: { - leftCount: number; - rightCount: number; - leftImageIndex: number; - rightImageIndex: number; - imageType: 'fruits' | 'twelve-animals'; - }, - startX: number, - startY: number, - boxWidth: number, - boxHeight: number, - borderRadius: number, - circleRadius: number, - boxToCircleSpacing: number, -) { - const { - leftCount, - rightCount, - leftImageIndex, - rightImageIndex, - imageType, - } = problem; - - // 图片配置 - const imageConfig = { - 'twelve-animals': { - folder: 'twelve-animals', - maxIndex: 12, - }, - fruits: { - folder: 'fruits', - maxIndex: 22, - }, - }; - - const config = imageConfig[imageType] || imageConfig['twelve-animals']; - - // 计算左框位置 - const leftBoxX = startX; - const leftBoxY = startY; - - // 计算圆圈位置 - const circleX = leftBoxX + boxWidth + boxToCircleSpacing + circleRadius; - const circleY = startY + boxHeight / 2; - - // 计算右框位置 - const rightBoxX = circleX + circleRadius + boxToCircleSpacing; - const rightBoxY = startY; - - // 绘制左框 - ctx.strokeStyle = '#999'; - ctx.lineWidth = 1; - drawRoundedRect( - ctx, - leftBoxX, - leftBoxY, - boxWidth, - boxHeight, - borderRadius, - true, - ); - - // 绘制左框内的图片 - await drawImagesInBox( - ctx, - canvas, - leftCount, - leftImageIndex, - config.folder, - leftBoxX, - leftBoxY, - boxWidth, - boxHeight, - ); - - // 绘制实心圆圈 - ctx.fillStyle = '#fff'; - ctx.strokeStyle = '#333'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.arc(circleX, circleY, circleRadius, 0, Math.PI * 2); - ctx.fill(); // 先填充白色 - ctx.stroke(); // 再绘制边框 - - // 绘制右框 - ctx.strokeStyle = '#999'; - ctx.lineWidth = 1; - drawRoundedRect( - ctx, - rightBoxX, - rightBoxY, - boxWidth, - boxHeight, - borderRadius, - true, - ); - - // 绘制右框内的图片 - await drawImagesInBox( - ctx, - canvas, - rightCount, - rightImageIndex, - config.folder, - rightBoxX, - rightBoxY, - boxWidth, - boxHeight, - ); -} - -/** - * 在框内绘制多张图片 - */ -async function drawImagesInBox( - ctx: RenderingContext, - canvas: WechatMiniprogram.Canvas, - count: number, - imageIndex: number, - folder: string, - boxX: number, - boxY: number, - boxWidth: number, - boxHeight: number, -) { - 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/assets/${folder}/${imageIndex}.png`; - boxImage = await getImage(canvas, imagePath); - } catch (error) { - console.error(`加载${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); - } - } -} diff --git a/miniprogram/mathPages/shared/service/compareDraw.ts b/miniprogram/mathPages/shared/service/compareDraw.ts index f1a1869..0044501 100644 --- a/miniprogram/mathPages/shared/service/compareDraw.ts +++ b/miniprogram/mathPages/shared/service/compareDraw.ts @@ -1,20 +1,28 @@ +/** + * 数一数比大小绘制服务 + */ + import { BaseDrawService } from '../../../service/baseDraw'; -import { drawCompareContent } from './compareContentDraw'; +import { getImage } from '../../../utils/index'; + +/** + * 数一数比大小数据 + */ +export interface CompareData { + problems: Array<{ + leftCount: number; + rightCount: number; + leftImageIndex: number; + rightImageIndex: number; + imageType: 'fruits' | 'twelve-animals'; + }>; +} /** * 数一数比大小绘制服务 - * 组合使用基础绘制服务和内容区域绘制服务 */ class CompareDraw extends BaseDrawService { - compareData: { - problems: Array<{ - leftCount: number; - rightCount: number; - leftImageIndex: number; - rightImageIndex: number; - imageType: 'fruits' | 'twelve-animals'; - }>; - } | null; + compareData: CompareData | null = null; constructor( canvas: Canvas, @@ -25,34 +33,18 @@ class CompareDraw extends BaseDrawService { this.compareData = null; } - async draw(compareData: { - problems: Array<{ - leftCount: number; - rightCount: number; - leftImageIndex: number; - rightImageIndex: number; - imageType: 'fruits' | 'twelve-animals'; - }>; - }) { + /** + * 绘制数一数比大小内容 + */ + async draw(compareData: CompareData) { if (!compareData || !compareData.problems) { return; } - this.setPrintConfig(); this.compareData = compareData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域(数一数比大小) - this.drawDivider(); - await drawCompareContent({ + this.prepareDraw(); + await this.drawHeaderAndDivider(); + await this.drawContent({ canvas: this.canvas, ctx: this.ctx, compareData: this.compareData, @@ -60,6 +52,334 @@ class CompareDraw extends BaseDrawService { startY: this.currentY, }); } + + /** + * 绘制内容区域 + */ + private async drawContent(params: { + canvas: WechatMiniprogram.Canvas; + ctx: RenderingContext; + compareData: CompareData; + canvasWidth: number; + startY: number; + }): Promise { + const { canvas, ctx, compareData, canvasWidth, startY } = params; + const { problems } = compareData; + + // 布局参数 + const leftMargin = 30; + const rightMargin = 30; + const topMargin = 12; + const rowSpacing = 116; // 行之间的间距 + const columnSpacing = 40; // 两列之间的间距(可调节) + + // 计算每列的宽度(减去边距和列间距) + const availableWidth = + canvasWidth - leftMargin - rightMargin - columnSpacing; + const columnWidth = availableWidth / 2; + + // 框的尺寸 + const boxWidth = 100; + const boxHeight = 100; + const borderRadius = 12; + const circleRadius = 18; // 圆圈半径 + + // 框与圆圈之间的间距 + const boxToCircleSpacing = 12; + + // 计算左侧列和右侧列的起始X位置 + const leftColumnStartX = leftMargin; + const rightColumnStartX = leftMargin + columnWidth + columnSpacing; + + // 计算每列内部的布局(左框 | 圆圈 | 右框,居中) + const totalWidthPerColumn = + boxWidth + + boxToCircleSpacing + + circleRadius * 2 + + boxToCircleSpacing + + boxWidth; + const columnPadding = (columnWidth - totalWidthPerColumn) / 2; + + let currentY = startY + topMargin; + + // 固定绘制6行 + const totalRows = 6; + for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) { + const rowY = currentY + rowIndex * rowSpacing; + // 绘制左侧列 + const leftProblemIndex = rowIndex * 2; + if (leftProblemIndex < problems.length) { + await this.drawCompareProblem( + ctx, + canvas, + problems[leftProblemIndex], + leftColumnStartX + columnPadding, + rowY, + boxWidth, + boxHeight, + borderRadius, + circleRadius, + boxToCircleSpacing, + ); + } + + // 绘制右侧列 + const rightProblemIndex = rowIndex * 2 + 1; + if (rightProblemIndex < problems.length) { + await this.drawCompareProblem( + ctx, + canvas, + problems[rightProblemIndex], + rightColumnStartX + columnPadding, + rowY, + boxWidth, + boxHeight, + borderRadius, + circleRadius, + boxToCircleSpacing, + ); + } + } + } + + /** + * 绘制一个比较问题(左框 | 圆圈 | 右框) + */ + private async drawCompareProblem( + ctx: RenderingContext, + canvas: WechatMiniprogram.Canvas, + problem: { + leftCount: number; + rightCount: number; + leftImageIndex: number; + rightImageIndex: number; + imageType: 'fruits' | 'twelve-animals'; + }, + startX: number, + startY: number, + boxWidth: number, + boxHeight: number, + borderRadius: number, + circleRadius: number, + boxToCircleSpacing: number, + ) { + const { + leftCount, + rightCount, + leftImageIndex, + rightImageIndex, + imageType, + } = problem; + + // 图片配置 + const imageConfig = { + 'twelve-animals': { + folder: 'twelve-animals', + maxIndex: 12, + }, + fruits: { + folder: 'fruits', + maxIndex: 22, + }, + }; + + const config = imageConfig[imageType] || imageConfig['twelve-animals']; + + // 计算左框位置 + const leftBoxX = startX; + const leftBoxY = startY; + + // 计算圆圈位置 + const circleX = leftBoxX + boxWidth + boxToCircleSpacing + circleRadius; + const circleY = startY + boxHeight / 2; + + // 计算右框位置 + const rightBoxX = circleX + circleRadius + boxToCircleSpacing; + const rightBoxY = startY; + + // 绘制左框 + ctx.strokeStyle = '#999'; + ctx.lineWidth = 1; + this.drawRoundedRectInternal( + ctx, + leftBoxX, + leftBoxY, + boxWidth, + boxHeight, + borderRadius, + true, + ); + + // 绘制左框内的图片 + await this.drawImagesInBox( + ctx, + canvas, + leftCount, + leftImageIndex, + config.folder, + leftBoxX, + leftBoxY, + boxWidth, + boxHeight, + ); + + // 绘制实心圆圈 + ctx.fillStyle = '#fff'; + ctx.strokeStyle = '#333'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(circleX, circleY, circleRadius, 0, Math.PI * 2); + ctx.fill(); // 先填充白色 + ctx.stroke(); // 再绘制边框 + + // 绘制右框 + ctx.strokeStyle = '#999'; + ctx.lineWidth = 1; + this.drawRoundedRectInternal( + ctx, + rightBoxX, + rightBoxY, + boxWidth, + boxHeight, + borderRadius, + true, + ); + + // 绘制右框内的图片 + await this.drawImagesInBox( + ctx, + canvas, + rightCount, + rightImageIndex, + config.folder, + rightBoxX, + rightBoxY, + boxWidth, + boxHeight, + ); + } + + /** + * 在框内绘制多张图片 + */ + private async drawImagesInBox( + ctx: RenderingContext, + canvas: WechatMiniprogram.Canvas, + count: number, + imageIndex: number, + folder: string, + boxX: number, + boxY: number, + boxWidth: number, + boxHeight: number, + ) { + 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/assets/${folder}/${imageIndex}.png`; + boxImage = await getImage(canvas, imagePath); + } catch (error) { + console.error(`加载${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 CompareDraw; diff --git a/miniprogram/mathPages/shared/service/countMatchContentDraw.ts b/miniprogram/mathPages/shared/service/countMatchContentDraw.ts deleted file mode 100644 index 10546c5..0000000 --- a/miniprogram/mathPages/shared/service/countMatchContentDraw.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { getImage } from '../../../utils/index'; -import { getRandomNumberColor } from '../../../constants/colors'; - -interface DrawCountMatchContentParams { - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - matchData: { - leftNumbers: number[]; - rightNumbers: number[]; // 打乱顺序后的数字数组 - }; - canvasWidth: number; - startY: number; - imageType?: string; // 'twelve-animals' 或 'fruits' -} - -/** - * 绘制圆角矩形(虚线边框) - */ -function drawRoundedRect( - 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([]); // 重置为实线 -} - -/** - * 绘制数一数连一连内容区域 - * 左侧显示数字(虚线圆角框),右侧显示对应数量的图片(虚线圆角框) - * 支持根据 imageType 选择不同的图片文件夹(twelve-animals 或 fruits) - */ -export async function drawCountMatchContent({ - canvas, - ctx, - matchData, - canvasWidth, - startY, - imageType = 'twelve-animals', -}: DrawCountMatchContentParams): Promise { - 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; - drawRoundedRect( - 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; - drawRoundedRect( - 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/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(); - } -} diff --git a/miniprogram/mathPages/shared/service/countMatchDraw.ts b/miniprogram/mathPages/shared/service/countMatchDraw.ts index eee2f7e..c80ac11 100644 --- a/miniprogram/mathPages/shared/service/countMatchDraw.ts +++ b/miniprogram/mathPages/shared/service/countMatchDraw.ts @@ -1,15 +1,24 @@ +/** + * 数一数连一连绘制服务 + */ + import { BaseDrawService } from '../../../service/baseDraw'; -import { drawCountMatchContent } from './countMatchContentDraw'; +import { getImage } from '../../../utils/index'; +import { getRandomNumberColor } from '../../../constants/colors'; + +/** + * 数一数连一连数据 + */ +export interface CountMatchData { + leftNumbers: number[]; + rightNumbers: number[]; // 打乱顺序后的数字数组 +} /** * 数一数连一连绘制服务 - * 组合使用基础绘制服务和内容区域绘制服务 */ class CountMatchDraw extends BaseDrawService { - matchData: { - leftNumbers: number[]; - rightNumbers: number[]; // 打乱顺序后的数字数组 - } | null; + matchData: CountMatchData | null = null; constructor( canvas: Canvas, @@ -20,29 +29,21 @@ class CountMatchDraw extends BaseDrawService { this.matchData = null; } + /** + * 绘制数一数连一连内容 + */ async draw( - matchData: { leftNumbers: number[]; rightNumbers: number[] }, + matchData: CountMatchData, imageType: string = 'twelve-animals', ) { if (!matchData || !matchData.leftNumbers || !matchData.rightNumbers) { return; } - this.setPrintConfig(); this.matchData = matchData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域(数一数连一连) - this.drawDivider(); - await drawCountMatchContent({ + this.prepareDraw(); + await this.drawHeaderAndDivider(); + await this.drawContent({ canvas: this.canvas, ctx: this.ctx, matchData: this.matchData, @@ -51,6 +52,305 @@ class CountMatchDraw extends BaseDrawService { 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/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; diff --git a/miniprogram/mathPages/shared/service/countingSelectContentDraw.ts b/miniprogram/mathPages/shared/service/countingSelectContentDraw.ts deleted file mode 100644 index 448050a..0000000 --- a/miniprogram/mathPages/shared/service/countingSelectContentDraw.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { getImage } from '../../../utils/index'; -import { getRandomNumberColor } from '../../../constants/colors'; - -interface DrawCountingSelectContentParams { - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - countingData: { - problems: Array<{ - count: number; - imageIndex: number; - imageType: 'fruits' | 'twelve-animals'; - options?: number[]; // 选一选模式需要,填一填模式不需要 - correctIndex?: number; // 选一选模式需要 - }>; - }; - mode: 'select' | 'fill'; // 模式:'select' 显示选项,'fill' 显示括号 - canvasWidth: number; - startY: number; -} - -/** - * 绘制圆角矩形(虚线边框) - */ -function drawRoundedRect( - 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([]); // 重置为实线 -} - -/** - * 绘制数一数选一选/填一填内容区域 - * 3x3网格布局,每个框上面是图片,下面根据模式显示选项或括号 - */ -export async function drawCountingSelectContent({ - canvas, - ctx, - countingData, - mode, - canvasWidth, - startY, -}: DrawCountingSelectContentParams): Promise { - 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 drawProblemBox( - ctx, - canvas, - problem, - boxX, - boxY, - boxWidth, - boxHeight, - imageAreaHeight, - bottomAreaHeight, - optionCellWidth, - mode, - ); - } - } -} - -/** - * 绘制一个题目框(图片 + 选项表格 或 括号) - */ -async function 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; - drawRoundedRect(ctx, boxX, boxY, boxWidth, boxHeight, borderRadius, false); - - // 图片区域 - const imageAreaX = boxX; - const imageAreaY = boxY; - - // 绘制图片 - await 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); - } -} - -/** - * 在框内绘制多张图片 - */ -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 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/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); - } - } -} diff --git a/miniprogram/mathPages/shared/service/countingSelectDraw.ts b/miniprogram/mathPages/shared/service/countingSelectDraw.ts index d6844aa..2eccea0 100644 --- a/miniprogram/mathPages/shared/service/countingSelectDraw.ts +++ b/miniprogram/mathPages/shared/service/countingSelectDraw.ts @@ -1,21 +1,30 @@ +/** + * 数一数选一选/填一填绘制服务 + * 支持两种模式:'select'(选一选)和 'fill'(填一填) + */ + import { BaseDrawService } from '../../../service/baseDraw'; -import { drawCountingSelectContent } from './countingSelectContentDraw'; +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; // 选一选模式需要 + }>; +} /** * 数一数选一选/填一填绘制服务 - * 组合使用基础绘制服务和内容区域绘制服务 - * 支持两种模式:'select'(选一选)和 'fill'(填一填) */ class CountingSelectDraw extends BaseDrawService { - countingData: { - problems: Array<{ - count: number; - imageIndex: number; - imageType: 'fruits' | 'twelve-animals'; - options?: number[]; - correctIndex?: number; - }>; - } | null; + countingData: CountingSelectData | null = null; mode: 'select' | 'fill'; constructor( @@ -28,38 +37,22 @@ class CountingSelectDraw extends BaseDrawService { this.mode = 'select'; // 默认选一选模式 } + /** + * 绘制数一数选一选/填一填内容 + */ async draw( - countingData: { - problems: Array<{ - count: number; - imageIndex: number; - imageType: 'fruits' | 'twelve-animals'; - options?: number[]; - correctIndex?: number; - }>; - }, + countingData: CountingSelectData, mode: 'select' | 'fill' = 'select', ) { if (!countingData || !countingData.problems) { return; } - this.setPrintConfig(); this.countingData = countingData; this.mode = mode; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域 - this.drawDivider(); - await drawCountingSelectContent({ + this.prepareDraw(); + await this.drawHeaderAndDivider(); + await this.drawContent({ canvas: this.canvas, ctx: this.ctx, countingData: this.countingData, @@ -68,6 +61,326 @@ class CountingSelectDraw extends BaseDrawService { 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/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; diff --git a/miniprogram/mathPages/shared/service/missingNumberContentDraw.ts b/miniprogram/mathPages/shared/service/missingNumberContentDraw.ts deleted file mode 100644 index 01c52f4..0000000 --- a/miniprogram/mathPages/shared/service/missingNumberContentDraw.ts +++ /dev/null @@ -1,133 +0,0 @@ -interface DrawMissingNumberContentParams { - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - missingNumberData: { - grids: Array<{ - numbers: (number | null)[]; - colors: (string | null)[]; - }>; - maxNumber: number; - }; - canvasWidth: number; - startY: number; -} - -const gridMap = { - 10: { - cellSize: 95, - colsPerGrid: 5, - fontSize: 50, - gridSpacing: 40, - }, - 20: { - cellSize: 80, - colsPerGrid: 5, - fontSize: 40, - gridSpacing: 30, - }, - 40: { - cellSize: 60, - colsPerGrid: 8, - fontSize: 35, - gridSpacing: 30, - }, - 50: { - cellSize: 52, - colsPerGrid: 10, - fontSize: 28, - gridSpacing: 40, - }, - 80: { - cellSize: 60, - colsPerGrid: 8, - fontSize: 30, - gridSpacing: 30, - }, - 100: { - cellSize: 52, - colsPerGrid: 10, - fontSize: 28, - gridSpacing: 20, - }, - 120: { - cellSize: 52, - colsPerGrid: 10, - fontSize: 28, - gridSpacing: 20, - }, -}; - -/** - * 绘制填上缺少的数字内容区域 - * 显示多个网格,每个网格包含一些数字和一些空白位置 - */ -export async function drawMissingNumberContent({ - ctx, - missingNumberData, - canvasWidth, - startY, -}: DrawMissingNumberContentParams): Promise { - const { grids, maxNumber } = missingNumberData; - - // 计算网格布局参数 - const leftMargin = 40; - const rightMargin = 40; - const topMargin = 20; - - const config = gridMap[maxNumber as keyof typeof gridMap]; - const { cellSize, gridSpacing, colsPerGrid, fontSize } = config; - - const gridWidth = colsPerGrid * cellSize; - - // 计算网格的起始X位置(居中) - const gridStartX = - leftMargin + (canvasWidth - leftMargin - rightMargin - gridWidth) / 2; - - let currentY = startY + topMargin; - - // 绘制每个网格 - for (let gridIndex = 0; gridIndex < grids.length; gridIndex++) { - const grid = grids[gridIndex]; - const gridX = gridStartX; - - // 根据实际数字数量计算需要的行数 - const rowsPerGrid = Math.ceil(grid.numbers.length / colsPerGrid); - const gridHeight = rowsPerGrid * cellSize; - const gridY = currentY; - - // 绘制每个单元格 - for (let index = 0; index < grid.numbers.length; index++) { - const row = Math.floor(index / colsPerGrid); - const col = index % colsPerGrid; - - const cellX = gridX + col * cellSize; - const cellY = gridY + row * cellSize; - - // 绘制单元格边框 - ctx.strokeStyle = '#333'; - ctx.lineWidth = 1.5; - ctx.strokeRect(cellX, cellY, cellSize, cellSize); - - // 绘制数字或空白 - const number = grid.numbers[index]; - const color = grid.colors[index]; - - if (number !== null && color !== null) { - // 绘制数字(带颜色) - ctx.fillStyle = color; - ctx.font = `bold ${fontSize}px "Microsoft Yahei"`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText( - String(number), - cellX + cellSize / 2, - cellY + cellSize / 2, - ); - } - // 如果为null,则留空(用户填写) - } - - // 更新下一个网格的Y位置 - currentY += gridHeight + gridSpacing; - } -} diff --git a/miniprogram/mathPages/shared/service/missingNumberDraw.ts b/miniprogram/mathPages/shared/service/missingNumberDraw.ts index bfb8051..aced97b 100644 --- a/miniprogram/mathPages/shared/service/missingNumberDraw.ts +++ b/miniprogram/mathPages/shared/service/missingNumberDraw.ts @@ -1,18 +1,25 @@ +/** + * 填上缺少的数字绘制服务 + */ + import { BaseDrawService } from '../../../service/baseDraw'; -import { drawMissingNumberContent } from './missingNumberContentDraw'; + +/** + * 填上缺少的数字数据 + */ +export interface MissingNumberData { + grids: Array<{ + numbers: (number | null)[]; + colors: (string | null)[]; + }>; + maxNumber: number; +} /** * 填上缺少的数字绘制服务 - * 组合使用基础绘制服务和内容区域绘制服务 */ class MissingNumberDraw extends BaseDrawService { - missingNumberData: { - grids: Array<{ - numbers: (number | null)[]; - colors: (string | null)[]; - }>; - maxNumber: number; - } | null; + missingNumberData: MissingNumberData | null = null; constructor( canvas: Canvas, @@ -23,35 +30,21 @@ class MissingNumberDraw extends BaseDrawService { this.missingNumberData = null; } + /** + * 绘制填上缺少的数字内容 + */ async draw( - missingNumberData: { - grids: Array<{ - numbers: (number | null)[]; - colors: (string | null)[]; - }>; - maxNumber: number; - }, + missingNumberData: MissingNumberData, rangeType: string = 'within-10', ) { if (!missingNumberData || !missingNumberData.grids) { return; } - this.setPrintConfig(); this.missingNumberData = missingNumberData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域(填上缺少的数字) - this.drawDivider(); - await drawMissingNumberContent({ + this.prepareDraw(); + await this.drawHeaderAndDivider(); + await this.drawContent({ canvas: this.canvas, ctx: this.ctx, missingNumberData: this.missingNumberData, @@ -59,6 +52,128 @@ class MissingNumberDraw extends BaseDrawService { startY: this.currentY, }); } + + /** + * 绘制内容区域 + */ + private async drawContent(params: { + canvas: WechatMiniprogram.Canvas; + ctx: RenderingContext; + missingNumberData: MissingNumberData; + canvasWidth: number; + startY: number; + }): Promise { + const { ctx, missingNumberData, canvasWidth, startY } = params; + const { grids, maxNumber } = missingNumberData; + + const gridMap = { + 10: { + cellSize: 95, + colsPerGrid: 5, + fontSize: 50, + gridSpacing: 40, + }, + 20: { + cellSize: 80, + colsPerGrid: 5, + fontSize: 40, + gridSpacing: 30, + }, + 40: { + cellSize: 60, + colsPerGrid: 8, + fontSize: 35, + gridSpacing: 30, + }, + 50: { + cellSize: 52, + colsPerGrid: 10, + fontSize: 28, + gridSpacing: 40, + }, + 80: { + cellSize: 60, + colsPerGrid: 8, + fontSize: 30, + gridSpacing: 30, + }, + 100: { + cellSize: 52, + colsPerGrid: 10, + fontSize: 28, + gridSpacing: 20, + }, + 120: { + cellSize: 52, + colsPerGrid: 10, + fontSize: 28, + gridSpacing: 20, + }, + }; + + // 计算网格布局参数 + const leftMargin = 40; + const rightMargin = 40; + const topMargin = 20; + + const config = gridMap[maxNumber as keyof typeof gridMap]; + const { cellSize, gridSpacing, colsPerGrid, fontSize } = config; + + const gridWidth = colsPerGrid * cellSize; + + // 计算网格的起始X位置(居中) + const gridStartX = + leftMargin + + (canvasWidth - leftMargin - rightMargin - gridWidth) / 2; + + let currentY = startY + topMargin; + + // 绘制每个网格 + for (let gridIndex = 0; gridIndex < grids.length; gridIndex++) { + const grid = grids[gridIndex]; + const gridX = gridStartX; + + // 根据实际数字数量计算需要的行数 + const rowsPerGrid = Math.ceil(grid.numbers.length / colsPerGrid); + const gridHeight = rowsPerGrid * cellSize; + const gridY = currentY; + + // 绘制每个单元格 + for (let index = 0; index < grid.numbers.length; index++) { + const row = Math.floor(index / colsPerGrid); + const col = index % colsPerGrid; + + const cellX = gridX + col * cellSize; + const cellY = gridY + row * cellSize; + + // 绘制单元格边框 + ctx.strokeStyle = '#333'; + ctx.lineWidth = 1.5; + ctx.strokeRect(cellX, cellY, cellSize, cellSize); + + // 绘制数字或空白 + const number = grid.numbers[index]; + const color = grid.colors[index]; + + if (number !== null && color !== null) { + // 绘制数字(带颜色) + ctx.fillStyle = color; + ctx.font = `bold ${fontSize}px "Microsoft Yahei"`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText( + String(number), + cellX + cellSize / 2, + cellY + cellSize / 2, + ); + } + // 如果为null,则留空(用户填写) + } + + // 更新下一个网格的Y位置 + currentY += gridHeight + gridSpacing; + } + } } export default MissingNumberDraw; diff --git a/miniprogram/mathPages/shared/service/numberColorContentDraw.ts b/miniprogram/mathPages/shared/service/numberColorContentDraw.ts deleted file mode 100644 index 4718c46..0000000 --- a/miniprogram/mathPages/shared/service/numberColorContentDraw.ts +++ /dev/null @@ -1,459 +0,0 @@ -import { getNumberColors } from '../../../constants/colors'; - -interface DrawNumberColorContentParams { - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - colorData: { - numbers: number[]; - }; - canvasWidth: number; - startY: number; - patternType?: string; // 'circle' 或 'caterpillar' -} - -/** - * 绘制圆角矩形(虚线边框) - */ -function drawRoundedRect( - 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, 6]); // 虚线 - } else { - ctx.setLineDash([]); - } - ctx.stroke(); - ctx.setLineDash([]); // 重置为实线 -} - -/** - * 绘制毛毛虫头部 - */ -function drawCaterpillarHead( - ctx: RenderingContext, - x: number, - y: number, - size: number, - color: string, -) { - const headRadius = 18; // 固定头部半径 - const headCenterX = x + headRadius; - const headCenterY = y + size / 2; - - // 绘制头部(圆形) - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2); - ctx.fill(); - - // 绘制头部圆圈实线边框 - ctx.strokeStyle = '#000'; - ctx.lineWidth = 1; - ctx.setLineDash([]); // 确保是实线 - ctx.beginPath(); - ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2); - ctx.stroke(); - - // 绘制触角(在头部圆圈的正上方,带弧度的弯曲) - const antennaLength = headRadius * 0.6; - const antennaStartY = headCenterY - headRadius; // 头部圆圈的正上方 - const antennaOffset = headRadius * 0.3; // 触角左右偏移距离 - - // 左侧触角:从顶部左侧开始,先向上,然后向左弯曲(带弧度) - const leftAntennaStartX = headCenterX - antennaOffset; - const leftAntennaStartY = antennaStartY; - const leftAntennaMidY = leftAntennaStartY - antennaLength * 0.5; // 向上延伸的中点 - const leftAntennaEndX = leftAntennaStartX - antennaLength * 0.4; // 向左弯曲 - const leftAntennaEndY = leftAntennaMidY - antennaLength * 0.3; // 向上并向左 - - // 右侧触角:从顶部右侧开始,先向上,然后向右弯曲(带弧度) - const rightAntennaStartX = headCenterX + antennaOffset; - const rightAntennaStartY = antennaStartY; - const rightAntennaMidY = rightAntennaStartY - antennaLength * 0.5; // 向上延伸的中点 - const rightAntennaEndX = rightAntennaStartX + antennaLength * 0.4; // 向右弯曲 - const rightAntennaEndY = rightAntennaMidY - antennaLength * 0.3; // 向上并向右 - - ctx.strokeStyle = '#000'; - ctx.lineWidth = 1.5; - ctx.lineCap = 'round'; // 圆角端点 - - // 绘制左侧触角(使用二次贝塞尔曲线实现平滑弧度) - ctx.beginPath(); - ctx.moveTo(leftAntennaStartX, leftAntennaStartY); - // 使用二次贝塞尔曲线:起点、控制点、终点 - // 控制点在中间位置,使曲线平滑向左弯曲 - const leftControlX = leftAntennaStartX - antennaLength * 0.2; - const leftControlY = leftAntennaMidY; - ctx.quadraticCurveTo( - leftControlX, - leftControlY, - leftAntennaEndX, - leftAntennaEndY, - ); - ctx.stroke(); - - // 绘制右侧触角(使用二次贝塞尔曲线实现平滑弧度) - ctx.beginPath(); - ctx.moveTo(rightAntennaStartX, rightAntennaStartY); - // 使用二次贝塞尔曲线:起点、控制点、终点 - // 控制点在中间位置,使曲线平滑向右弯曲 - const rightControlX = rightAntennaStartX + antennaLength * 0.2; - const rightControlY = rightAntennaMidY; - ctx.quadraticCurveTo( - rightControlX, - rightControlY, - rightAntennaEndX, - rightAntennaEndY, - ); - ctx.stroke(); - - // 绘制触角末端的小圆点 - ctx.fillStyle = '#000'; - ctx.beginPath(); - ctx.arc(leftAntennaEndX, leftAntennaEndY, 2, 0, Math.PI * 2); - ctx.fill(); - ctx.beginPath(); - ctx.arc(rightAntennaEndX, rightAntennaEndY, 2, 0, Math.PI * 2); - ctx.fill(); - - // 绘制眼睛 - const eyeSize = headRadius * 0.15; - const eyeOffsetX = headRadius * 0.25; - const eyeOffsetY = headRadius * 0.22; - ctx.fillStyle = '#fff'; - ctx.beginPath(); - ctx.arc( - headCenterX - eyeOffsetX - 2, - headCenterY - eyeOffsetY, - eyeSize, - 0, - Math.PI * 2, - ); - ctx.fill(); - ctx.beginPath(); - ctx.arc( - headCenterX + eyeOffsetX - 2, - headCenterY - eyeOffsetY, - eyeSize, - 0, - Math.PI * 2, - ); - ctx.fill(); - - // 绘制眼珠 - ctx.fillStyle = '#000'; - ctx.beginPath(); - ctx.arc( - headCenterX - eyeOffsetX - 2, - headCenterY - eyeOffsetY, - eyeSize * 0.6, - 0, - Math.PI * 2, - ); - ctx.fill(); - ctx.beginPath(); - ctx.arc( - headCenterX + eyeOffsetX - 2, - headCenterY - eyeOffsetY, - eyeSize * 0.6, - 0, - Math.PI * 2, - ); - ctx.fill(); - - // 绘制嘴巴(微笑) - ctx.strokeStyle = '#000'; - ctx.lineWidth = 1.5; - ctx.beginPath(); - ctx.arc( - headCenterX - 2, - headCenterY + headRadius * 0.22, - headRadius * 0.3, - 0, - Math.PI, - ); - ctx.stroke(); -} - -/** - * 绘制毛毛虫身体(带弧度的圆圈) - */ -function drawCaterpillarBody( - ctx: RenderingContext, - startX: number, - startY: number, - count: number, - bodyRadius: number, - color: string, -) { - const spacing = bodyRadius * 2; // 身体圆圈之间的间距(稍微重叠) - const waveAmplitude = bodyRadius * 0.4; // 波浪幅度 - - for (let i = 0; i < count; i++) { - // 计算每个身体圆圈的位置(带弧度,形成弯曲效果) - const baseX = startX + i * spacing; - // 使用正弦函数创建波浪效果,使身体有弧度 - const waveOffset = Math.sin((i * Math.PI) / 2.5) * waveAmplitude; - const bodyX = baseX; - const bodyY = startY + waveOffset; - - // 绘制身体圆圈 - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(bodyX, bodyY, bodyRadius, 0, Math.PI * 2); - ctx.fill(); - - // 绘制边框(可选,根据设计需求) - ctx.strokeStyle = '#000'; - ctx.lineWidth = 1; - ctx.stroke(); - } -} - -/** - * 绘制圆圈模式的内容 - */ -function drawCirclePattern( - ctx: RenderingContext, - x: number, - y: number, - width: number, - height: number, - number: number, - numberColor: string, - isFirstRow: boolean, -) { - const totalCircles = 10; - const circleRadius = 16; // 固定半径 - const circleSpacing = 6; // 固定间距 - - // 计算所有圆圈的总宽度 - const totalWidth = - totalCircles * circleRadius * 2 + (totalCircles - 1) * circleSpacing; - - // 计算水平居中位置 - const startX = x + (width - totalWidth) / 2 + circleRadius; - - // 计算垂直居中位置 - const centerY = y + height / 2; - - // 绘制10个虚线圆圈(一行) - for (let i = 0; i < totalCircles; i++) { - const circleX = startX + i * (circleRadius * 2 + circleSpacing); - - // 绘制虚线圆圈 - ctx.strokeStyle = numberColor; - ctx.lineWidth = 1; - ctx.setLineDash([3, 3]); - ctx.beginPath(); - ctx.arc(circleX, centerY, circleRadius, 0, Math.PI * 2); - ctx.stroke(); - ctx.setLineDash([]); - - // 第一行:根据数字数量涂色 - if (isFirstRow && i < number) { - ctx.fillStyle = numberColor; - ctx.beginPath(); - ctx.arc(circleX, centerY, circleRadius - 1, 0, Math.PI * 2); - ctx.fill(); - } - } -} - -/** - * 绘制毛毛虫模式的内容 - */ -function drawCaterpillarPattern( - ctx: RenderingContext, - x: number, - y: number, - width: number, - height: number, - number: number, - numberColor: string, - isFirstRow: boolean, -) { - const padding = 15; - const headSize = 45; // 头部区域高度(用于垂直居中) - const headRadius = 18; // 固定头部半径 - const bodyRadius = 16; // 固定身体半径 - - if (isFirstRow) { - // 第一行:绘制完整的毛毛虫(头部+身体) - const headX = x + padding; - const headY = y + (height - headSize) / 2; - - // 绘制头部 - drawCaterpillarHead(ctx, headX, headY, headSize, numberColor); - - // 计算头部的实际宽度(头部半径 * 2) - const headWidth = headRadius * 2; // 头部的实际宽度 = 36 - - // 绘制身体(从头部右侧边缘开始,留一点间距) - const bodyStartX = headX + headWidth + 17; // 头部右边缘 + 5px间距 - const bodyStartY = headY + headSize / 2; // 与头部中心Y对齐 - drawCaterpillarBody( - ctx, - bodyStartX, - bodyStartY, - number, - bodyRadius, - numberColor, - ); - } else { - // 其他行:只绘制头部 - const headX = x + padding; - const headY = y + (height - headSize) / 2; - drawCaterpillarHead(ctx, headX, headY, headSize, numberColor); - } -} - -/** - * 绘制按数字涂颜色内容区域 - */ -export async function drawNumberColorContent({ - canvas, - ctx, - colorData, - canvasWidth, - startY, - patternType = 'circle', -}: DrawNumberColorContentParams): Promise { - const { numbers } = colorData; - - const leftMargin = 24; - const rightMargin = 24; - const itemSpacing = 115; // 行间距 - const leftBoxWidth = 100; - const rightBoxWidth = 420; - const boxHeight = 80; - const borderRadius = 12; - const startYPos = startY + 20; - - // 获取所有可用颜色值 - const availableColors = getNumberColors(); - - // 为每一行随机分配颜色,确保不重复 - const shuffledColors = [...availableColors]; - // Fisher-Yates 洗牌算法 - for (let i = shuffledColors.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [shuffledColors[i], shuffledColors[j]] = [ - shuffledColors[j], - shuffledColors[i], - ]; - } - - // 为每一行分配颜色(确保不重复) - // 如果行数超过颜色数量,从剩余颜色中随机选择,但确保相邻行不同 - const rowColors: string[] = []; - - for (let i = 0; i < numbers.length; i++) { - if (i < shuffledColors.length) { - // 前 N 行(N <= 颜色数量)使用不同的颜色 - rowColors.push(shuffledColors[i]); - } else { - // 如果行数超过颜色数量,从剩余颜色中选择(确保与上一行不同) - const remainingColors = shuffledColors.filter( - (color) => color !== rowColors[i - 1], - ); - const randomColor = - remainingColors[ - Math.floor(Math.random() * remainingColors.length) - ]; - rowColors.push(randomColor); - } - } - - // 计算左侧和右侧的起始X位置 - const leftBoxX = leftMargin; - const rightBoxX = canvasWidth - rightMargin - rightBoxWidth; - - // 绘制每一行 - for (let i = 0; i < numbers.length; i++) { - const boxY = startYPos + i * itemSpacing; - const number = numbers[i]; - const numberColor = rowColors[i]; // 使用随机分配的颜色 - const isFirstRow = i === 0; - - // 绘制左侧数字框(虚线) - ctx.strokeStyle = '#999'; - ctx.lineWidth = 1; - drawRoundedRect( - ctx, - leftBoxX, - boxY, - leftBoxWidth, - boxHeight, - borderRadius, - true, - ); - - // 绘制数字(带颜色) - ctx.fillStyle = numberColor; - ctx.font = `bold ${56}px "Microsoft Yahei"`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText( - String(number), - leftBoxX + leftBoxWidth / 2, - boxY + boxHeight / 2, - ); - - // 绘制右侧内容框(虚线) - ctx.strokeStyle = '#999'; - ctx.lineWidth = 1; - drawRoundedRect( - ctx, - rightBoxX, - boxY, - rightBoxWidth, - boxHeight, - borderRadius, - true, - ); - - // 根据模式绘制右侧内容 - if (patternType === 'circle') { - drawCirclePattern( - ctx, - rightBoxX, - boxY, - rightBoxWidth, - boxHeight, - number, - numberColor, - isFirstRow, - ); - } else if (patternType === 'caterpillar') { - drawCaterpillarPattern( - ctx, - rightBoxX, - boxY, - rightBoxWidth, - boxHeight, - number, - numberColor, - isFirstRow, - ); - } - } -} diff --git a/miniprogram/mathPages/shared/service/numberColorDraw.ts b/miniprogram/mathPages/shared/service/numberColorDraw.ts index 01d0021..f7b05b3 100644 --- a/miniprogram/mathPages/shared/service/numberColorDraw.ts +++ b/miniprogram/mathPages/shared/service/numberColorDraw.ts @@ -1,14 +1,22 @@ +/** + * 按数字涂颜色绘制服务 + */ + import { BaseDrawService } from '../../../service/baseDraw'; -import { drawNumberColorContent } from './numberColorContentDraw'; +import { getNumberColors } from '../../../constants/colors'; + +/** + * 按数字涂颜色数据 + */ +export interface NumberColorData { + numbers: number[]; +} /** * 按数字涂颜色绘制服务 - * 组合使用基础绘制服务和内容区域绘制服务 */ class NumberColorDraw extends BaseDrawService { - colorData: { - numbers: number[]; - } | null; + colorData: NumberColorData | null = null; constructor( canvas: Canvas, @@ -19,29 +27,21 @@ class NumberColorDraw extends BaseDrawService { this.colorData = null; } + /** + * 绘制按数字涂颜色内容 + */ async draw( - colorData: { numbers: number[] }, + colorData: NumberColorData, patternType: string = 'circle', // 'circle' 或 'caterpillar' ) { if (!colorData || !colorData.numbers) { return; } - this.setPrintConfig(); this.colorData = colorData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域(按数字涂颜色) - this.drawDivider(); - await drawNumberColorContent({ + this.prepareDraw(); + await this.drawHeaderAndDivider(); + await this.drawContent({ canvas: this.canvas, ctx: this.ctx, colorData: this.colorData, @@ -50,6 +50,466 @@ class NumberColorDraw extends BaseDrawService { patternType, }); } + + /** + * 绘制内容区域 + */ + private async drawContent(params: { + canvas: WechatMiniprogram.Canvas; + ctx: RenderingContext; + colorData: NumberColorData; + canvasWidth: number; + startY: number; + patternType?: string; // 'circle' 或 'caterpillar' + }): Promise { + const { + ctx, + colorData, + canvasWidth, + startY, + patternType = 'circle', + } = params; + const { numbers } = colorData; + + const leftMargin = 24; + const rightMargin = 24; + const itemSpacing = 115; // 行间距 + const leftBoxWidth = 100; + const rightBoxWidth = 420; + const boxHeight = 80; + const borderRadius = 12; + const startYPos = startY + 20; + + // 获取所有可用颜色值 + const availableColors = getNumberColors(); + + // 为每一行随机分配颜色,确保不重复 + const shuffledColors = [...availableColors]; + // Fisher-Yates 洗牌算法 + for (let i = shuffledColors.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffledColors[i], shuffledColors[j]] = [ + shuffledColors[j], + shuffledColors[i], + ]; + } + + // 为每一行分配颜色(确保不重复) + // 如果行数超过颜色数量,从剩余颜色中随机选择,但确保相邻行不同 + const rowColors: string[] = []; + + for (let i = 0; i < numbers.length; i++) { + if (i < shuffledColors.length) { + // 前 N 行(N <= 颜色数量)使用不同的颜色 + rowColors.push(shuffledColors[i]); + } else { + // 如果行数超过颜色数量,从剩余颜色中选择(确保与上一行不同) + const remainingColors = shuffledColors.filter( + (color) => color !== rowColors[i - 1], + ); + const randomColor = + remainingColors[ + Math.floor(Math.random() * remainingColors.length) + ]; + rowColors.push(randomColor); + } + } + + // 计算左侧和右侧的起始X位置 + const leftBoxX = leftMargin; + const rightBoxX = canvasWidth - rightMargin - rightBoxWidth; + + // 绘制每一行 + for (let i = 0; i < numbers.length; i++) { + const boxY = startYPos + i * itemSpacing; + const number = numbers[i]; + const numberColor = rowColors[i]; // 使用随机分配的颜色 + const isFirstRow = i === 0; + + // 绘制左侧数字框(虚线) + ctx.strokeStyle = '#999'; + ctx.lineWidth = 1; + this.drawRoundedRectInternal( + ctx, + leftBoxX, + boxY, + leftBoxWidth, + boxHeight, + borderRadius, + true, + ); + + // 绘制数字(带颜色) + ctx.fillStyle = numberColor; + ctx.font = `bold ${56}px "Microsoft Yahei"`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText( + String(number), + leftBoxX + leftBoxWidth / 2, + boxY + boxHeight / 2, + ); + + // 绘制右侧内容框(虚线) + ctx.strokeStyle = '#999'; + ctx.lineWidth = 1; + this.drawRoundedRectInternal( + ctx, + rightBoxX, + boxY, + rightBoxWidth, + boxHeight, + borderRadius, + true, + ); + + // 根据模式绘制右侧内容 + if (patternType === 'circle') { + this.drawCirclePattern( + ctx, + rightBoxX, + boxY, + rightBoxWidth, + boxHeight, + number, + numberColor, + isFirstRow, + ); + } else if (patternType === 'caterpillar') { + this.drawCaterpillarPattern( + ctx, + rightBoxX, + boxY, + rightBoxWidth, + boxHeight, + number, + numberColor, + isFirstRow, + ); + } + } + } + + /** + * 绘制圆圈模式的内容 + */ + private drawCirclePattern( + ctx: RenderingContext, + x: number, + y: number, + width: number, + height: number, + number: number, + numberColor: string, + isFirstRow: boolean, + ) { + const totalCircles = 10; + const circleRadius = 16; // 固定半径 + const circleSpacing = 6; // 固定间距 + + // 计算所有圆圈的总宽度 + const totalWidth = + totalCircles * circleRadius * 2 + + (totalCircles - 1) * circleSpacing; + + // 计算水平居中位置 + const startX = x + (width - totalWidth) / 2 + circleRadius; + + // 计算垂直居中位置 + const centerY = y + height / 2; + + // 绘制10个虚线圆圈(一行) + for (let i = 0; i < totalCircles; i++) { + const circleX = startX + i * (circleRadius * 2 + circleSpacing); + + // 绘制虚线圆圈 + ctx.strokeStyle = numberColor; + ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); + ctx.beginPath(); + ctx.arc(circleX, centerY, circleRadius, 0, Math.PI * 2); + ctx.stroke(); + ctx.setLineDash([]); + + // 第一行:根据数字数量涂色 + if (isFirstRow && i < number) { + ctx.fillStyle = numberColor; + ctx.beginPath(); + ctx.arc(circleX, centerY, circleRadius - 1, 0, Math.PI * 2); + ctx.fill(); + } + } + } + + /** + * 绘制毛毛虫模式的内容 + */ + private drawCaterpillarPattern( + ctx: RenderingContext, + x: number, + y: number, + width: number, + height: number, + number: number, + numberColor: string, + isFirstRow: boolean, + ) { + const padding = 15; + const headSize = 45; // 头部区域高度(用于垂直居中) + const headRadius = 18; // 固定头部半径 + const bodyRadius = 16; // 固定身体半径 + + if (isFirstRow) { + // 第一行:绘制完整的毛毛虫(头部+身体) + const headX = x + padding; + const headY = y + (height - headSize) / 2; + + // 绘制头部 + this.drawCaterpillarHead(ctx, headX, headY, headSize, numberColor); + + // 计算头部的实际宽度(头部半径 * 2) + const headWidth = headRadius * 2; // 头部的实际宽度 = 36 + + // 绘制身体(从头部右侧边缘开始,留一点间距) + const bodyStartX = headX + headWidth + 17; // 头部右边缘 + 5px间距 + const bodyStartY = headY + headSize / 2; // 与头部中心Y对齐 + this.drawCaterpillarBody( + ctx, + bodyStartX, + bodyStartY, + number, + bodyRadius, + numberColor, + ); + } else { + // 其他行:只绘制头部 + const headX = x + padding; + const headY = y + (height - headSize) / 2; + this.drawCaterpillarHead(ctx, headX, headY, headSize, numberColor); + } + } + + /** + * 绘制毛毛虫头部 + */ + private drawCaterpillarHead( + ctx: RenderingContext, + x: number, + y: number, + size: number, + color: string, + ) { + const headRadius = 18; // 固定头部半径 + const headCenterX = x + headRadius; + const headCenterY = y + size / 2; + + // 绘制头部(圆形) + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2); + ctx.fill(); + + // 绘制头部圆圈实线边框 + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1; + ctx.setLineDash([]); // 确保是实线 + ctx.beginPath(); + ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2); + ctx.stroke(); + + // 绘制触角(在头部圆圈的正上方,带弧度的弯曲) + const antennaLength = headRadius * 0.6; + const antennaStartY = headCenterY - headRadius; // 头部圆圈的正上方 + const antennaOffset = headRadius * 0.3; // 触角左右偏移距离 + + // 左侧触角:从顶部左侧开始,先向上,然后向左弯曲(带弧度) + const leftAntennaStartX = headCenterX - antennaOffset; + const leftAntennaStartY = antennaStartY; + const leftAntennaMidY = leftAntennaStartY - antennaLength * 0.5; // 向上延伸的中点 + const leftAntennaEndX = leftAntennaStartX - antennaLength * 0.4; // 向左弯曲 + const leftAntennaEndY = leftAntennaMidY - antennaLength * 0.3; // 向上并向左 + + // 右侧触角:从顶部右侧开始,先向上,然后向右弯曲(带弧度) + const rightAntennaStartX = headCenterX + antennaOffset; + const rightAntennaStartY = antennaStartY; + const rightAntennaMidY = rightAntennaStartY - antennaLength * 0.5; // 向上延伸的中点 + const rightAntennaEndX = rightAntennaStartX + antennaLength * 0.4; // 向右弯曲 + const rightAntennaEndY = rightAntennaMidY - antennaLength * 0.3; // 向上并向右 + + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1.5; + ctx.lineCap = 'round'; // 圆角端点 + + // 绘制左侧触角(使用二次贝塞尔曲线实现平滑弧度) + ctx.beginPath(); + ctx.moveTo(leftAntennaStartX, leftAntennaStartY); + // 使用二次贝塞尔曲线:起点、控制点、终点 + // 控制点在中间位置,使曲线平滑向左弯曲 + const leftControlX = leftAntennaStartX - antennaLength * 0.2; + const leftControlY = leftAntennaMidY; + ctx.quadraticCurveTo( + leftControlX, + leftControlY, + leftAntennaEndX, + leftAntennaEndY, + ); + ctx.stroke(); + + // 绘制右侧触角(使用二次贝塞尔曲线实现平滑弧度) + ctx.beginPath(); + ctx.moveTo(rightAntennaStartX, rightAntennaStartY); + // 使用二次贝塞尔曲线:起点、控制点、终点 + // 控制点在中间位置,使曲线平滑向右弯曲 + const rightControlX = rightAntennaStartX + antennaLength * 0.2; + const rightControlY = rightAntennaMidY; + ctx.quadraticCurveTo( + rightControlX, + rightControlY, + rightAntennaEndX, + rightAntennaEndY, + ); + ctx.stroke(); + + // 绘制触角末端的小圆点 + ctx.fillStyle = '#000'; + ctx.beginPath(); + ctx.arc(leftAntennaEndX, leftAntennaEndY, 2, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.arc(rightAntennaEndX, rightAntennaEndY, 2, 0, Math.PI * 2); + ctx.fill(); + + // 绘制眼睛 + const eyeSize = headRadius * 0.15; + const eyeOffsetX = headRadius * 0.25; + const eyeOffsetY = headRadius * 0.22; + ctx.fillStyle = '#fff'; + ctx.beginPath(); + ctx.arc( + headCenterX - eyeOffsetX - 2, + headCenterY - eyeOffsetY, + eyeSize, + 0, + Math.PI * 2, + ); + ctx.fill(); + ctx.beginPath(); + ctx.arc( + headCenterX + eyeOffsetX - 2, + headCenterY - eyeOffsetY, + eyeSize, + 0, + Math.PI * 2, + ); + ctx.fill(); + + // 绘制眼珠 + ctx.fillStyle = '#000'; + ctx.beginPath(); + ctx.arc( + headCenterX - eyeOffsetX - 2, + headCenterY - eyeOffsetY, + eyeSize * 0.6, + 0, + Math.PI * 2, + ); + ctx.fill(); + ctx.beginPath(); + ctx.arc( + headCenterX + eyeOffsetX - 2, + headCenterY - eyeOffsetY, + eyeSize * 0.6, + 0, + Math.PI * 2, + ); + ctx.fill(); + + // 绘制嘴巴(微笑) + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1.5; + ctx.beginPath(); + ctx.arc( + headCenterX - 2, + headCenterY + headRadius * 0.22, + headRadius * 0.3, + 0, + Math.PI, + ); + ctx.stroke(); + } + + /** + * 绘制毛毛虫身体(带弧度的圆圈) + */ + private drawCaterpillarBody( + ctx: RenderingContext, + startX: number, + startY: number, + count: number, + bodyRadius: number, + color: string, + ) { + const spacing = bodyRadius * 2; // 身体圆圈之间的间距(稍微重叠) + const waveAmplitude = bodyRadius * 0.4; // 波浪幅度 + + for (let i = 0; i < count; i++) { + // 计算每个身体圆圈的位置(带弧度,形成弯曲效果) + const baseX = startX + i * spacing; + // 使用正弦函数创建波浪效果,使身体有弧度 + const waveOffset = Math.sin((i * Math.PI) / 2.5) * waveAmplitude; + const bodyX = baseX; + const bodyY = startY + waveOffset; + + // 绘制身体圆圈 + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(bodyX, bodyY, bodyRadius, 0, Math.PI * 2); + ctx.fill(); + + // 绘制边框(可选,根据设计需求) + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + + /** + * 绘制圆角矩形(虚线边框) + */ + 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, 6]); // 虚线 + } else { + ctx.setLineDash([]); + } + ctx.stroke(); + ctx.setLineDash([]); // 重置为实线 + } } export default NumberColorDraw; diff --git a/miniprogram/mathPages/shared/service/numberContentDraw.ts b/miniprogram/mathPages/shared/service/numberContentDraw.ts deleted file mode 100644 index f188706..0000000 --- a/miniprogram/mathPages/shared/service/numberContentDraw.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * 绘制内容区域的参数接口 - */ -interface DrawNumberContentParams { - ctx: RenderingContext; - selectedNumber: number; - canvasWidth: number; // 逻辑像素宽度(已除以3) - startY: number; // 起始Y坐标 -} - -/** - * 绘制数字涂色内容区域服务 - * 随机绘制1-10的数字,尺寸除以3 - */ -export function drawNumberContent({ - ctx, - selectedNumber, - canvasWidth, - startY, -}: DrawNumberContentParams): void { - const rows = 6; - const radius = 26; // 约26.67 - const fontSize = 24; // 24 - - const startYPos = startY + 10 + radius; // 约47.33 - const startX1 = 100 + radius; // 约112.22 - const startX2 = 60 + radius; // 约75.56 - const spaceWidthFirst = (canvasWidth - startX1 * 2) / (5 - 1); - const spaceWidthSecond = (canvasWidth - startX2 * 2) / (6 - 1); - const spaceHeight = 18; // 18 - - ctx.moveTo(startX1, startYPos); - for (let i = 0; i < rows; i++) { - const cols = i % 2 === 0 ? 5 : 6; - - for (let j = 0; j < cols; j++) { - const y = startYPos + i * (spaceHeight + radius * 2); - const x = - i % 2 === 0 - ? startX1 + j * spaceWidthFirst - : startX2 + j * spaceWidthSecond; - - // 随机生成1-10的数字,但确保包含选中的数字 - let number: number; - if (i === 0 && j === 0) { - // 第一个位置固定为选中的数字 - number = selectedNumber; - } else { - // 其他位置随机,但增加选中数字出现的概率 - const random = Math.random(); - if (random < 0.3) { - // 30%概率是选中的数字 - number = selectedNumber; - } else { - // 70%概率是其他随机数字 - number = Math.floor(Math.random() * 10) + 1; - } - } - - // 绘制圆形背景(线宽除以3) - ctx.fillStyle = '#fff'; - ctx.strokeStyle = '#000'; - ctx.lineWidth = 1; // 约1.33 - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fill(); - ctx.stroke(); - ctx.closePath(); - - // 绘制数字(字体大小除以3) - ctx.fillStyle = '#333'; - ctx.font = `bold ${fontSize}px "Microsoft Yahei"`; - ctx.textBaseline = 'middle'; - ctx.textAlign = 'center'; - ctx.fillText(String(number), x, y, radius * 2); - } - } -} diff --git a/miniprogram/mathPages/shared/service/numberDecomposeContentDraw.ts b/miniprogram/mathPages/shared/service/numberDecomposeContentDraw.ts deleted file mode 100644 index 157fac1..0000000 --- a/miniprogram/mathPages/shared/service/numberDecomposeContentDraw.ts +++ /dev/null @@ -1,504 +0,0 @@ -import { getImage } from '../../../utils/index'; -import { getRandomNumberColor } from '../../../constants/colors'; - -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); - } - } -} diff --git a/miniprogram/mathPages/shared/service/numberDecomposeDraw.ts b/miniprogram/mathPages/shared/service/numberDecomposeDraw.ts index 6fc86dc..b3f33c3 100644 --- a/miniprogram/mathPages/shared/service/numberDecomposeDraw.ts +++ b/miniprogram/mathPages/shared/service/numberDecomposeDraw.ts @@ -1,21 +1,30 @@ +/** + * 10以内数的分与合绘制服务 + */ + import { BaseDrawService } from '../../../service/baseDraw'; -import { drawNumberDecomposeContent } from './numberDecomposeContentDraw'; +import { getImage } from '../../../utils/index'; +import { getRandomNumberColor } from '../../../constants/colors'; + +/** + * 10以内数的分与合数据 + */ +export interface NumberDecomposeData { + problems: Array<{ + whole: number | null; + part1: number | null; + part2: number | null; + imageIndex?: number; + imageType?: 'fruits' | 'twelve-animals'; + }>; + mode: 'with-image' | 'decompose' | 'compose'; +} /** * 10以内数的分与合绘制服务 - * 组合使用基础绘制服务和内容区域绘制服务 */ class NumberDecomposeDraw extends BaseDrawService { - decomposeData: { - problems: Array<{ - whole: number | null; - part1: number | null; - part2: number | null; - imageIndex?: number; - imageType?: 'fruits' | 'twelve-animals'; - }>; - mode: 'with-image' | 'decompose' | 'compose'; - } | null; + decomposeData: NumberDecomposeData | null = null; constructor( canvas: Canvas, @@ -26,35 +35,18 @@ class NumberDecomposeDraw extends BaseDrawService { this.decomposeData = null; } - async draw(decomposeData: { - problems: Array<{ - whole: number | null; - part1: number | null; - part2: number | null; - imageIndex?: number; - imageType?: 'fruits' | 'twelve-animals'; - }>; - mode: 'with-image' | 'decompose' | 'compose'; - }) { + /** + * 绘制10以内数的分与合内容 + */ + async draw(decomposeData: NumberDecomposeData) { if (!decomposeData || !decomposeData.problems) { return; } - this.setPrintConfig(); this.decomposeData = decomposeData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域 - this.drawDivider(); - await drawNumberDecomposeContent({ + this.prepareDraw(); + await this.drawHeaderAndDivider(); + await this.drawContent({ canvas: this.canvas, ctx: this.ctx, decomposeData: this.decomposeData, @@ -62,6 +54,519 @@ class NumberDecomposeDraw extends BaseDrawService { startY: this.currentY, }); } + + /** + * 绘制内容区域 + */ + private async drawContent(params: { + canvas: WechatMiniprogram.Canvas; + ctx: RenderingContext; + decomposeData: NumberDecomposeData; + canvasWidth: number; + startY: number; + }): Promise { + const { canvas, ctx, decomposeData, canvasWidth, startY } = params; + const { problems, mode } = decomposeData; + + if (mode === 'with-image') { + await this.drawWithImageMode( + canvas, + ctx, + problems, + canvasWidth, + startY, + ); + } else if (mode === 'decompose') { + await this.drawDecomposeOrComposeMode( + ctx, + problems, + canvasWidth, + startY, + false, + ); + } else if (mode === 'compose') { + await this.drawDecomposeOrComposeMode( + ctx, + problems, + canvasWidth, + startY, + true, + ); + } + } + + /** + * 有图片模式:一行三列,总共三行(9个题目) + */ + private async 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 this.drawWithImageProblem( + ctx, + canvas, + problem, + boxX, + boxY, + boxWidth, + imageAreaHeight, + treeAreaHeight, + ); + } + } + } + + /** + * 分模式/组合模式:一行3个,总共5行(15个题目) + * @param isInverted true 为组合模式(倒置二叉树),false 为分模式(正常二叉树) + */ + private async 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; + + // 绘制二叉树 + this.drawTree( + ctx, + problem, + boxX, + boxY, + boxWidth, + boxHeight, + isInverted, + ); + } + } + } + + /** + * 绘制有图片模式的单个题目 + */ + private async 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; + this.drawRoundedRectInternal( + ctx, + boxX, + boxY, + boxWidth, + imageAreaHeight, + borderRadius, + false, + ); + + // 图片区域(增加底部间距,避免与二叉树根节点重叠) + const imageBottomPadding = 20; // 图片区域底部额外间距 + const effectiveImageHeight = imageAreaHeight - imageBottomPadding; + + if (problem.imageIndex && problem.imageType && problem.whole) { + await this.drawImagesInBox( + ctx, + canvas, + problem.whole, + problem.imageIndex, + problem.imageType, + boxX, + boxY, + boxWidth, + effectiveImageHeight, + ); + } + + // 二叉树区域(增加与图片区域的间距) + const treeTopSpacing = -5; // 二叉树区域顶部间距 + const treeY = boxY + imageAreaHeight + treeTopSpacing; + this.drawTree( + ctx, + problem, + boxX, + treeY, + boxWidth, + treeAreaHeight, + false, // 不是倒置 + ); + } + + /** + * 绘制二叉树结构 + * @param isInverted 是否倒置(组合模式为true) + */ + private 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; + + // 左子节点 + this.drawNode(ctx, leftNodeX, topY, nodeRadius, problem.part1); + // 右子节点 + this.drawNode(ctx, rightNodeX, topY, nodeRadius, problem.part2); + + // 绘制根节点(底部) + this.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; + + // 绘制根节点(顶部) + this.drawNode(ctx, centerX, topY, nodeRadius, problem.whole); + + // 绘制两个子节点(底部) + const leftNodeX = centerX - nodeSpacing / 2 - nodeRadius; + const rightNodeX = centerX + nodeSpacing / 2 + nodeRadius; + + // 左子节点 + this.drawNode(ctx, leftNodeX, bottomY, nodeRadius, problem.part1); + // 右子节点 + this.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(); + } + } + + /** + * 绘制节点(圆圈和数字) + */ + private 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); + } + } + + /** + * 在框内绘制多张图片 + */ + 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 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, + ); + } + } + } + + /** + * 绘制圆角矩形(虚线边框) + */ + private drawRoundedRectInternal( + 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 default NumberDecomposeDraw; diff --git a/miniprogram/mathPages/shared/service/numberFindDraw.ts b/miniprogram/mathPages/shared/service/numberFindDraw.ts index cffb7b0..e820827 100644 --- a/miniprogram/mathPages/shared/service/numberFindDraw.ts +++ b/miniprogram/mathPages/shared/service/numberFindDraw.ts @@ -1,6 +1,5 @@ import { BaseDrawService } from '../../../service/baseDraw'; import { drawNumberPreview } from './numberPreviewDraw'; -import { drawNumberContent } from './numberContentDraw'; import { drawNumberWriteContent } from './numberWriteDraw'; /** @@ -26,20 +25,11 @@ class NumberFindDraw extends BaseDrawService { return; } - this.setPrintConfig(); this.selectedNumber = selectedNumber; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } + this.prepareDraw(); + await this.drawHeaderAndDivider(); // 绘制预览区域 - this.drawDivider(); await drawNumberPreview({ canvas: this.canvas, ctx: this.ctx, @@ -63,7 +53,7 @@ class NumberFindDraw extends BaseDrawService { }); } else { // 默认类型(number-find):绘制数字涂色内容 - drawNumberContent({ + this.drawNumberContent({ ctx: this.ctx, selectedNumber: this.selectedNumber, canvasWidth: this.canvasWidth, @@ -71,6 +61,75 @@ class NumberFindDraw extends BaseDrawService { }); } } + + /** + * 绘制数字涂色内容区域 + */ + private drawNumberContent(params: { + ctx: RenderingContext; + selectedNumber: number; + canvasWidth: number; + startY: number; + }): void { + const { ctx, selectedNumber, canvasWidth, startY } = params; + const rows = 6; + const radius = 26; // 约26.67 + const fontSize = 24; // 24 + + const startYPos = startY + 10 + radius; // 约47.33 + const startX1 = 100 + radius; // 约112.22 + const startX2 = 60 + radius; // 约75.56 + const spaceWidthFirst = (canvasWidth - startX1 * 2) / (5 - 1); + const spaceWidthSecond = (canvasWidth - startX2 * 2) / (6 - 1); + const spaceHeight = 18; // 18 + + ctx.moveTo(startX1, startYPos); + for (let i = 0; i < rows; i++) { + const cols = i % 2 === 0 ? 5 : 6; + + for (let j = 0; j < cols; j++) { + const y = startYPos + i * (spaceHeight + radius * 2); + const x = + i % 2 === 0 + ? startX1 + j * spaceWidthFirst + : startX2 + j * spaceWidthSecond; + + // 随机生成1-10的数字,但确保包含选中的数字 + let number: number; + if (i === 0 && j === 0) { + // 第一个位置固定为选中的数字 + number = selectedNumber; + } else { + // 其他位置随机,但增加选中数字出现的概率 + const random = Math.random(); + if (random < 0.3) { + // 30%概率是选中的数字 + number = selectedNumber; + } else { + // 70%概率是其他随机数字 + number = Math.floor(Math.random() * 10) + 1; + } + } + + // 绘制圆形背景(线宽除以3) + ctx.fillStyle = '#fff'; + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1; // 约1.33 + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + ctx.closePath(); + + // 绘制数字(字体大小除以3) + ctx.fillStyle = '#333'; + ctx.font = `bold ${fontSize}px "Microsoft Yahei"`; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + ctx.fillText(String(number), x, y, radius * 2); + } + } + } } export default NumberFindDraw; diff --git a/miniprogram/mathPages/shared/service/numberSortDraw.ts b/miniprogram/mathPages/shared/service/numberSortDraw.ts index 46d3536..a79e16c 100644 --- a/miniprogram/mathPages/shared/service/numberSortDraw.ts +++ b/miniprogram/mathPages/shared/service/numberSortDraw.ts @@ -41,20 +41,9 @@ class NumberSortDraw extends BaseDrawService { return; } - this.setPrintConfig(); this.sortData = sortData; - this.clear(); - this.setPaper(); - - // 绘制Header - if (this.headerType !== 'minimal') { - await this.drawHeader(); - } else { - this.drawMiniHeader(); - } - - // 绘制内容区域 - this.drawDivider(); + this.prepareDraw(); + await this.drawHeaderAndDivider(); this.drawContent({ ctx: this.ctx, sortData: this.sortData, diff --git a/miniprogram/service/baseDraw.ts b/miniprogram/service/baseDraw.ts index b4867d3..60c9296 100644 --- a/miniprogram/service/baseDraw.ts +++ b/miniprogram/service/baseDraw.ts @@ -397,4 +397,31 @@ export class BaseDrawService { ctx.stroke(); } } + + /** + * 准备绘制(公共初始化逻辑) + * 执行:setPrintConfig -> clear -> setPaper + * 子类可以在调用此方法前后执行自定义逻辑(如数据验证、异步资源加载等) + */ + prepareDraw() { + this.setPrintConfig(); + this.clear(); + this.setPaper(); + } + + /** + * 绘制 Header 和 Divider(公共绘制逻辑) + * 根据 headerType 自动选择绘制完整 Header 或迷你 Header,然后绘制分割线 + */ + async drawHeaderAndDivider() { + // 绘制Header + if (this.headerType !== 'minimal') { + await this.drawHeader(); + } else { + this.drawMiniHeader(); + } + + // 绘制内容区域分割线 + this.drawDivider(); + } }