Files
doodle-mini/miniprogram/mathPages/shared/service/countingSelectDraw.ts
T
2025-12-22 11:25:22 +08:00

387 lines
12 KiB
TypeScript

/**
* 数一数选一选/填一填绘制服务
* 支持两种模式:'select'(选一选)和 'fill'(填一填)
*/
import { BaseDrawService } from '../../../service/baseDraw';
import { getImage } from '../../../utils/index';
import { getRandomNumberColor } from '../../../constants/colors';
/**
* 数一数选一选/填一填数据
*/
export interface CountingSelectData {
problems: Array<{
count: number;
imageIndex: number;
imageType: 'fruits' | 'twelve-animals';
options?: number[]; // 选一选模式需要,填一填模式不需要
correctIndex?: number; // 选一选模式需要
}>;
}
/**
* 数一数选一选/填一填绘制服务
*/
class CountingSelectDraw extends BaseDrawService {
countingData: CountingSelectData | null = null;
mode: 'select' | 'fill';
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
this.countingData = null;
this.mode = 'select'; // 默认选一选模式
}
/**
* 绘制数一数选一选/填一填内容
*/
async draw(
countingData: CountingSelectData,
mode: 'select' | 'fill' = 'select',
) {
if (!countingData || !countingData.problems) {
return;
}
this.countingData = countingData;
this.mode = mode;
this.prepareDraw();
await this.drawHeaderAndDivider();
await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
countingData: this.countingData,
mode: this.mode,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
/**
* 绘制内容区域
*/
private async drawContent(params: {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
countingData: CountingSelectData;
mode: 'select' | 'fill';
canvasWidth: number;
startY: number;
}): Promise<void> {
const { canvas, ctx, countingData, mode, canvasWidth, startY } = params;
const { problems } = countingData;
// 布局参数
const leftMargin = 30;
const rightMargin = 30;
const topMargin = 20;
const boxSpacing = 20; // 框之间的间距
// 计算每个框的尺寸
const availableWidth = canvasWidth - leftMargin - rightMargin;
const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
const boxHeight = 210; // 框的高度(增加)
// 图片区域高度
const imageAreaHeight = 160;
// 底部区域高度(选项表格或括号区域)
const bottomAreaHeight = boxHeight - imageAreaHeight;
// 选项表格相关(仅 select 模式使用)
const optionCellWidth = boxWidth / 3; // 每列宽度(均匀分三列)
let currentY = startY + topMargin;
// 绘制3x3网格
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 3; col++) {
const problemIndex = row * 3 + col;
if (problemIndex >= problems.length) {
continue;
}
const problem = problems[problemIndex];
// 计算框的位置
const boxX = leftMargin + col * (boxWidth + boxSpacing);
const boxY = currentY + row * (boxHeight + boxSpacing);
// 绘制一个完整的题目框
await this.drawProblemBox(
ctx,
canvas,
problem,
boxX,
boxY,
boxWidth,
boxHeight,
imageAreaHeight,
bottomAreaHeight,
optionCellWidth,
mode,
);
}
}
}
/**
* 绘制一个题目框(图片 + 选项表格 或 括号)
*/
private async drawProblemBox(
ctx: RenderingContext,
canvas: WechatMiniprogram.Canvas,
problem: {
count: number;
imageIndex: number;
imageType: 'fruits' | 'twelve-animals';
options?: number[];
correctIndex?: number;
},
boxX: number,
boxY: number,
boxWidth: number,
boxHeight: number,
imageAreaHeight: number,
bottomAreaHeight: number,
optionCellWidth: number,
mode: 'select' | 'fill',
) {
const borderRadius = 12;
// 绘制实线框
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
this.drawRoundedRectInternal(
ctx,
boxX,
boxY,
boxWidth,
boxHeight,
borderRadius,
false,
);
// 图片区域
const imageAreaX = boxX;
const imageAreaY = boxY;
// 绘制图片
await this.drawImagesInBox(
ctx,
canvas,
problem.count,
problem.imageIndex,
problem.imageType,
imageAreaX,
imageAreaY,
boxWidth,
imageAreaHeight,
);
// 底部区域
const bottomAreaX = boxX;
const bottomAreaY = boxY + imageAreaHeight;
// 绘制底部区域的顶部横线(两种模式都需要)
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(bottomAreaX, bottomAreaY);
ctx.lineTo(bottomAreaX + boxWidth, bottomAreaY);
ctx.stroke();
if (mode === 'select' && problem.options) {
// 选一选模式:绘制三个数字选项(表格样式)
// 绘制三个单元格(均匀分三列)
for (let i = 0; i < problem.options.length; i++) {
const cellX = bottomAreaX + i * optionCellWidth;
const cellY = bottomAreaY;
// 绘制单元格边框(右边框,除了最后一个)
if (i < problem.options.length - 1) {
ctx.beginPath();
ctx.moveTo(cellX + optionCellWidth, cellY);
ctx.lineTo(
cellX + optionCellWidth,
cellY + bottomAreaHeight,
);
ctx.stroke();
}
// 绘制数字(居中)
const numberX = cellX + optionCellWidth / 2;
const numberY = cellY + bottomAreaHeight / 2;
ctx.fillStyle = getRandomNumberColor();
ctx.font = `bold ${28}px "Microsoft Yahei"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(problem.options[i]), numberX, numberY);
}
} else if (mode === 'fill') {
// 填一填模式:绘制括号 "( )"
const bracketFontSize = 28;
const bracketSpacing = 46; // 左右括号之间的间距(留空区域)
// 计算括号位置(居中)
const bracketY = bottomAreaY + bottomAreaHeight / 2;
const centerX = boxX + boxWidth / 2;
// 左括号
ctx.fillStyle = '#333';
ctx.font = `${bracketFontSize}px "Microsoft Yahei"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const leftBracketX = centerX - bracketSpacing / 2;
ctx.fillText('(', leftBracketX, bracketY);
// 右括号
const rightBracketX = centerX + bracketSpacing / 2;
ctx.fillText(')', rightBracketX, bracketY);
}
}
/**
* 在框内绘制多张图片
*/
private async drawImagesInBox(
ctx: RenderingContext,
canvas: WechatMiniprogram.Canvas,
count: number,
imageIndex: number,
imageType: 'fruits' | 'twelve-animals',
boxX: number,
boxY: number,
boxWidth: number,
boxHeight: number,
) {
// 图片配置
const imageConfig = {
'twelve-animals': {
folder: 'twelve-animals',
maxIndex: 12,
},
fruits: {
folder: 'fruits',
maxIndex: 22,
},
};
const config = imageConfig[imageType] || imageConfig['twelve-animals'];
const padding = 8; // 框内边距
const availableWidth = boxWidth - padding * 2;
const availableHeight = boxHeight - padding * 2;
// 根据数量确定每行的图片数和图片大小
let imagesPerRow: number;
let imageSize: number;
if (count <= 4) {
imagesPerRow = count <= 2 ? count : 2;
imageSize =
Math.min(availableWidth / imagesPerRow, availableHeight / 2) -
4;
} else if (count <= 6) {
imagesPerRow = 3;
imageSize = Math.min(availableWidth / 3, availableHeight / 2) - 4;
} else {
imagesPerRow = 3;
imageSize = Math.min(availableWidth / 3, availableHeight / 3) - 4;
}
const rows = Math.ceil(count / imagesPerRow);
const imageSpacing =
(availableWidth - imageSize * imagesPerRow) / (imagesPerRow + 1);
const rowSpacing =
rows > 1 ? (availableHeight - imageSize * rows) / (rows + 1) : 0;
// 加载图片
let boxImage: any = null;
try {
const imagePath = `/mathPages/shared/assets/${config.folder}/${imageIndex}.png`;
boxImage = await getImage(canvas, imagePath);
} catch (error) {
console.error(`加载${config.folder}/${imageIndex}图片失败:`, error);
return;
}
// 绘制图片
for (let i = 0; i < count; i++) {
const row = Math.floor(i / imagesPerRow);
const col = i % imagesPerRow;
const imageX =
boxX +
padding +
imageSpacing +
col * (imageSize + imageSpacing);
const imageY =
boxY +
padding +
(rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) +
row * (imageSize + (rows > 1 ? rowSpacing : 0));
if (boxImage) {
// 计算图片高度(等比例缩放)
// @ts-ignore - 微信小程序图片对象有 width 和 height 属性
const scaledHeight =
(boxImage.height / boxImage.width) * imageSize;
ctx.drawImage(
boxImage,
imageX,
imageY,
imageSize,
scaledHeight,
);
}
}
}
/**
* 绘制圆角矩形(虚线边框)
*/
private drawRoundedRectInternal(
ctx: RenderingContext,
x: number,
y: number,
width: number,
height: number,
radius: number,
isDashed: boolean = true,
) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(
x + width,
y + height,
x + width - radius,
y + height,
);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
if (isDashed) {
ctx.setLineDash([6 / 3, 6 / 3]); // 约2px的虚线
} else {
ctx.setLineDash([]);
}
ctx.stroke();
ctx.setLineDash([]); // 重置为实线
}
}
export default CountingSelectDraw;