266 lines
8.9 KiB
TypeScript
266 lines
8.9 KiB
TypeScript
/**
|
||
* 数物对应绘制服务
|
||
*/
|
||
|
||
import { BaseDrawService } from '../../../service/baseDraw';
|
||
import { getImage } from '../../../utils/index';
|
||
|
||
/**
|
||
* 数物对应数据
|
||
*/
|
||
export interface NumberObjectMatchData {
|
||
/** 上面区域的图片配置(4行4列,共16张) */
|
||
gridImages: Array<{
|
||
imageIndex: number;
|
||
folder: 'fruits' | 'twelve-animals';
|
||
offsetX: number; // 随机左右偏移
|
||
offsetY: number; // 随机上下偏移
|
||
}>;
|
||
/** 下面区域第一行的4张图片配置 */
|
||
bottomImages: Array<{
|
||
imageIndex: number;
|
||
folder: 'fruits' | 'twelve-animals';
|
||
}>;
|
||
/** 第二行要显示的数量(乱序排列) */
|
||
bottomNumbers: number[];
|
||
}
|
||
|
||
/**
|
||
* 数物对应绘制服务
|
||
*/
|
||
class NumberObjectMatchDraw extends BaseDrawService {
|
||
matchData: NumberObjectMatchData | null = null;
|
||
|
||
constructor(
|
||
canvas: Canvas,
|
||
ctx: RenderingContext,
|
||
options?: Record<string, any>,
|
||
) {
|
||
super(canvas, ctx, options);
|
||
this.matchData = null;
|
||
}
|
||
|
||
/**
|
||
* 绘制数物对应内容
|
||
*/
|
||
async draw(matchData: NumberObjectMatchData) {
|
||
if (
|
||
!matchData ||
|
||
!matchData.gridImages ||
|
||
!matchData.bottomImages ||
|
||
!matchData.bottomNumbers
|
||
) {
|
||
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,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 绘制内容区域
|
||
*/
|
||
private async drawContent(params: {
|
||
canvas: WechatMiniprogram.Canvas;
|
||
ctx: RenderingContext;
|
||
matchData: NumberObjectMatchData;
|
||
canvasWidth: number;
|
||
startY: number;
|
||
}): Promise<void> {
|
||
const { canvas, ctx, matchData, canvasWidth } = params;
|
||
let { startY } = params;
|
||
|
||
startY = startY + 25;
|
||
|
||
const margin = 0; // 左右边距
|
||
const imagePadding = 10; // 图片内边距
|
||
const imageSize = 75; // 图片大小
|
||
const cellSize = imageSize + imagePadding * 2; // 每个格子的大小
|
||
|
||
// ========== 绘制上面区域(4行4列) ==========
|
||
const gridCols = 5;
|
||
const gridRows = 4;
|
||
const gridWidth = cellSize * gridCols;
|
||
const gridStartX = margin + (canvasWidth - margin * 2 - gridWidth) / 2; // 居中
|
||
const gridStartY = startY;
|
||
|
||
// 加载并绘制所有图片
|
||
const imagePromises = matchData.gridImages.map(async (item, index) => {
|
||
const row = Math.floor(index / gridCols);
|
||
const col = index % gridCols;
|
||
|
||
const baseX = gridStartX + col * cellSize + cellSize / 2;
|
||
const baseY = gridStartY + row * cellSize + cellSize / 2;
|
||
|
||
// 加上随机偏移
|
||
const imageX = baseX + item.offsetX;
|
||
const imageY = baseY + item.offsetY;
|
||
|
||
// 加载图片
|
||
let image: any = null;
|
||
try {
|
||
const imagePath = `/mathPages/shared/assets/${item.folder}/${item.imageIndex}.png`;
|
||
image = await getImage(canvas, imagePath);
|
||
} catch (error) {
|
||
console.error(
|
||
`加载图片失败: ${item.folder}/${item.imageIndex}`,
|
||
error,
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (image) {
|
||
// 计算图片高度(等比例缩放)
|
||
// @ts-ignore - 微信小程序图片对象有 width 和 height 属性
|
||
const scaledHeight = (image.height / image.width) * imageSize;
|
||
|
||
// 绘制图片(居中)
|
||
ctx.drawImage(
|
||
image,
|
||
imageX - imageSize / 2,
|
||
imageY - scaledHeight / 2,
|
||
imageSize,
|
||
scaledHeight,
|
||
);
|
||
}
|
||
});
|
||
|
||
await Promise.all(imagePromises);
|
||
|
||
// ========== 绘制虚线分割线 ==========
|
||
const dividerY = gridStartY + gridRows * cellSize + 30;
|
||
this.drawDashedDivider(dividerY, margin);
|
||
|
||
// ========== 绘制下面区域 ==========
|
||
const bottomStartY = dividerY + 30;
|
||
const bottomImageSize = 50; // 下面图片大小
|
||
const bottomBoxSize = bottomImageSize + imagePadding * 2; // 图片框大小
|
||
const bottomSpacing = 40; // 图片间距
|
||
const bottomRowSpacing = 80; // 两行间距
|
||
|
||
// 计算下面区域的总宽度(4个图片框 + 3个间距)
|
||
const bottomTotalWidth = bottomBoxSize * 4 + bottomSpacing * 3;
|
||
const bottomStartX =
|
||
margin + (canvasWidth - margin * 2 - bottomTotalWidth) / 2; // 居中
|
||
|
||
// 第一行:绘制图片框和图片
|
||
for (let i = 0; i < matchData.bottomImages.length; i++) {
|
||
const item = matchData.bottomImages[i];
|
||
const boxX = bottomStartX + i * (bottomBoxSize + bottomSpacing);
|
||
const boxY = bottomStartY;
|
||
const imageX = boxX + imagePadding;
|
||
const imageY = boxY + imagePadding;
|
||
|
||
// 绘制方框(边线宽度2)
|
||
ctx.strokeStyle = '#999';
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(boxX, boxY, bottomBoxSize, bottomBoxSize);
|
||
|
||
// 加载并绘制图片
|
||
let image: any = null;
|
||
try {
|
||
const imagePath = `/mathPages/shared/assets/${item.folder}/${item.imageIndex}.png`;
|
||
image = await getImage(canvas, imagePath);
|
||
} catch (error) {
|
||
console.error(
|
||
`加载图片失败: ${item.folder}/${item.imageIndex}`,
|
||
error,
|
||
);
|
||
continue;
|
||
}
|
||
|
||
if (image) {
|
||
// 计算图片高度(等比例缩放)
|
||
// @ts-ignore - 微信小程序图片对象有 width 和 height 属性
|
||
const scaledHeight =
|
||
(image.height / image.width) * bottomImageSize;
|
||
|
||
// 绘制图片(居中)
|
||
const drawImageX =
|
||
imageX + (bottomImageSize - bottomImageSize) / 2;
|
||
const drawImageY =
|
||
imageY + (bottomImageSize - scaledHeight) / 2;
|
||
ctx.drawImage(
|
||
image,
|
||
drawImageX,
|
||
drawImageY,
|
||
bottomImageSize,
|
||
scaledHeight,
|
||
);
|
||
}
|
||
}
|
||
|
||
// 绘制第一行方框下方的圆点(间隔5)
|
||
const dotRadius = 4; // 圆点半径
|
||
const dotSpacing = 10; // 圆点与方框的间距
|
||
const topDotY = bottomStartY + bottomBoxSize + dotSpacing;
|
||
|
||
for (let i = 0; i < matchData.bottomImages.length; i++) {
|
||
const boxX = bottomStartX + i * (bottomBoxSize + bottomSpacing);
|
||
const dotX = boxX + bottomBoxSize / 2; // 方框正中间
|
||
|
||
// 绘制圆点
|
||
ctx.fillStyle = '#93D333';
|
||
ctx.beginPath();
|
||
ctx.arc(dotX, topDotY, dotRadius, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
|
||
// 第二行:绘制数字输入框(4个方框)
|
||
const numberBoxY = bottomStartY + bottomBoxSize + bottomRowSpacing;
|
||
const numberBoxSize = bottomBoxSize; // 数字框大小和图片框一样
|
||
|
||
// 绘制第二行方框上方的圆点(间隔5)
|
||
const bottomDotY = numberBoxY - dotSpacing;
|
||
|
||
// 先绘制所有圆点
|
||
for (let i = 0; i < 4; i++) {
|
||
const boxX = bottomStartX + i * (numberBoxSize + bottomSpacing);
|
||
const dotX = boxX + numberBoxSize / 2; // 方框正中间
|
||
|
||
// 绘制圆点
|
||
ctx.fillStyle = '#93D333';
|
||
ctx.beginPath();
|
||
ctx.arc(dotX, bottomDotY, dotRadius, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
|
||
// 再绘制方框和数字
|
||
for (let i = 0; i < 4; i++) {
|
||
const boxX = bottomStartX + i * (numberBoxSize + bottomSpacing);
|
||
|
||
// 绘制方框(边线宽度2)
|
||
ctx.strokeStyle = '#999';
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(boxX, numberBoxY, numberBoxSize, numberBoxSize);
|
||
|
||
// 绘制数字(如果存在)
|
||
if (
|
||
matchData.bottomNumbers &&
|
||
matchData.bottomNumbers[i] !== undefined
|
||
) {
|
||
const number = matchData.bottomNumbers[i];
|
||
ctx.fillStyle = '#141414';
|
||
ctx.font = `bold ${48}px "Microsoft Yahei"`;
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText(
|
||
String(number),
|
||
boxX + numberBoxSize / 2,
|
||
numberBoxY + numberBoxSize / 2,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
export default NumberObjectMatchDraw;
|