Files
doodle-mini/miniprogram/mathPages/shared/service/countingSelectContentDraw.ts
T
2025-12-15 17:00:06 +08:00

313 lines
9.3 KiB
TypeScript

import { getImage } from '../../../utils/index';
import { getRandomNumberColor } from '../../../constants/mathFunctions';
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<void> {
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);
}
}
}