Files
2026-04-09 17:43:22 +08:00

387 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 数物对应绘制服务
*/
import { BaseDrawService } from '../../../core/draw/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, mode: string = 'match') {
if (!matchData || !matchData.gridImages || !matchData.bottomImages) {
return;
}
// 连线模式需要 bottomNumbers,填写模式不需要
if (mode === 'match' && !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,
mode: mode,
});
}
/**
* 绘制内容区域
*/
private async drawContent(params: {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
matchData: NumberObjectMatchData;
canvasWidth: number;
startY: number;
mode?: string; // 'match' 连线 或 'fill' 填写
}): Promise<void> {
const { canvas, ctx, matchData, canvasWidth, mode = 'match' } = 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);
// ========== 绘制下面区域 ==========
if (mode === 'fill') {
// 填写模式:两行四列网格
await this.drawFillMode({
canvas,
ctx,
matchData,
canvasWidth,
dividerY,
margin,
});
} else {
// 连线模式:保持原有逻辑
await this.drawMatchMode({
canvas,
ctx,
matchData,
canvasWidth,
dividerY,
margin,
});
}
}
/**
* 绘制填写模式
*/
private async drawFillMode(params: {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
matchData: NumberObjectMatchData;
canvasWidth: number;
dividerY: number;
margin: number;
}): Promise<void> {
const { canvas, ctx, matchData, canvasWidth, dividerY, margin } =
params;
const bottomStartY = dividerY + 30;
const bottomImageSize = 75; // 下面图片大小
const imagePadding = 10; // 图片内边距
const bottomBoxSize = bottomImageSize + imagePadding * 2; // 图片框大小
const gridRows = 2; // 两行
const gridCols = 4; // 四列
const cellSize = bottomBoxSize; // 格子大小
// 计算网格总宽度
const gridWidth = cellSize * gridCols;
const gridStartX = margin + (canvasWidth - margin * 2 - gridWidth) / 2; // 居中
// 绘制网格线
this.drawGridLines(
gridStartX,
bottomStartY,
cellSize,
cellSize,
gridCols,
gridRows,
);
// 第一行:绘制图片
for (let i = 0; i < matchData.bottomImages.length; i++) {
const item = matchData.bottomImages[i];
const col = i;
const boxX = gridStartX + col * cellSize;
const boxY = bottomStartY;
const imageX = boxX + imagePadding;
const imageY = boxY + imagePadding;
// 加载并绘制图片
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,
);
}
}
// 第二行:空着,让小朋友填写数字(只绘制网格线,不绘制内容)
}
/**
* 绘制连线模式
*/
private async drawMatchMode(params: {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
matchData: NumberObjectMatchData;
canvasWidth: number;
dividerY: number;
margin: number;
}): Promise<void> {
const { canvas, ctx, matchData, canvasWidth, dividerY, margin } =
params;
const bottomStartY = dividerY + 30;
const bottomImageSize = 50; // 下面图片大小
const imagePadding = 10; // 图片内边距
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;