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

357 lines
12 KiB
TypeScript

/**
* 数一数连一连绘制服务
*/
import { BaseDrawService } from '../../../service/baseDraw';
import { getImage } from '../../../utils/index';
import { getRandomNumberColor } from '../../../constants/colors';
/**
* 数一数连一连数据
*/
export interface CountMatchData {
leftNumbers: number[];
rightNumbers: number[]; // 打乱顺序后的数字数组
}
/**
* 数一数连一连绘制服务
*/
class CountMatchDraw extends BaseDrawService {
matchData: CountMatchData | null = null;
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
this.matchData = null;
}
/**
* 绘制数一数连一连内容
*/
async draw(
matchData: CountMatchData,
imageType: string = 'twelve-animals',
) {
if (!matchData || !matchData.leftNumbers || !matchData.rightNumbers) {
return;
}
this.matchData = matchData;
this.prepareDraw();
await this.drawHeaderAndDivider();
await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
matchData: this.matchData,
canvasWidth: this.canvasWidth,
startY: this.currentY,
imageType,
});
}
/**
* 绘制内容区域
*/
private async drawContent(params: {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
matchData: CountMatchData;
canvasWidth: number;
startY: number;
imageType?: string; // 'twelve-animals' 或 'fruits'
}): Promise<void> {
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/shared/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;