feat:精简draw服务
This commit is contained in:
@@ -50,20 +50,9 @@ class CodeConnectDraw extends BaseDrawService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.codeData = codeData;
|
this.codeData = codeData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域
|
|
||||||
this.drawDivider();
|
|
||||||
await this.drawContent({
|
await this.drawContent({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
|
|||||||
@@ -41,20 +41,9 @@ class ColorPatternDraw extends BaseDrawService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.gridData = gridData;
|
this.gridData = gridData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域
|
|
||||||
this.drawDivider();
|
|
||||||
this.drawContent({
|
this.drawContent({
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
gridData: this.gridData,
|
gridData: this.gridData,
|
||||||
|
|||||||
@@ -51,20 +51,9 @@ class GridDraw extends BaseDrawService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.gridData = gridData;
|
this.gridData = gridData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域(网格和填充)
|
|
||||||
this.drawDivider();
|
|
||||||
await this.drawGridContent({
|
await this.drawGridContent({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
|
|||||||
@@ -60,20 +60,9 @@ class GridReasoningDraw extends BaseDrawService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.gridData = gridData;
|
this.gridData = gridData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域
|
|
||||||
this.drawDivider();
|
|
||||||
await this.drawContent({
|
await this.drawContent({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
|
|||||||
@@ -83,20 +83,9 @@ class LineRecognitionDraw extends BaseDrawService {
|
|||||||
await this.loadSpiralLineImage();
|
await this.loadSpiralLineImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.lineData = lineData;
|
this.lineData = lineData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域
|
|
||||||
this.drawDivider();
|
|
||||||
await this.drawContent({
|
await this.drawContent({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
|
|||||||
@@ -39,20 +39,9 @@ class MatchConnectDraw extends BaseDrawService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.matchData = matchData;
|
this.matchData = matchData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域
|
|
||||||
this.drawDivider();
|
|
||||||
await this.drawContent({
|
await this.drawContent({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
|
|||||||
@@ -40,20 +40,9 @@ class PositionColoringDraw extends BaseDrawService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.gridData = gridData;
|
this.gridData = gridData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域
|
|
||||||
this.drawDivider();
|
|
||||||
await this.drawContent({
|
await this.drawContent({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
|
|||||||
@@ -45,20 +45,9 @@ class ShapeSymbolDraw extends BaseDrawService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.gridData = gridData;
|
this.gridData = gridData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域
|
|
||||||
this.drawDivider();
|
|
||||||
this.drawContent({
|
this.drawContent({
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
gridData: this.gridData,
|
gridData: this.gridData,
|
||||||
|
|||||||
@@ -1,576 +0,0 @@
|
|||||||
import { getImage } from '../../../utils/index';
|
|
||||||
|
|
||||||
interface DrawAdditionContentParams {
|
|
||||||
canvas: WechatMiniprogram.Canvas;
|
|
||||||
ctx: RenderingContext;
|
|
||||||
problems: Array<{
|
|
||||||
type: 'addition' | 'subtraction';
|
|
||||||
left: number;
|
|
||||||
right: number;
|
|
||||||
result: number;
|
|
||||||
}>;
|
|
||||||
canvasWidth: number;
|
|
||||||
startY: number;
|
|
||||||
imageType?: string; // 'twelve-animals' 或 'fruits'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制圆角矩形(虚线或实线边框)
|
|
||||||
*/
|
|
||||||
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([2, 2]); // 虚线
|
|
||||||
} else {
|
|
||||||
ctx.setLineDash([]); // 实线
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.setLineDash([]); // 重置为实线
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制实线圆角方框(用于填写答案)
|
|
||||||
*/
|
|
||||||
function drawAnswerBox(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
) {
|
|
||||||
const radius = 6; // 圆角大小
|
|
||||||
ctx.strokeStyle = '#000';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.setLineDash([]); // 实线
|
|
||||||
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();
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制右侧答案框
|
|
||||||
*/
|
|
||||||
function drawAnswerBoxArea(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
params: {
|
|
||||||
rightBoxX: number;
|
|
||||||
boxY: number;
|
|
||||||
rightBoxWidth: number;
|
|
||||||
borderRadius: number;
|
|
||||||
boxHeight: number;
|
|
||||||
padding: number;
|
|
||||||
problemType: 'addition' | 'subtraction';
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
rightBoxX,
|
|
||||||
boxY,
|
|
||||||
rightBoxWidth,
|
|
||||||
borderRadius,
|
|
||||||
boxHeight,
|
|
||||||
padding,
|
|
||||||
problemType,
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
const plusSignSize = 18; // 加号大小(缩小)
|
|
||||||
const minusSignSize = 18; // 减号大小(缩小)
|
|
||||||
const equalsSignSize = 18; // 等号大小(缩小)
|
|
||||||
const spacing = 8; // 加号左右间距(方便修改和调试)
|
|
||||||
const answerBoxWidth = 34; // 答案框宽度
|
|
||||||
const answerBoxHeight = 34; // 答案框高度
|
|
||||||
|
|
||||||
// 绘制右侧答案框边框
|
|
||||||
ctx.strokeStyle = '#999';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
drawRoundedRect(
|
|
||||||
ctx,
|
|
||||||
rightBoxX,
|
|
||||||
boxY,
|
|
||||||
rightBoxWidth,
|
|
||||||
boxHeight,
|
|
||||||
borderRadius,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const rightBoxCenterY = boxY + boxHeight / 2;
|
|
||||||
const rightBoxAvailableWidth = rightBoxWidth - padding * 2;
|
|
||||||
const answerStartY = rightBoxCenterY - answerBoxHeight / 2;
|
|
||||||
|
|
||||||
// 根据问题类型确定运算符号大小
|
|
||||||
const operatorSignSize =
|
|
||||||
problemType === 'addition' ? plusSignSize : minusSignSize;
|
|
||||||
|
|
||||||
// 计算总宽度并水平居中(加法和减法逻辑相同)
|
|
||||||
const totalAnswerWidth =
|
|
||||||
answerBoxWidth +
|
|
||||||
spacing +
|
|
||||||
operatorSignSize +
|
|
||||||
spacing +
|
|
||||||
answerBoxWidth +
|
|
||||||
spacing +
|
|
||||||
equalsSignSize +
|
|
||||||
spacing +
|
|
||||||
answerBoxWidth;
|
|
||||||
const answerStartX =
|
|
||||||
rightBoxX + padding + (rightBoxAvailableWidth - totalAnswerWidth) / 2;
|
|
||||||
|
|
||||||
// 计算各个元素的位置
|
|
||||||
const box1X = answerStartX;
|
|
||||||
const operatorX = box1X + answerBoxWidth + spacing;
|
|
||||||
const box2X = operatorX + plusSignSize + spacing;
|
|
||||||
const equalsX = box2X + answerBoxWidth + spacing;
|
|
||||||
const box3X = equalsX + equalsSignSize + spacing;
|
|
||||||
|
|
||||||
// 绘制第一个方框(圆角)
|
|
||||||
drawAnswerBox(ctx, box1X, answerStartY, answerBoxWidth, answerBoxHeight);
|
|
||||||
|
|
||||||
// 绘制运算符号(加号或减号)
|
|
||||||
ctx.strokeStyle = '#000';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.beginPath();
|
|
||||||
if (problemType === 'addition') {
|
|
||||||
// 加号:横线和竖线
|
|
||||||
ctx.moveTo(operatorX, rightBoxCenterY);
|
|
||||||
ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY);
|
|
||||||
ctx.moveTo(
|
|
||||||
operatorX + operatorSignSize / 2,
|
|
||||||
rightBoxCenterY - operatorSignSize / 2,
|
|
||||||
);
|
|
||||||
ctx.lineTo(
|
|
||||||
operatorX + operatorSignSize / 2,
|
|
||||||
rightBoxCenterY + operatorSignSize / 2,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 减号:只有横线
|
|
||||||
ctx.moveTo(operatorX, rightBoxCenterY);
|
|
||||||
ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 绘制第二个方框(圆角)
|
|
||||||
drawAnswerBox(ctx, box2X, answerStartY, answerBoxWidth, answerBoxHeight);
|
|
||||||
|
|
||||||
// 绘制等号(缩短)
|
|
||||||
ctx.strokeStyle = '#000';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(equalsX, rightBoxCenterY - 3);
|
|
||||||
ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY - 3);
|
|
||||||
ctx.moveTo(equalsX, rightBoxCenterY + 3);
|
|
||||||
ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY + 3);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 绘制第三个方框(答案,圆角)
|
|
||||||
drawAnswerBox(ctx, box3X, answerStartY, answerBoxWidth, answerBoxHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据图片类型设置图片大小
|
|
||||||
function getImageSize(imageType: 'twelve-animals' | 'fruits'): number {
|
|
||||||
return imageType === 'twelve-animals' ? 40 : 36;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制加减法计算内容区域
|
|
||||||
* 左侧框:展示图片(加法:加号左右展示图片;减法:总数图片,后几个加划线)
|
|
||||||
* 右侧框:填写运算方式(实线方框 + 实线方框 = 实线方框 或 实线方框 - 实线方框 = 实线方框)
|
|
||||||
*/
|
|
||||||
export async function drawAdditionContent({
|
|
||||||
canvas,
|
|
||||||
ctx,
|
|
||||||
problems,
|
|
||||||
canvasWidth,
|
|
||||||
startY,
|
|
||||||
imageType: _imageType = '', // 不再使用,每个运算会随机选择图片类型
|
|
||||||
}: DrawAdditionContentParams): Promise<void> {
|
|
||||||
// 根据图片类型确定图片目录和最大索引
|
|
||||||
const imageConfig = {
|
|
||||||
'twelve-animals': {
|
|
||||||
folder: 'twelve-animals',
|
|
||||||
maxIndex: 12,
|
|
||||||
},
|
|
||||||
fruits: {
|
|
||||||
folder: 'fruits',
|
|
||||||
maxIndex: 22,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const leftMargin = 24;
|
|
||||||
const rightMargin = 24;
|
|
||||||
const itemSpacing = 140;
|
|
||||||
// 调整左右框宽度比例:6:4 (约420:280)
|
|
||||||
const leftBoxWidth = 334; // 左侧图片框固定宽度
|
|
||||||
const rightBoxWidth = 196; // 右侧答案框固定宽度
|
|
||||||
const boxHeight = 110;
|
|
||||||
const borderRadius = 12;
|
|
||||||
|
|
||||||
const imageSpacing = 0; // 图片之间的间距(水平方向)
|
|
||||||
const rowSpacing = 4; // 第一行和第二行之间的间距(垂直方向)
|
|
||||||
const padding = 15; // 框内边距
|
|
||||||
const startYPos = startY + 20;
|
|
||||||
const plusSignSize = 20; // 加号大小(缩小)
|
|
||||||
const plusSignSpacing = 15; // 加号左右间距(方便修改和调试)
|
|
||||||
|
|
||||||
// 计算左侧和右侧的起始X位置
|
|
||||||
const leftBoxX = leftMargin;
|
|
||||||
const rightBoxX = canvasWidth - rightMargin - rightBoxWidth;
|
|
||||||
|
|
||||||
// 为每个题目随机选择图片类型和图片索引
|
|
||||||
const problemImageConfigs: Array<{
|
|
||||||
type: 'twelve-animals' | 'fruits';
|
|
||||||
imageIndex: number;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < problems.length; i++) {
|
|
||||||
// 随机选择图片类型
|
|
||||||
const imageTypes: ('twelve-animals' | 'fruits')[] = [
|
|
||||||
'twelve-animals',
|
|
||||||
'fruits',
|
|
||||||
];
|
|
||||||
const randomType =
|
|
||||||
imageTypes[Math.floor(Math.random() * imageTypes.length)];
|
|
||||||
const config = imageConfig[randomType];
|
|
||||||
|
|
||||||
// 为当前题目生成唯一的图片索引
|
|
||||||
const availableImageIndices = Array.from(
|
|
||||||
{ length: config.maxIndex },
|
|
||||||
(_, i) => i + 1,
|
|
||||||
);
|
|
||||||
const randomIndex = Math.floor(
|
|
||||||
Math.random() * availableImageIndices.length,
|
|
||||||
);
|
|
||||||
const imageIndex = availableImageIndices[randomIndex];
|
|
||||||
|
|
||||||
problemImageConfigs.push({
|
|
||||||
type: randomType,
|
|
||||||
imageIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制每一行的内容
|
|
||||||
for (let i = 0; i < problems.length; i++) {
|
|
||||||
const problem = problems[i];
|
|
||||||
const boxY = startYPos + i * itemSpacing;
|
|
||||||
const imageConfigItem = problemImageConfigs[i];
|
|
||||||
const config = imageConfig[imageConfigItem.type];
|
|
||||||
|
|
||||||
// 根据图片类型获取图片大小
|
|
||||||
const imageSize = getImageSize(imageConfigItem.type);
|
|
||||||
|
|
||||||
// 加载图片
|
|
||||||
let image: any = null;
|
|
||||||
try {
|
|
||||||
const imagePath = `/mathPages/assets/${config.folder}/${imageConfigItem.imageIndex}.png`;
|
|
||||||
image = await getImage(canvas, imagePath);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('图片加载失败:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 绘制左侧图片框 ==========
|
|
||||||
ctx.strokeStyle = '#999';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
drawRoundedRect(
|
|
||||||
ctx,
|
|
||||||
leftBoxX,
|
|
||||||
boxY,
|
|
||||||
leftBoxWidth,
|
|
||||||
boxHeight,
|
|
||||||
borderRadius,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const leftBoxCenterY = boxY + boxHeight / 2;
|
|
||||||
const leftBoxContentX = leftBoxX + padding;
|
|
||||||
|
|
||||||
if (problem.type === 'addition') {
|
|
||||||
// 加法:左侧图片 + 加号 + 右侧图片
|
|
||||||
const leftImageCount = problem.left;
|
|
||||||
const rightImageCount = problem.right;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据数量计算每行最多展示的图片数
|
|
||||||
*/
|
|
||||||
const getMaxImagesPerRow = (count: number): number => {
|
|
||||||
if (count <= 6) return 3;
|
|
||||||
if (count <= 8) return 4;
|
|
||||||
return 5; // 8-10
|
|
||||||
};
|
|
||||||
|
|
||||||
const leftMaxPerRow = getMaxImagesPerRow(leftImageCount);
|
|
||||||
const rightMaxPerRow = getMaxImagesPerRow(rightImageCount);
|
|
||||||
const leftRows = Math.min(
|
|
||||||
2,
|
|
||||||
Math.ceil(leftImageCount / leftMaxPerRow),
|
|
||||||
);
|
|
||||||
const rightRows = Math.min(
|
|
||||||
2,
|
|
||||||
Math.ceil(rightImageCount / rightMaxPerRow),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 计算左侧图片区域的宽度和高度
|
|
||||||
// 需要根据实际行数计算每行的最大宽度
|
|
||||||
const leftFirstRowCount = Math.min(leftImageCount, leftMaxPerRow);
|
|
||||||
const leftSecondRowCount = Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(leftMaxPerRow, leftImageCount - leftMaxPerRow),
|
|
||||||
);
|
|
||||||
const leftImagesWidth =
|
|
||||||
Math.max(
|
|
||||||
leftFirstRowCount * imageSize +
|
|
||||||
(leftFirstRowCount - 1) * imageSpacing,
|
|
||||||
leftSecondRowCount * imageSize +
|
|
||||||
(leftSecondRowCount - 1) * imageSpacing,
|
|
||||||
) || imageSize; // 至少为 imageSize
|
|
||||||
|
|
||||||
// 计算右侧图片区域的宽度和高度
|
|
||||||
const rightFirstRowCount = Math.min(
|
|
||||||
rightImageCount,
|
|
||||||
rightMaxPerRow,
|
|
||||||
);
|
|
||||||
const rightSecondRowCount = Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(rightMaxPerRow, rightImageCount - rightMaxPerRow),
|
|
||||||
);
|
|
||||||
const rightImagesWidth =
|
|
||||||
Math.max(
|
|
||||||
rightFirstRowCount * imageSize +
|
|
||||||
(rightFirstRowCount - 1) * imageSpacing,
|
|
||||||
rightSecondRowCount * imageSize +
|
|
||||||
(rightSecondRowCount - 1) * imageSpacing,
|
|
||||||
) || imageSize; // 至少为 imageSize
|
|
||||||
|
|
||||||
// 获取图片实际高度(用于垂直居中计算)
|
|
||||||
let imageActualHeight = imageSize;
|
|
||||||
if (image) {
|
|
||||||
// @ts-ignore
|
|
||||||
imageActualHeight = (image.height / image.width) * imageSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算左侧图片区域的实际高度(考虑图片的实际高度和行间距)
|
|
||||||
const leftImagesHeight =
|
|
||||||
leftRows * imageActualHeight + (leftRows - 1) * rowSpacing;
|
|
||||||
// 计算右侧图片区域的实际高度
|
|
||||||
const rightImagesHeight =
|
|
||||||
rightRows * imageActualHeight + (rightRows - 1) * rowSpacing;
|
|
||||||
|
|
||||||
// 计算加号位置(居中,包含左右间距)
|
|
||||||
const totalContentWidth =
|
|
||||||
leftImagesWidth +
|
|
||||||
plusSignSpacing +
|
|
||||||
plusSignSize +
|
|
||||||
plusSignSpacing +
|
|
||||||
rightImagesWidth;
|
|
||||||
const availableWidth = leftBoxWidth - padding * 2;
|
|
||||||
const contentStartX =
|
|
||||||
leftBoxContentX + (availableWidth - totalContentWidth) / 2;
|
|
||||||
|
|
||||||
const leftImagesStartX = contentStartX;
|
|
||||||
// 垂直居中:使用图片实际高度计算
|
|
||||||
const leftImagesStartY = leftBoxCenterY - leftImagesHeight / 2;
|
|
||||||
|
|
||||||
// 绘制左侧图片(最多两行)
|
|
||||||
for (let j = 0; j < Math.min(leftImageCount, 10); j++) {
|
|
||||||
const row = Math.floor(j / leftMaxPerRow);
|
|
||||||
const col = j % leftMaxPerRow;
|
|
||||||
|
|
||||||
// 如果超过两行,停止绘制
|
|
||||||
if (row >= 2) break;
|
|
||||||
|
|
||||||
const imagesInThisRow = Math.min(
|
|
||||||
leftMaxPerRow,
|
|
||||||
leftImageCount - row * leftMaxPerRow,
|
|
||||||
);
|
|
||||||
const rowWidth =
|
|
||||||
imagesInThisRow * imageSize +
|
|
||||||
(imagesInThisRow - 1) * imageSpacing;
|
|
||||||
const rowStartX =
|
|
||||||
leftImagesStartX + (leftImagesWidth - rowWidth) / 2;
|
|
||||||
|
|
||||||
const imageX = rowStartX + col * (imageSize + imageSpacing);
|
|
||||||
// 使用图片实际高度和行间距计算垂直位置
|
|
||||||
const imageY =
|
|
||||||
leftImagesStartY + row * (imageActualHeight + rowSpacing);
|
|
||||||
|
|
||||||
if (image) {
|
|
||||||
// @ts-ignore
|
|
||||||
const scaledHeight =
|
|
||||||
(image.height / image.width) * imageSize;
|
|
||||||
ctx.drawImage(
|
|
||||||
image,
|
|
||||||
imageX,
|
|
||||||
imageY,
|
|
||||||
imageSize,
|
|
||||||
scaledHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制加号(左右有间距)
|
|
||||||
const plusX =
|
|
||||||
leftImagesStartX +
|
|
||||||
leftImagesWidth +
|
|
||||||
plusSignSpacing +
|
|
||||||
plusSignSize / 2;
|
|
||||||
const plusY = leftBoxCenterY;
|
|
||||||
ctx.strokeStyle = '#000';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.beginPath();
|
|
||||||
// 横线
|
|
||||||
ctx.moveTo(plusX - plusSignSize / 2, plusY);
|
|
||||||
ctx.lineTo(plusX + plusSignSize / 2, plusY);
|
|
||||||
// 竖线
|
|
||||||
ctx.moveTo(plusX, plusY - plusSignSize / 2);
|
|
||||||
ctx.lineTo(plusX, plusY + plusSignSize / 2);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
const rightImagesStartX =
|
|
||||||
plusX + plusSignSize / 2 + plusSignSpacing;
|
|
||||||
// 垂直居中:使用图片实际高度计算
|
|
||||||
const rightImagesStartY = leftBoxCenterY - rightImagesHeight / 2;
|
|
||||||
|
|
||||||
// 绘制右侧图片(最多两行)
|
|
||||||
for (let j = 0; j < Math.min(rightImageCount, 10); j++) {
|
|
||||||
const row = Math.floor(j / rightMaxPerRow);
|
|
||||||
const col = j % rightMaxPerRow;
|
|
||||||
|
|
||||||
// 如果超过两行,停止绘制
|
|
||||||
if (row >= 2) break;
|
|
||||||
|
|
||||||
const imagesInThisRow = Math.min(
|
|
||||||
rightMaxPerRow,
|
|
||||||
rightImageCount - row * rightMaxPerRow,
|
|
||||||
);
|
|
||||||
const rowWidth =
|
|
||||||
imagesInThisRow * imageSize +
|
|
||||||
(imagesInThisRow - 1) * imageSpacing;
|
|
||||||
const rowStartX =
|
|
||||||
rightImagesStartX + (rightImagesWidth - rowWidth) / 2;
|
|
||||||
|
|
||||||
const imageX = rowStartX + col * (imageSize + imageSpacing);
|
|
||||||
// 使用图片实际高度和行间距计算垂直位置
|
|
||||||
const imageY =
|
|
||||||
rightImagesStartY + row * (imageActualHeight + rowSpacing);
|
|
||||||
|
|
||||||
if (image) {
|
|
||||||
// @ts-ignore
|
|
||||||
const scaledHeight =
|
|
||||||
(image.height / image.width) * imageSize;
|
|
||||||
ctx.drawImage(
|
|
||||||
image,
|
|
||||||
imageX,
|
|
||||||
imageY,
|
|
||||||
imageSize,
|
|
||||||
scaledHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 减法:展示总数量的图片,后几个加划线
|
|
||||||
const totalCount = problem.left; // 总数
|
|
||||||
const subtractCount = problem.right; // 要减去的数量
|
|
||||||
|
|
||||||
// 获取图片实际高度(用于垂直居中计算)
|
|
||||||
let imageActualHeight = imageSize;
|
|
||||||
if (image) {
|
|
||||||
// @ts-ignore
|
|
||||||
imageActualHeight = (image.height / image.width) * imageSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算图片布局
|
|
||||||
const imagesPerRow = 5;
|
|
||||||
const totalRows = Math.ceil(totalCount / imagesPerRow);
|
|
||||||
// 使用图片实际高度和行间距计算总高度
|
|
||||||
const totalHeight =
|
|
||||||
totalRows * imageActualHeight + (totalRows - 1) * rowSpacing;
|
|
||||||
const imagesStartY = leftBoxCenterY - totalHeight / 2;
|
|
||||||
|
|
||||||
// 绘制所有图片
|
|
||||||
for (let j = 0; j < totalCount; j++) {
|
|
||||||
const row = Math.floor(j / imagesPerRow);
|
|
||||||
const col = j % imagesPerRow;
|
|
||||||
const imagesInThisRow = Math.min(
|
|
||||||
imagesPerRow,
|
|
||||||
totalCount - row * imagesPerRow,
|
|
||||||
);
|
|
||||||
const rowWidth =
|
|
||||||
imagesInThisRow * imageSize +
|
|
||||||
(imagesInThisRow - 1) * imageSpacing;
|
|
||||||
const rowStartX =
|
|
||||||
leftBoxContentX +
|
|
||||||
(leftBoxWidth - padding * 2 - rowWidth) / 2;
|
|
||||||
|
|
||||||
const imageX = rowStartX + col * (imageSize + imageSpacing);
|
|
||||||
// 使用图片实际高度和行间距计算垂直位置
|
|
||||||
const imageY =
|
|
||||||
imagesStartY + row * (imageActualHeight + rowSpacing);
|
|
||||||
|
|
||||||
if (image) {
|
|
||||||
// @ts-ignore
|
|
||||||
const scaledHeight =
|
|
||||||
(image.height / image.width) * imageSize;
|
|
||||||
ctx.drawImage(
|
|
||||||
image,
|
|
||||||
imageX,
|
|
||||||
imageY,
|
|
||||||
imageSize,
|
|
||||||
scaledHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 如果是后几个图片,绘制虚线删除线(表示减去)
|
|
||||||
if (j >= totalCount - subtractCount) {
|
|
||||||
ctx.strokeStyle = '#000';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.setLineDash([4, 3]); // 设置虚线样式
|
|
||||||
ctx.beginPath();
|
|
||||||
// 绘制第一条虚线对角线(从左上到右下)
|
|
||||||
ctx.moveTo(imageX, imageY);
|
|
||||||
ctx.lineTo(imageX + imageSize, imageY + scaledHeight);
|
|
||||||
// // 绘制第二条虚线对角线(从右上到左下)
|
|
||||||
// ctx.moveTo(imageX + imageSize, imageY);
|
|
||||||
// ctx.lineTo(imageX, imageY + scaledHeight);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.setLineDash([]); // 重置为实线,防止影响后续绘制
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 绘制右侧答案框 ==========
|
|
||||||
drawAnswerBoxArea(ctx, {
|
|
||||||
rightBoxX,
|
|
||||||
boxY,
|
|
||||||
rightBoxWidth,
|
|
||||||
borderRadius,
|
|
||||||
boxHeight,
|
|
||||||
padding,
|
|
||||||
problemType: problem.type,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 加减法计算绘制服务
|
||||||
|
*/
|
||||||
|
|
||||||
import { BaseDrawService } from '../../../service/baseDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawAdditionContent } from './additionContentDraw';
|
import { getImage } from '../../../utils/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加减法计算数据
|
||||||
|
*/
|
||||||
|
export interface AdditionData {
|
||||||
|
problems: Array<{
|
||||||
|
type: 'addition' | 'subtraction';
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
result: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加减法计算绘制服务
|
* 加减法计算绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
|
||||||
*/
|
*/
|
||||||
class AdditionDraw extends BaseDrawService {
|
class AdditionDraw extends BaseDrawService {
|
||||||
calculationData: {
|
calculationData: AdditionData | null = null;
|
||||||
problems: Array<{
|
|
||||||
type: 'addition' | 'subtraction';
|
|
||||||
left: number;
|
|
||||||
right: number;
|
|
||||||
result: number;
|
|
||||||
}>;
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
@@ -24,15 +32,11 @@ class AdditionDraw extends BaseDrawService {
|
|||||||
this.calculationData = null;
|
this.calculationData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制加减法计算内容
|
||||||
|
*/
|
||||||
async draw(
|
async draw(
|
||||||
calculationData: {
|
calculationData: AdditionData,
|
||||||
problems: Array<{
|
|
||||||
type: 'addition' | 'subtraction';
|
|
||||||
left: number;
|
|
||||||
right: number;
|
|
||||||
result: number;
|
|
||||||
}>;
|
|
||||||
},
|
|
||||||
_calculationType: string = 'addition-5', // 保留参数以保持接口兼容性
|
_calculationType: string = 'addition-5', // 保留参数以保持接口兼容性
|
||||||
_imageType?: string, // 可选,不再使用,每个运算会随机选择图片类型
|
_imageType?: string, // 可选,不再使用,每个运算会随机选择图片类型
|
||||||
) {
|
) {
|
||||||
@@ -40,21 +44,10 @@ class AdditionDraw extends BaseDrawService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.calculationData = calculationData;
|
this.calculationData = calculationData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
await this.drawContent({
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域(加减法计算)
|
|
||||||
this.drawDivider();
|
|
||||||
await drawAdditionContent({
|
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
problems: this.calculationData.problems,
|
problems: this.calculationData.problems,
|
||||||
@@ -62,6 +55,613 @@ class AdditionDraw extends BaseDrawService {
|
|||||||
startY: this.currentY,
|
startY: this.currentY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制内容区域
|
||||||
|
*/
|
||||||
|
private async drawContent(params: {
|
||||||
|
canvas: WechatMiniprogram.Canvas;
|
||||||
|
ctx: RenderingContext;
|
||||||
|
problems: Array<{
|
||||||
|
type: 'addition' | 'subtraction';
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
result: number;
|
||||||
|
}>;
|
||||||
|
canvasWidth: number;
|
||||||
|
startY: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { canvas, ctx, problems, canvasWidth, startY } = params;
|
||||||
|
|
||||||
|
// 根据图片类型确定图片目录和最大索引
|
||||||
|
const imageConfig = {
|
||||||
|
'twelve-animals': {
|
||||||
|
folder: 'twelve-animals',
|
||||||
|
maxIndex: 12,
|
||||||
|
},
|
||||||
|
fruits: {
|
||||||
|
folder: 'fruits',
|
||||||
|
maxIndex: 22,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const leftMargin = 24;
|
||||||
|
const rightMargin = 24;
|
||||||
|
const itemSpacing = 140;
|
||||||
|
// 调整左右框宽度比例:6:4 (约420:280)
|
||||||
|
const leftBoxWidth = 334; // 左侧图片框固定宽度
|
||||||
|
const rightBoxWidth = 196; // 右侧答案框固定宽度
|
||||||
|
const boxHeight = 110;
|
||||||
|
const borderRadius = 12;
|
||||||
|
|
||||||
|
const imageSpacing = 0; // 图片之间的间距(水平方向)
|
||||||
|
const rowSpacing = 4; // 第一行和第二行之间的间距(垂直方向)
|
||||||
|
const padding = 15; // 框内边距
|
||||||
|
const startYPos = startY + 20;
|
||||||
|
const plusSignSize = 20; // 加号大小(缩小)
|
||||||
|
const plusSignSpacing = 15; // 加号左右间距(方便修改和调试)
|
||||||
|
|
||||||
|
// 计算左侧和右侧的起始X位置
|
||||||
|
const leftBoxX = leftMargin;
|
||||||
|
const rightBoxX = canvasWidth - rightMargin - rightBoxWidth;
|
||||||
|
|
||||||
|
// 为每个题目随机选择图片类型和图片索引
|
||||||
|
const problemImageConfigs: Array<{
|
||||||
|
type: 'twelve-animals' | 'fruits';
|
||||||
|
imageIndex: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < problems.length; i++) {
|
||||||
|
// 随机选择图片类型
|
||||||
|
const imageTypes: ('twelve-animals' | 'fruits')[] = [
|
||||||
|
'twelve-animals',
|
||||||
|
'fruits',
|
||||||
|
];
|
||||||
|
const randomType =
|
||||||
|
imageTypes[Math.floor(Math.random() * imageTypes.length)];
|
||||||
|
const config = imageConfig[randomType];
|
||||||
|
|
||||||
|
// 为当前题目生成唯一的图片索引
|
||||||
|
const availableImageIndices = Array.from(
|
||||||
|
{ length: config.maxIndex },
|
||||||
|
(_, i) => i + 1,
|
||||||
|
);
|
||||||
|
const randomIndex = Math.floor(
|
||||||
|
Math.random() * availableImageIndices.length,
|
||||||
|
);
|
||||||
|
const imageIndex = availableImageIndices[randomIndex];
|
||||||
|
|
||||||
|
problemImageConfigs.push({
|
||||||
|
type: randomType,
|
||||||
|
imageIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制每一行的内容
|
||||||
|
for (let i = 0; i < problems.length; i++) {
|
||||||
|
const problem = problems[i];
|
||||||
|
const boxY = startYPos + i * itemSpacing;
|
||||||
|
const imageConfigItem = problemImageConfigs[i];
|
||||||
|
const config = imageConfig[imageConfigItem.type];
|
||||||
|
|
||||||
|
// 根据图片类型获取图片大小
|
||||||
|
const imageSize = this.getImageSize(imageConfigItem.type);
|
||||||
|
|
||||||
|
// 加载图片
|
||||||
|
let image: any = null;
|
||||||
|
try {
|
||||||
|
const imagePath = `/mathPages/assets/${config.folder}/${imageConfigItem.imageIndex}.png`;
|
||||||
|
image = await getImage(canvas, imagePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('图片加载失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 绘制左侧图片框 ==========
|
||||||
|
ctx.strokeStyle = '#999';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
this.drawRoundedRectInternal(
|
||||||
|
ctx,
|
||||||
|
leftBoxX,
|
||||||
|
boxY,
|
||||||
|
leftBoxWidth,
|
||||||
|
boxHeight,
|
||||||
|
borderRadius,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const leftBoxCenterY = boxY + boxHeight / 2;
|
||||||
|
const leftBoxContentX = leftBoxX + padding;
|
||||||
|
|
||||||
|
if (problem.type === 'addition') {
|
||||||
|
// 加法:左侧图片 + 加号 + 右侧图片
|
||||||
|
const leftImageCount = problem.left;
|
||||||
|
const rightImageCount = problem.right;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据数量计算每行最多展示的图片数
|
||||||
|
*/
|
||||||
|
const getMaxImagesPerRow = (count: number): number => {
|
||||||
|
if (count <= 6) return 3;
|
||||||
|
if (count <= 8) return 4;
|
||||||
|
return 5; // 8-10
|
||||||
|
};
|
||||||
|
|
||||||
|
const leftMaxPerRow = getMaxImagesPerRow(leftImageCount);
|
||||||
|
const rightMaxPerRow = getMaxImagesPerRow(rightImageCount);
|
||||||
|
const leftRows = Math.min(
|
||||||
|
2,
|
||||||
|
Math.ceil(leftImageCount / leftMaxPerRow),
|
||||||
|
);
|
||||||
|
const rightRows = Math.min(
|
||||||
|
2,
|
||||||
|
Math.ceil(rightImageCount / rightMaxPerRow),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 计算左侧图片区域的宽度和高度
|
||||||
|
// 需要根据实际行数计算每行的最大宽度
|
||||||
|
const leftFirstRowCount = Math.min(
|
||||||
|
leftImageCount,
|
||||||
|
leftMaxPerRow,
|
||||||
|
);
|
||||||
|
const leftSecondRowCount = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(leftMaxPerRow, leftImageCount - leftMaxPerRow),
|
||||||
|
);
|
||||||
|
const leftImagesWidth =
|
||||||
|
Math.max(
|
||||||
|
leftFirstRowCount * imageSize +
|
||||||
|
(leftFirstRowCount - 1) * imageSpacing,
|
||||||
|
leftSecondRowCount * imageSize +
|
||||||
|
(leftSecondRowCount - 1) * imageSpacing,
|
||||||
|
) || imageSize; // 至少为 imageSize
|
||||||
|
|
||||||
|
// 计算右侧图片区域的宽度和高度
|
||||||
|
const rightFirstRowCount = Math.min(
|
||||||
|
rightImageCount,
|
||||||
|
rightMaxPerRow,
|
||||||
|
);
|
||||||
|
const rightSecondRowCount = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(rightMaxPerRow, rightImageCount - rightMaxPerRow),
|
||||||
|
);
|
||||||
|
const rightImagesWidth =
|
||||||
|
Math.max(
|
||||||
|
rightFirstRowCount * imageSize +
|
||||||
|
(rightFirstRowCount - 1) * imageSpacing,
|
||||||
|
rightSecondRowCount * imageSize +
|
||||||
|
(rightSecondRowCount - 1) * imageSpacing,
|
||||||
|
) || imageSize; // 至少为 imageSize
|
||||||
|
|
||||||
|
// 获取图片实际高度(用于垂直居中计算)
|
||||||
|
let imageActualHeight = imageSize;
|
||||||
|
if (image) {
|
||||||
|
// @ts-ignore
|
||||||
|
imageActualHeight =
|
||||||
|
(image.height / image.width) * imageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算左侧图片区域的实际高度(考虑图片的实际高度和行间距)
|
||||||
|
const leftImagesHeight =
|
||||||
|
leftRows * imageActualHeight + (leftRows - 1) * rowSpacing;
|
||||||
|
// 计算右侧图片区域的实际高度
|
||||||
|
const rightImagesHeight =
|
||||||
|
rightRows * imageActualHeight +
|
||||||
|
(rightRows - 1) * rowSpacing;
|
||||||
|
|
||||||
|
// 计算加号位置(居中,包含左右间距)
|
||||||
|
const totalContentWidth =
|
||||||
|
leftImagesWidth +
|
||||||
|
plusSignSpacing +
|
||||||
|
plusSignSize +
|
||||||
|
plusSignSpacing +
|
||||||
|
rightImagesWidth;
|
||||||
|
const availableWidth = leftBoxWidth - padding * 2;
|
||||||
|
const contentStartX =
|
||||||
|
leftBoxContentX + (availableWidth - totalContentWidth) / 2;
|
||||||
|
|
||||||
|
const leftImagesStartX = contentStartX;
|
||||||
|
// 垂直居中:使用图片实际高度计算
|
||||||
|
const leftImagesStartY = leftBoxCenterY - leftImagesHeight / 2;
|
||||||
|
|
||||||
|
// 绘制左侧图片(最多两行)
|
||||||
|
for (let j = 0; j < Math.min(leftImageCount, 10); j++) {
|
||||||
|
const row = Math.floor(j / leftMaxPerRow);
|
||||||
|
const col = j % leftMaxPerRow;
|
||||||
|
|
||||||
|
// 如果超过两行,停止绘制
|
||||||
|
if (row >= 2) break;
|
||||||
|
|
||||||
|
const imagesInThisRow = Math.min(
|
||||||
|
leftMaxPerRow,
|
||||||
|
leftImageCount - row * leftMaxPerRow,
|
||||||
|
);
|
||||||
|
const rowWidth =
|
||||||
|
imagesInThisRow * imageSize +
|
||||||
|
(imagesInThisRow - 1) * imageSpacing;
|
||||||
|
const rowStartX =
|
||||||
|
leftImagesStartX + (leftImagesWidth - rowWidth) / 2;
|
||||||
|
|
||||||
|
const imageX = rowStartX + col * (imageSize + imageSpacing);
|
||||||
|
// 使用图片实际高度和行间距计算垂直位置
|
||||||
|
const imageY =
|
||||||
|
leftImagesStartY +
|
||||||
|
row * (imageActualHeight + rowSpacing);
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
// @ts-ignore
|
||||||
|
const scaledHeight =
|
||||||
|
(image.height / image.width) * imageSize;
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
imageX,
|
||||||
|
imageY,
|
||||||
|
imageSize,
|
||||||
|
scaledHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制加号(左右有间距)
|
||||||
|
const plusX =
|
||||||
|
leftImagesStartX +
|
||||||
|
leftImagesWidth +
|
||||||
|
plusSignSpacing +
|
||||||
|
plusSignSize / 2;
|
||||||
|
const plusY = leftBoxCenterY;
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
// 横线
|
||||||
|
ctx.moveTo(plusX - plusSignSize / 2, plusY);
|
||||||
|
ctx.lineTo(plusX + plusSignSize / 2, plusY);
|
||||||
|
// 竖线
|
||||||
|
ctx.moveTo(plusX, plusY - plusSignSize / 2);
|
||||||
|
ctx.lineTo(plusX, plusY + plusSignSize / 2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const rightImagesStartX =
|
||||||
|
plusX + plusSignSize / 2 + plusSignSpacing;
|
||||||
|
// 垂直居中:使用图片实际高度计算
|
||||||
|
const rightImagesStartY =
|
||||||
|
leftBoxCenterY - rightImagesHeight / 2;
|
||||||
|
|
||||||
|
// 绘制右侧图片(最多两行)
|
||||||
|
for (let j = 0; j < Math.min(rightImageCount, 10); j++) {
|
||||||
|
const row = Math.floor(j / rightMaxPerRow);
|
||||||
|
const col = j % rightMaxPerRow;
|
||||||
|
|
||||||
|
// 如果超过两行,停止绘制
|
||||||
|
if (row >= 2) break;
|
||||||
|
|
||||||
|
const imagesInThisRow = Math.min(
|
||||||
|
rightMaxPerRow,
|
||||||
|
rightImageCount - row * rightMaxPerRow,
|
||||||
|
);
|
||||||
|
const rowWidth =
|
||||||
|
imagesInThisRow * imageSize +
|
||||||
|
(imagesInThisRow - 1) * imageSpacing;
|
||||||
|
const rowStartX =
|
||||||
|
rightImagesStartX + (rightImagesWidth - rowWidth) / 2;
|
||||||
|
|
||||||
|
const imageX = rowStartX + col * (imageSize + imageSpacing);
|
||||||
|
// 使用图片实际高度和行间距计算垂直位置
|
||||||
|
const imageY =
|
||||||
|
rightImagesStartY +
|
||||||
|
row * (imageActualHeight + rowSpacing);
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
// @ts-ignore
|
||||||
|
const scaledHeight =
|
||||||
|
(image.height / image.width) * imageSize;
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
imageX,
|
||||||
|
imageY,
|
||||||
|
imageSize,
|
||||||
|
scaledHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 减法:展示总数量的图片,后几个加划线
|
||||||
|
const totalCount = problem.left; // 总数
|
||||||
|
const subtractCount = problem.right; // 要减去的数量
|
||||||
|
|
||||||
|
// 获取图片实际高度(用于垂直居中计算)
|
||||||
|
let imageActualHeight = imageSize;
|
||||||
|
if (image) {
|
||||||
|
// @ts-ignore
|
||||||
|
imageActualHeight =
|
||||||
|
(image.height / image.width) * imageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算图片布局
|
||||||
|
const imagesPerRow = 5;
|
||||||
|
const totalRows = Math.ceil(totalCount / imagesPerRow);
|
||||||
|
// 使用图片实际高度和行间距计算总高度
|
||||||
|
const totalHeight =
|
||||||
|
totalRows * imageActualHeight +
|
||||||
|
(totalRows - 1) * rowSpacing;
|
||||||
|
const imagesStartY = leftBoxCenterY - totalHeight / 2;
|
||||||
|
|
||||||
|
// 绘制所有图片
|
||||||
|
for (let j = 0; j < totalCount; j++) {
|
||||||
|
const row = Math.floor(j / imagesPerRow);
|
||||||
|
const col = j % imagesPerRow;
|
||||||
|
const imagesInThisRow = Math.min(
|
||||||
|
imagesPerRow,
|
||||||
|
totalCount - row * imagesPerRow,
|
||||||
|
);
|
||||||
|
const rowWidth =
|
||||||
|
imagesInThisRow * imageSize +
|
||||||
|
(imagesInThisRow - 1) * imageSpacing;
|
||||||
|
const rowStartX =
|
||||||
|
leftBoxContentX +
|
||||||
|
(leftBoxWidth - padding * 2 - rowWidth) / 2;
|
||||||
|
|
||||||
|
const imageX = rowStartX + col * (imageSize + imageSpacing);
|
||||||
|
// 使用图片实际高度和行间距计算垂直位置
|
||||||
|
const imageY =
|
||||||
|
imagesStartY + row * (imageActualHeight + rowSpacing);
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
// @ts-ignore
|
||||||
|
const scaledHeight =
|
||||||
|
(image.height / image.width) * imageSize;
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
imageX,
|
||||||
|
imageY,
|
||||||
|
imageSize,
|
||||||
|
scaledHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果是后几个图片,绘制虚线删除线(表示减去)
|
||||||
|
if (j >= totalCount - subtractCount) {
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.setLineDash([4, 3]); // 设置虚线样式
|
||||||
|
ctx.beginPath();
|
||||||
|
// 绘制第一条虚线对角线(从左上到右下)
|
||||||
|
ctx.moveTo(imageX, imageY);
|
||||||
|
ctx.lineTo(
|
||||||
|
imageX + imageSize,
|
||||||
|
imageY + scaledHeight,
|
||||||
|
);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]); // 重置为实线,防止影响后续绘制
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 绘制右侧答案框 ==========
|
||||||
|
this.drawAnswerBoxArea(ctx, {
|
||||||
|
rightBoxX,
|
||||||
|
boxY,
|
||||||
|
rightBoxWidth,
|
||||||
|
borderRadius,
|
||||||
|
boxHeight,
|
||||||
|
padding,
|
||||||
|
problemType: problem.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制圆角矩形(虚线或实线边框)
|
||||||
|
*/
|
||||||
|
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([2, 2]); // 虚线
|
||||||
|
} else {
|
||||||
|
ctx.setLineDash([]); // 实线
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]); // 重置为实线
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制实线圆角方框(用于填写答案)
|
||||||
|
*/
|
||||||
|
private drawAnswerBox(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
) {
|
||||||
|
const radius = 6; // 圆角大小
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([]); // 实线
|
||||||
|
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();
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制右侧答案框
|
||||||
|
*/
|
||||||
|
private drawAnswerBoxArea(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
params: {
|
||||||
|
rightBoxX: number;
|
||||||
|
boxY: number;
|
||||||
|
rightBoxWidth: number;
|
||||||
|
borderRadius: number;
|
||||||
|
boxHeight: number;
|
||||||
|
padding: number;
|
||||||
|
problemType: 'addition' | 'subtraction';
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
rightBoxX,
|
||||||
|
boxY,
|
||||||
|
rightBoxWidth,
|
||||||
|
borderRadius,
|
||||||
|
boxHeight,
|
||||||
|
padding,
|
||||||
|
problemType,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const plusSignSize = 18; // 加号大小(缩小)
|
||||||
|
const minusSignSize = 18; // 减号大小(缩小)
|
||||||
|
const equalsSignSize = 18; // 等号大小(缩小)
|
||||||
|
const spacing = 8; // 加号左右间距(方便修改和调试)
|
||||||
|
const answerBoxWidth = 34; // 答案框宽度
|
||||||
|
const answerBoxHeight = 34; // 答案框高度
|
||||||
|
|
||||||
|
// 绘制右侧答案框边框
|
||||||
|
ctx.strokeStyle = '#999';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
this.drawRoundedRectInternal(
|
||||||
|
ctx,
|
||||||
|
rightBoxX,
|
||||||
|
boxY,
|
||||||
|
rightBoxWidth,
|
||||||
|
boxHeight,
|
||||||
|
borderRadius,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rightBoxCenterY = boxY + boxHeight / 2;
|
||||||
|
const rightBoxAvailableWidth = rightBoxWidth - padding * 2;
|
||||||
|
const answerStartY = rightBoxCenterY - answerBoxHeight / 2;
|
||||||
|
|
||||||
|
// 根据问题类型确定运算符号大小
|
||||||
|
const operatorSignSize =
|
||||||
|
problemType === 'addition' ? plusSignSize : minusSignSize;
|
||||||
|
|
||||||
|
// 计算总宽度并水平居中(加法和减法逻辑相同)
|
||||||
|
const totalAnswerWidth =
|
||||||
|
answerBoxWidth +
|
||||||
|
spacing +
|
||||||
|
operatorSignSize +
|
||||||
|
spacing +
|
||||||
|
answerBoxWidth +
|
||||||
|
spacing +
|
||||||
|
equalsSignSize +
|
||||||
|
spacing +
|
||||||
|
answerBoxWidth;
|
||||||
|
const answerStartX =
|
||||||
|
rightBoxX +
|
||||||
|
padding +
|
||||||
|
(rightBoxAvailableWidth - totalAnswerWidth) / 2;
|
||||||
|
|
||||||
|
// 计算各个元素的位置
|
||||||
|
const box1X = answerStartX;
|
||||||
|
const operatorX = box1X + answerBoxWidth + spacing;
|
||||||
|
const box2X = operatorX + plusSignSize + spacing;
|
||||||
|
const equalsX = box2X + answerBoxWidth + spacing;
|
||||||
|
const box3X = equalsX + equalsSignSize + spacing;
|
||||||
|
|
||||||
|
// 绘制第一个方框(圆角)
|
||||||
|
this.drawAnswerBox(
|
||||||
|
ctx,
|
||||||
|
box1X,
|
||||||
|
answerStartY,
|
||||||
|
answerBoxWidth,
|
||||||
|
answerBoxHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 绘制运算符号(加号或减号)
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
if (problemType === 'addition') {
|
||||||
|
// 加号:横线和竖线
|
||||||
|
ctx.moveTo(operatorX, rightBoxCenterY);
|
||||||
|
ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY);
|
||||||
|
ctx.moveTo(
|
||||||
|
operatorX + operatorSignSize / 2,
|
||||||
|
rightBoxCenterY - operatorSignSize / 2,
|
||||||
|
);
|
||||||
|
ctx.lineTo(
|
||||||
|
operatorX + operatorSignSize / 2,
|
||||||
|
rightBoxCenterY + operatorSignSize / 2,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 减号:只有横线
|
||||||
|
ctx.moveTo(operatorX, rightBoxCenterY);
|
||||||
|
ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 绘制第二个方框(圆角)
|
||||||
|
this.drawAnswerBox(
|
||||||
|
ctx,
|
||||||
|
box2X,
|
||||||
|
answerStartY,
|
||||||
|
answerBoxWidth,
|
||||||
|
answerBoxHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 绘制等号(缩短)
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(equalsX, rightBoxCenterY - 3);
|
||||||
|
ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY - 3);
|
||||||
|
ctx.moveTo(equalsX, rightBoxCenterY + 3);
|
||||||
|
ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY + 3);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 绘制第三个方框(答案,圆角)
|
||||||
|
this.drawAnswerBox(
|
||||||
|
ctx,
|
||||||
|
box3X,
|
||||||
|
answerStartY,
|
||||||
|
answerBoxWidth,
|
||||||
|
answerBoxHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图片类型设置图片大小
|
||||||
|
*/
|
||||||
|
private getImageSize(imageType: 'twelve-animals' | 'fruits'): number {
|
||||||
|
return imageType === 'twelve-animals' ? 40 : 36;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdditionDraw;
|
export default AdditionDraw;
|
||||||
|
|||||||
@@ -1,344 +0,0 @@
|
|||||||
import { getImage } from '../../../utils/index';
|
|
||||||
|
|
||||||
interface DrawCompareContentParams {
|
|
||||||
canvas: WechatMiniprogram.Canvas;
|
|
||||||
ctx: RenderingContext;
|
|
||||||
compareData: {
|
|
||||||
problems: Array<{
|
|
||||||
leftCount: number;
|
|
||||||
rightCount: number;
|
|
||||||
leftImageIndex: number;
|
|
||||||
rightImageIndex: number;
|
|
||||||
imageType: 'fruits' | 'twelve-animals';
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
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([]); // 重置为实线
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制数一数比大小内容区域
|
|
||||||
* 两列布局,每列左右两个虚线框,中间圆圈
|
|
||||||
*/
|
|
||||||
export async function drawCompareContent({
|
|
||||||
canvas,
|
|
||||||
ctx,
|
|
||||||
compareData,
|
|
||||||
canvasWidth,
|
|
||||||
startY,
|
|
||||||
}: DrawCompareContentParams): Promise<void> {
|
|
||||||
const { problems } = compareData;
|
|
||||||
|
|
||||||
// 布局参数
|
|
||||||
const leftMargin = 30;
|
|
||||||
const rightMargin = 30;
|
|
||||||
const topMargin = 12;
|
|
||||||
const rowSpacing = 116; // 行之间的间距
|
|
||||||
const columnSpacing = 40; // 两列之间的间距(可调节)
|
|
||||||
|
|
||||||
// 计算每列的宽度(减去边距和列间距)
|
|
||||||
const availableWidth =
|
|
||||||
canvasWidth - leftMargin - rightMargin - columnSpacing;
|
|
||||||
const columnWidth = availableWidth / 2;
|
|
||||||
|
|
||||||
// 框的尺寸
|
|
||||||
const boxWidth = 100;
|
|
||||||
const boxHeight = 100;
|
|
||||||
const borderRadius = 12;
|
|
||||||
const circleRadius = 18; // 圆圈半径
|
|
||||||
|
|
||||||
// 框与圆圈之间的间距
|
|
||||||
const boxToCircleSpacing = 12;
|
|
||||||
|
|
||||||
// 计算左侧列和右侧列的起始X位置
|
|
||||||
const leftColumnStartX = leftMargin;
|
|
||||||
const rightColumnStartX = leftMargin + columnWidth + columnSpacing;
|
|
||||||
|
|
||||||
// 计算每列内部的布局(左框 | 圆圈 | 右框,居中)
|
|
||||||
const totalWidthPerColumn =
|
|
||||||
boxWidth +
|
|
||||||
boxToCircleSpacing +
|
|
||||||
circleRadius * 2 +
|
|
||||||
boxToCircleSpacing +
|
|
||||||
boxWidth;
|
|
||||||
const columnPadding = (columnWidth - totalWidthPerColumn) / 2;
|
|
||||||
|
|
||||||
let currentY = startY + topMargin;
|
|
||||||
|
|
||||||
// 计算分割线位置(两列中间)
|
|
||||||
// const dividerX = leftMargin + columnWidth + columnSpacing / 2;
|
|
||||||
|
|
||||||
// 固定绘制6行
|
|
||||||
const totalRows = 6;
|
|
||||||
for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
|
|
||||||
const rowY = currentY + rowIndex * rowSpacing;
|
|
||||||
// 绘制左侧列
|
|
||||||
const leftProblemIndex = rowIndex * 2;
|
|
||||||
if (leftProblemIndex < problems.length) {
|
|
||||||
await drawCompareProblem(
|
|
||||||
ctx,
|
|
||||||
canvas,
|
|
||||||
problems[leftProblemIndex],
|
|
||||||
leftColumnStartX + columnPadding,
|
|
||||||
rowY,
|
|
||||||
boxWidth,
|
|
||||||
boxHeight,
|
|
||||||
borderRadius,
|
|
||||||
circleRadius,
|
|
||||||
boxToCircleSpacing,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制右侧列
|
|
||||||
const rightProblemIndex = rowIndex * 2 + 1;
|
|
||||||
if (rightProblemIndex < problems.length) {
|
|
||||||
await drawCompareProblem(
|
|
||||||
ctx,
|
|
||||||
canvas,
|
|
||||||
problems[rightProblemIndex],
|
|
||||||
rightColumnStartX + columnPadding,
|
|
||||||
rowY,
|
|
||||||
boxWidth,
|
|
||||||
boxHeight,
|
|
||||||
borderRadius,
|
|
||||||
circleRadius,
|
|
||||||
boxToCircleSpacing,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制中间分割线(贯穿所有行)
|
|
||||||
// if (totalRows > 0) {
|
|
||||||
// ctx.strokeStyle = '#999';
|
|
||||||
// ctx.lineWidth = 1;
|
|
||||||
// ctx.beginPath();
|
|
||||||
// const dividerStartY = currentY;
|
|
||||||
// const dividerEndY = currentY + (totalRows - 1) * rowSpacing + boxHeight;
|
|
||||||
// ctx.moveTo(dividerX, dividerStartY);
|
|
||||||
// ctx.lineTo(dividerX, dividerEndY);
|
|
||||||
// ctx.stroke();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制一个比较问题(左框 | 圆圈 | 右框)
|
|
||||||
*/
|
|
||||||
async function drawCompareProblem(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
canvas: WechatMiniprogram.Canvas,
|
|
||||||
problem: {
|
|
||||||
leftCount: number;
|
|
||||||
rightCount: number;
|
|
||||||
leftImageIndex: number;
|
|
||||||
rightImageIndex: number;
|
|
||||||
imageType: 'fruits' | 'twelve-animals';
|
|
||||||
},
|
|
||||||
startX: number,
|
|
||||||
startY: number,
|
|
||||||
boxWidth: number,
|
|
||||||
boxHeight: number,
|
|
||||||
borderRadius: number,
|
|
||||||
circleRadius: number,
|
|
||||||
boxToCircleSpacing: number,
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
leftCount,
|
|
||||||
rightCount,
|
|
||||||
leftImageIndex,
|
|
||||||
rightImageIndex,
|
|
||||||
imageType,
|
|
||||||
} = problem;
|
|
||||||
|
|
||||||
// 图片配置
|
|
||||||
const imageConfig = {
|
|
||||||
'twelve-animals': {
|
|
||||||
folder: 'twelve-animals',
|
|
||||||
maxIndex: 12,
|
|
||||||
},
|
|
||||||
fruits: {
|
|
||||||
folder: 'fruits',
|
|
||||||
maxIndex: 22,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const config = imageConfig[imageType] || imageConfig['twelve-animals'];
|
|
||||||
|
|
||||||
// 计算左框位置
|
|
||||||
const leftBoxX = startX;
|
|
||||||
const leftBoxY = startY;
|
|
||||||
|
|
||||||
// 计算圆圈位置
|
|
||||||
const circleX = leftBoxX + boxWidth + boxToCircleSpacing + circleRadius;
|
|
||||||
const circleY = startY + boxHeight / 2;
|
|
||||||
|
|
||||||
// 计算右框位置
|
|
||||||
const rightBoxX = circleX + circleRadius + boxToCircleSpacing;
|
|
||||||
const rightBoxY = startY;
|
|
||||||
|
|
||||||
// 绘制左框
|
|
||||||
ctx.strokeStyle = '#999';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
drawRoundedRect(
|
|
||||||
ctx,
|
|
||||||
leftBoxX,
|
|
||||||
leftBoxY,
|
|
||||||
boxWidth,
|
|
||||||
boxHeight,
|
|
||||||
borderRadius,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 绘制左框内的图片
|
|
||||||
await drawImagesInBox(
|
|
||||||
ctx,
|
|
||||||
canvas,
|
|
||||||
leftCount,
|
|
||||||
leftImageIndex,
|
|
||||||
config.folder,
|
|
||||||
leftBoxX,
|
|
||||||
leftBoxY,
|
|
||||||
boxWidth,
|
|
||||||
boxHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 绘制实心圆圈
|
|
||||||
ctx.fillStyle = '#fff';
|
|
||||||
ctx.strokeStyle = '#333';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(circleX, circleY, circleRadius, 0, Math.PI * 2);
|
|
||||||
ctx.fill(); // 先填充白色
|
|
||||||
ctx.stroke(); // 再绘制边框
|
|
||||||
|
|
||||||
// 绘制右框
|
|
||||||
ctx.strokeStyle = '#999';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
drawRoundedRect(
|
|
||||||
ctx,
|
|
||||||
rightBoxX,
|
|
||||||
rightBoxY,
|
|
||||||
boxWidth,
|
|
||||||
boxHeight,
|
|
||||||
borderRadius,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 绘制右框内的图片
|
|
||||||
await drawImagesInBox(
|
|
||||||
ctx,
|
|
||||||
canvas,
|
|
||||||
rightCount,
|
|
||||||
rightImageIndex,
|
|
||||||
config.folder,
|
|
||||||
rightBoxX,
|
|
||||||
rightBoxY,
|
|
||||||
boxWidth,
|
|
||||||
boxHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在框内绘制多张图片
|
|
||||||
*/
|
|
||||||
async function drawImagesInBox(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
canvas: WechatMiniprogram.Canvas,
|
|
||||||
count: number,
|
|
||||||
imageIndex: number,
|
|
||||||
folder: string,
|
|
||||||
boxX: number,
|
|
||||||
boxY: number,
|
|
||||||
boxWidth: number,
|
|
||||||
boxHeight: number,
|
|
||||||
) {
|
|
||||||
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/${folder}/${imageIndex}.png`;
|
|
||||||
boxImage = await getImage(canvas, imagePath);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`加载${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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 数一数比大小绘制服务
|
||||||
|
*/
|
||||||
|
|
||||||
import { BaseDrawService } from '../../../service/baseDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawCompareContent } from './compareContentDraw';
|
import { getImage } from '../../../utils/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数一数比大小数据
|
||||||
|
*/
|
||||||
|
export interface CompareData {
|
||||||
|
problems: Array<{
|
||||||
|
leftCount: number;
|
||||||
|
rightCount: number;
|
||||||
|
leftImageIndex: number;
|
||||||
|
rightImageIndex: number;
|
||||||
|
imageType: 'fruits' | 'twelve-animals';
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数一数比大小绘制服务
|
* 数一数比大小绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
|
||||||
*/
|
*/
|
||||||
class CompareDraw extends BaseDrawService {
|
class CompareDraw extends BaseDrawService {
|
||||||
compareData: {
|
compareData: CompareData | null = null;
|
||||||
problems: Array<{
|
|
||||||
leftCount: number;
|
|
||||||
rightCount: number;
|
|
||||||
leftImageIndex: number;
|
|
||||||
rightImageIndex: number;
|
|
||||||
imageType: 'fruits' | 'twelve-animals';
|
|
||||||
}>;
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
@@ -25,34 +33,18 @@ class CompareDraw extends BaseDrawService {
|
|||||||
this.compareData = null;
|
this.compareData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async draw(compareData: {
|
/**
|
||||||
problems: Array<{
|
* 绘制数一数比大小内容
|
||||||
leftCount: number;
|
*/
|
||||||
rightCount: number;
|
async draw(compareData: CompareData) {
|
||||||
leftImageIndex: number;
|
|
||||||
rightImageIndex: number;
|
|
||||||
imageType: 'fruits' | 'twelve-animals';
|
|
||||||
}>;
|
|
||||||
}) {
|
|
||||||
if (!compareData || !compareData.problems) {
|
if (!compareData || !compareData.problems) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.compareData = compareData;
|
this.compareData = compareData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
await this.drawContent({
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域(数一数比大小)
|
|
||||||
this.drawDivider();
|
|
||||||
await drawCompareContent({
|
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
compareData: this.compareData,
|
compareData: this.compareData,
|
||||||
@@ -60,6 +52,334 @@ class CompareDraw extends BaseDrawService {
|
|||||||
startY: this.currentY,
|
startY: this.currentY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制内容区域
|
||||||
|
*/
|
||||||
|
private async drawContent(params: {
|
||||||
|
canvas: WechatMiniprogram.Canvas;
|
||||||
|
ctx: RenderingContext;
|
||||||
|
compareData: CompareData;
|
||||||
|
canvasWidth: number;
|
||||||
|
startY: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { canvas, ctx, compareData, canvasWidth, startY } = params;
|
||||||
|
const { problems } = compareData;
|
||||||
|
|
||||||
|
// 布局参数
|
||||||
|
const leftMargin = 30;
|
||||||
|
const rightMargin = 30;
|
||||||
|
const topMargin = 12;
|
||||||
|
const rowSpacing = 116; // 行之间的间距
|
||||||
|
const columnSpacing = 40; // 两列之间的间距(可调节)
|
||||||
|
|
||||||
|
// 计算每列的宽度(减去边距和列间距)
|
||||||
|
const availableWidth =
|
||||||
|
canvasWidth - leftMargin - rightMargin - columnSpacing;
|
||||||
|
const columnWidth = availableWidth / 2;
|
||||||
|
|
||||||
|
// 框的尺寸
|
||||||
|
const boxWidth = 100;
|
||||||
|
const boxHeight = 100;
|
||||||
|
const borderRadius = 12;
|
||||||
|
const circleRadius = 18; // 圆圈半径
|
||||||
|
|
||||||
|
// 框与圆圈之间的间距
|
||||||
|
const boxToCircleSpacing = 12;
|
||||||
|
|
||||||
|
// 计算左侧列和右侧列的起始X位置
|
||||||
|
const leftColumnStartX = leftMargin;
|
||||||
|
const rightColumnStartX = leftMargin + columnWidth + columnSpacing;
|
||||||
|
|
||||||
|
// 计算每列内部的布局(左框 | 圆圈 | 右框,居中)
|
||||||
|
const totalWidthPerColumn =
|
||||||
|
boxWidth +
|
||||||
|
boxToCircleSpacing +
|
||||||
|
circleRadius * 2 +
|
||||||
|
boxToCircleSpacing +
|
||||||
|
boxWidth;
|
||||||
|
const columnPadding = (columnWidth - totalWidthPerColumn) / 2;
|
||||||
|
|
||||||
|
let currentY = startY + topMargin;
|
||||||
|
|
||||||
|
// 固定绘制6行
|
||||||
|
const totalRows = 6;
|
||||||
|
for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
|
||||||
|
const rowY = currentY + rowIndex * rowSpacing;
|
||||||
|
// 绘制左侧列
|
||||||
|
const leftProblemIndex = rowIndex * 2;
|
||||||
|
if (leftProblemIndex < problems.length) {
|
||||||
|
await this.drawCompareProblem(
|
||||||
|
ctx,
|
||||||
|
canvas,
|
||||||
|
problems[leftProblemIndex],
|
||||||
|
leftColumnStartX + columnPadding,
|
||||||
|
rowY,
|
||||||
|
boxWidth,
|
||||||
|
boxHeight,
|
||||||
|
borderRadius,
|
||||||
|
circleRadius,
|
||||||
|
boxToCircleSpacing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制右侧列
|
||||||
|
const rightProblemIndex = rowIndex * 2 + 1;
|
||||||
|
if (rightProblemIndex < problems.length) {
|
||||||
|
await this.drawCompareProblem(
|
||||||
|
ctx,
|
||||||
|
canvas,
|
||||||
|
problems[rightProblemIndex],
|
||||||
|
rightColumnStartX + columnPadding,
|
||||||
|
rowY,
|
||||||
|
boxWidth,
|
||||||
|
boxHeight,
|
||||||
|
borderRadius,
|
||||||
|
circleRadius,
|
||||||
|
boxToCircleSpacing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制一个比较问题(左框 | 圆圈 | 右框)
|
||||||
|
*/
|
||||||
|
private async drawCompareProblem(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
canvas: WechatMiniprogram.Canvas,
|
||||||
|
problem: {
|
||||||
|
leftCount: number;
|
||||||
|
rightCount: number;
|
||||||
|
leftImageIndex: number;
|
||||||
|
rightImageIndex: number;
|
||||||
|
imageType: 'fruits' | 'twelve-animals';
|
||||||
|
},
|
||||||
|
startX: number,
|
||||||
|
startY: number,
|
||||||
|
boxWidth: number,
|
||||||
|
boxHeight: number,
|
||||||
|
borderRadius: number,
|
||||||
|
circleRadius: number,
|
||||||
|
boxToCircleSpacing: number,
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
leftCount,
|
||||||
|
rightCount,
|
||||||
|
leftImageIndex,
|
||||||
|
rightImageIndex,
|
||||||
|
imageType,
|
||||||
|
} = problem;
|
||||||
|
|
||||||
|
// 图片配置
|
||||||
|
const imageConfig = {
|
||||||
|
'twelve-animals': {
|
||||||
|
folder: 'twelve-animals',
|
||||||
|
maxIndex: 12,
|
||||||
|
},
|
||||||
|
fruits: {
|
||||||
|
folder: 'fruits',
|
||||||
|
maxIndex: 22,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = imageConfig[imageType] || imageConfig['twelve-animals'];
|
||||||
|
|
||||||
|
// 计算左框位置
|
||||||
|
const leftBoxX = startX;
|
||||||
|
const leftBoxY = startY;
|
||||||
|
|
||||||
|
// 计算圆圈位置
|
||||||
|
const circleX = leftBoxX + boxWidth + boxToCircleSpacing + circleRadius;
|
||||||
|
const circleY = startY + boxHeight / 2;
|
||||||
|
|
||||||
|
// 计算右框位置
|
||||||
|
const rightBoxX = circleX + circleRadius + boxToCircleSpacing;
|
||||||
|
const rightBoxY = startY;
|
||||||
|
|
||||||
|
// 绘制左框
|
||||||
|
ctx.strokeStyle = '#999';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
this.drawRoundedRectInternal(
|
||||||
|
ctx,
|
||||||
|
leftBoxX,
|
||||||
|
leftBoxY,
|
||||||
|
boxWidth,
|
||||||
|
boxHeight,
|
||||||
|
borderRadius,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 绘制左框内的图片
|
||||||
|
await this.drawImagesInBox(
|
||||||
|
ctx,
|
||||||
|
canvas,
|
||||||
|
leftCount,
|
||||||
|
leftImageIndex,
|
||||||
|
config.folder,
|
||||||
|
leftBoxX,
|
||||||
|
leftBoxY,
|
||||||
|
boxWidth,
|
||||||
|
boxHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 绘制实心圆圈
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(circleX, circleY, circleRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill(); // 先填充白色
|
||||||
|
ctx.stroke(); // 再绘制边框
|
||||||
|
|
||||||
|
// 绘制右框
|
||||||
|
ctx.strokeStyle = '#999';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
this.drawRoundedRectInternal(
|
||||||
|
ctx,
|
||||||
|
rightBoxX,
|
||||||
|
rightBoxY,
|
||||||
|
boxWidth,
|
||||||
|
boxHeight,
|
||||||
|
borderRadius,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 绘制右框内的图片
|
||||||
|
await this.drawImagesInBox(
|
||||||
|
ctx,
|
||||||
|
canvas,
|
||||||
|
rightCount,
|
||||||
|
rightImageIndex,
|
||||||
|
config.folder,
|
||||||
|
rightBoxX,
|
||||||
|
rightBoxY,
|
||||||
|
boxWidth,
|
||||||
|
boxHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在框内绘制多张图片
|
||||||
|
*/
|
||||||
|
private async drawImagesInBox(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
canvas: WechatMiniprogram.Canvas,
|
||||||
|
count: number,
|
||||||
|
imageIndex: number,
|
||||||
|
folder: string,
|
||||||
|
boxX: number,
|
||||||
|
boxY: number,
|
||||||
|
boxWidth: number,
|
||||||
|
boxHeight: number,
|
||||||
|
) {
|
||||||
|
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/${folder}/${imageIndex}.png`;
|
||||||
|
boxImage = await getImage(canvas, imagePath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`加载${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 CompareDraw;
|
export default CompareDraw;
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
import { getImage } from '../../../utils/index';
|
|
||||||
import { getRandomNumberColor } from '../../../constants/colors';
|
|
||||||
|
|
||||||
interface DrawCountMatchContentParams {
|
|
||||||
canvas: WechatMiniprogram.Canvas;
|
|
||||||
ctx: RenderingContext;
|
|
||||||
matchData: {
|
|
||||||
leftNumbers: number[];
|
|
||||||
rightNumbers: number[]; // 打乱顺序后的数字数组
|
|
||||||
};
|
|
||||||
canvasWidth: number;
|
|
||||||
startY: number;
|
|
||||||
imageType?: string; // 'twelve-animals' 或 'fruits'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制圆角矩形(虚线边框)
|
|
||||||
*/
|
|
||||||
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([]); // 重置为实线
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制数一数连一连内容区域
|
|
||||||
* 左侧显示数字(虚线圆角框),右侧显示对应数量的图片(虚线圆角框)
|
|
||||||
* 支持根据 imageType 选择不同的图片文件夹(twelve-animals 或 fruits)
|
|
||||||
*/
|
|
||||||
export async function drawCountMatchContent({
|
|
||||||
canvas,
|
|
||||||
ctx,
|
|
||||||
matchData,
|
|
||||||
canvasWidth,
|
|
||||||
startY,
|
|
||||||
imageType = 'twelve-animals',
|
|
||||||
}: DrawCountMatchContentParams): Promise<void> {
|
|
||||||
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;
|
|
||||||
drawRoundedRect(
|
|
||||||
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;
|
|
||||||
drawRoundedRect(
|
|
||||||
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/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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 数一数连一连绘制服务
|
||||||
|
*/
|
||||||
|
|
||||||
import { BaseDrawService } from '../../../service/baseDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawCountMatchContent } from './countMatchContentDraw';
|
import { getImage } from '../../../utils/index';
|
||||||
|
import { getRandomNumberColor } from '../../../constants/colors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数一数连一连数据
|
||||||
|
*/
|
||||||
|
export interface CountMatchData {
|
||||||
|
leftNumbers: number[];
|
||||||
|
rightNumbers: number[]; // 打乱顺序后的数字数组
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数一数连一连绘制服务
|
* 数一数连一连绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
|
||||||
*/
|
*/
|
||||||
class CountMatchDraw extends BaseDrawService {
|
class CountMatchDraw extends BaseDrawService {
|
||||||
matchData: {
|
matchData: CountMatchData | null = null;
|
||||||
leftNumbers: number[];
|
|
||||||
rightNumbers: number[]; // 打乱顺序后的数字数组
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
@@ -20,29 +29,21 @@ class CountMatchDraw extends BaseDrawService {
|
|||||||
this.matchData = null;
|
this.matchData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制数一数连一连内容
|
||||||
|
*/
|
||||||
async draw(
|
async draw(
|
||||||
matchData: { leftNumbers: number[]; rightNumbers: number[] },
|
matchData: CountMatchData,
|
||||||
imageType: string = 'twelve-animals',
|
imageType: string = 'twelve-animals',
|
||||||
) {
|
) {
|
||||||
if (!matchData || !matchData.leftNumbers || !matchData.rightNumbers) {
|
if (!matchData || !matchData.leftNumbers || !matchData.rightNumbers) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.matchData = matchData;
|
this.matchData = matchData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
await this.drawContent({
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域(数一数连一连)
|
|
||||||
this.drawDivider();
|
|
||||||
await drawCountMatchContent({
|
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
matchData: this.matchData,
|
matchData: this.matchData,
|
||||||
@@ -51,6 +52,305 @@ class CountMatchDraw extends BaseDrawService {
|
|||||||
imageType,
|
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/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;
|
export default CountMatchDraw;
|
||||||
|
|||||||
@@ -1,312 +0,0 @@
|
|||||||
import { getImage } from '../../../utils/index';
|
|
||||||
import { getRandomNumberColor } from '../../../constants/colors';
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 数一数选一选/填一填绘制服务
|
||||||
|
* 支持两种模式:'select'(选一选)和 'fill'(填一填)
|
||||||
|
*/
|
||||||
|
|
||||||
import { BaseDrawService } from '../../../service/baseDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawCountingSelectContent } from './countingSelectContentDraw';
|
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; // 选一选模式需要
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数一数选一选/填一填绘制服务
|
* 数一数选一选/填一填绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
|
||||||
* 支持两种模式:'select'(选一选)和 'fill'(填一填)
|
|
||||||
*/
|
*/
|
||||||
class CountingSelectDraw extends BaseDrawService {
|
class CountingSelectDraw extends BaseDrawService {
|
||||||
countingData: {
|
countingData: CountingSelectData | null = null;
|
||||||
problems: Array<{
|
|
||||||
count: number;
|
|
||||||
imageIndex: number;
|
|
||||||
imageType: 'fruits' | 'twelve-animals';
|
|
||||||
options?: number[];
|
|
||||||
correctIndex?: number;
|
|
||||||
}>;
|
|
||||||
} | null;
|
|
||||||
mode: 'select' | 'fill';
|
mode: 'select' | 'fill';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -28,38 +37,22 @@ class CountingSelectDraw extends BaseDrawService {
|
|||||||
this.mode = 'select'; // 默认选一选模式
|
this.mode = 'select'; // 默认选一选模式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制数一数选一选/填一填内容
|
||||||
|
*/
|
||||||
async draw(
|
async draw(
|
||||||
countingData: {
|
countingData: CountingSelectData,
|
||||||
problems: Array<{
|
|
||||||
count: number;
|
|
||||||
imageIndex: number;
|
|
||||||
imageType: 'fruits' | 'twelve-animals';
|
|
||||||
options?: number[];
|
|
||||||
correctIndex?: number;
|
|
||||||
}>;
|
|
||||||
},
|
|
||||||
mode: 'select' | 'fill' = 'select',
|
mode: 'select' | 'fill' = 'select',
|
||||||
) {
|
) {
|
||||||
if (!countingData || !countingData.problems) {
|
if (!countingData || !countingData.problems) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.countingData = countingData;
|
this.countingData = countingData;
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
await this.drawContent({
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域
|
|
||||||
this.drawDivider();
|
|
||||||
await drawCountingSelectContent({
|
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
countingData: this.countingData,
|
countingData: this.countingData,
|
||||||
@@ -68,6 +61,326 @@ class CountingSelectDraw extends BaseDrawService {
|
|||||||
startY: this.currentY,
|
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/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;
|
export default CountingSelectDraw;
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
interface DrawMissingNumberContentParams {
|
|
||||||
canvas: WechatMiniprogram.Canvas;
|
|
||||||
ctx: RenderingContext;
|
|
||||||
missingNumberData: {
|
|
||||||
grids: Array<{
|
|
||||||
numbers: (number | null)[];
|
|
||||||
colors: (string | null)[];
|
|
||||||
}>;
|
|
||||||
maxNumber: number;
|
|
||||||
};
|
|
||||||
canvasWidth: number;
|
|
||||||
startY: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gridMap = {
|
|
||||||
10: {
|
|
||||||
cellSize: 95,
|
|
||||||
colsPerGrid: 5,
|
|
||||||
fontSize: 50,
|
|
||||||
gridSpacing: 40,
|
|
||||||
},
|
|
||||||
20: {
|
|
||||||
cellSize: 80,
|
|
||||||
colsPerGrid: 5,
|
|
||||||
fontSize: 40,
|
|
||||||
gridSpacing: 30,
|
|
||||||
},
|
|
||||||
40: {
|
|
||||||
cellSize: 60,
|
|
||||||
colsPerGrid: 8,
|
|
||||||
fontSize: 35,
|
|
||||||
gridSpacing: 30,
|
|
||||||
},
|
|
||||||
50: {
|
|
||||||
cellSize: 52,
|
|
||||||
colsPerGrid: 10,
|
|
||||||
fontSize: 28,
|
|
||||||
gridSpacing: 40,
|
|
||||||
},
|
|
||||||
80: {
|
|
||||||
cellSize: 60,
|
|
||||||
colsPerGrid: 8,
|
|
||||||
fontSize: 30,
|
|
||||||
gridSpacing: 30,
|
|
||||||
},
|
|
||||||
100: {
|
|
||||||
cellSize: 52,
|
|
||||||
colsPerGrid: 10,
|
|
||||||
fontSize: 28,
|
|
||||||
gridSpacing: 20,
|
|
||||||
},
|
|
||||||
120: {
|
|
||||||
cellSize: 52,
|
|
||||||
colsPerGrid: 10,
|
|
||||||
fontSize: 28,
|
|
||||||
gridSpacing: 20,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制填上缺少的数字内容区域
|
|
||||||
* 显示多个网格,每个网格包含一些数字和一些空白位置
|
|
||||||
*/
|
|
||||||
export async function drawMissingNumberContent({
|
|
||||||
ctx,
|
|
||||||
missingNumberData,
|
|
||||||
canvasWidth,
|
|
||||||
startY,
|
|
||||||
}: DrawMissingNumberContentParams): Promise<void> {
|
|
||||||
const { grids, maxNumber } = missingNumberData;
|
|
||||||
|
|
||||||
// 计算网格布局参数
|
|
||||||
const leftMargin = 40;
|
|
||||||
const rightMargin = 40;
|
|
||||||
const topMargin = 20;
|
|
||||||
|
|
||||||
const config = gridMap[maxNumber as keyof typeof gridMap];
|
|
||||||
const { cellSize, gridSpacing, colsPerGrid, fontSize } = config;
|
|
||||||
|
|
||||||
const gridWidth = colsPerGrid * cellSize;
|
|
||||||
|
|
||||||
// 计算网格的起始X位置(居中)
|
|
||||||
const gridStartX =
|
|
||||||
leftMargin + (canvasWidth - leftMargin - rightMargin - gridWidth) / 2;
|
|
||||||
|
|
||||||
let currentY = startY + topMargin;
|
|
||||||
|
|
||||||
// 绘制每个网格
|
|
||||||
for (let gridIndex = 0; gridIndex < grids.length; gridIndex++) {
|
|
||||||
const grid = grids[gridIndex];
|
|
||||||
const gridX = gridStartX;
|
|
||||||
|
|
||||||
// 根据实际数字数量计算需要的行数
|
|
||||||
const rowsPerGrid = Math.ceil(grid.numbers.length / colsPerGrid);
|
|
||||||
const gridHeight = rowsPerGrid * cellSize;
|
|
||||||
const gridY = currentY;
|
|
||||||
|
|
||||||
// 绘制每个单元格
|
|
||||||
for (let index = 0; index < grid.numbers.length; index++) {
|
|
||||||
const row = Math.floor(index / colsPerGrid);
|
|
||||||
const col = index % colsPerGrid;
|
|
||||||
|
|
||||||
const cellX = gridX + col * cellSize;
|
|
||||||
const cellY = gridY + row * cellSize;
|
|
||||||
|
|
||||||
// 绘制单元格边框
|
|
||||||
ctx.strokeStyle = '#333';
|
|
||||||
ctx.lineWidth = 1.5;
|
|
||||||
ctx.strokeRect(cellX, cellY, cellSize, cellSize);
|
|
||||||
|
|
||||||
// 绘制数字或空白
|
|
||||||
const number = grid.numbers[index];
|
|
||||||
const color = grid.colors[index];
|
|
||||||
|
|
||||||
if (number !== null && color !== null) {
|
|
||||||
// 绘制数字(带颜色)
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText(
|
|
||||||
String(number),
|
|
||||||
cellX + cellSize / 2,
|
|
||||||
cellY + cellSize / 2,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果为null,则留空(用户填写)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新下一个网格的Y位置
|
|
||||||
currentY += gridHeight + gridSpacing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 填上缺少的数字绘制服务
|
||||||
|
*/
|
||||||
|
|
||||||
import { BaseDrawService } from '../../../service/baseDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawMissingNumberContent } from './missingNumberContentDraw';
|
|
||||||
|
/**
|
||||||
|
* 填上缺少的数字数据
|
||||||
|
*/
|
||||||
|
export interface MissingNumberData {
|
||||||
|
grids: Array<{
|
||||||
|
numbers: (number | null)[];
|
||||||
|
colors: (string | null)[];
|
||||||
|
}>;
|
||||||
|
maxNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 填上缺少的数字绘制服务
|
* 填上缺少的数字绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
|
||||||
*/
|
*/
|
||||||
class MissingNumberDraw extends BaseDrawService {
|
class MissingNumberDraw extends BaseDrawService {
|
||||||
missingNumberData: {
|
missingNumberData: MissingNumberData | null = null;
|
||||||
grids: Array<{
|
|
||||||
numbers: (number | null)[];
|
|
||||||
colors: (string | null)[];
|
|
||||||
}>;
|
|
||||||
maxNumber: number;
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
@@ -23,35 +30,21 @@ class MissingNumberDraw extends BaseDrawService {
|
|||||||
this.missingNumberData = null;
|
this.missingNumberData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制填上缺少的数字内容
|
||||||
|
*/
|
||||||
async draw(
|
async draw(
|
||||||
missingNumberData: {
|
missingNumberData: MissingNumberData,
|
||||||
grids: Array<{
|
|
||||||
numbers: (number | null)[];
|
|
||||||
colors: (string | null)[];
|
|
||||||
}>;
|
|
||||||
maxNumber: number;
|
|
||||||
},
|
|
||||||
rangeType: string = 'within-10',
|
rangeType: string = 'within-10',
|
||||||
) {
|
) {
|
||||||
if (!missingNumberData || !missingNumberData.grids) {
|
if (!missingNumberData || !missingNumberData.grids) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.missingNumberData = missingNumberData;
|
this.missingNumberData = missingNumberData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
await this.drawContent({
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域(填上缺少的数字)
|
|
||||||
this.drawDivider();
|
|
||||||
await drawMissingNumberContent({
|
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
missingNumberData: this.missingNumberData,
|
missingNumberData: this.missingNumberData,
|
||||||
@@ -59,6 +52,128 @@ class MissingNumberDraw extends BaseDrawService {
|
|||||||
startY: this.currentY,
|
startY: this.currentY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制内容区域
|
||||||
|
*/
|
||||||
|
private async drawContent(params: {
|
||||||
|
canvas: WechatMiniprogram.Canvas;
|
||||||
|
ctx: RenderingContext;
|
||||||
|
missingNumberData: MissingNumberData;
|
||||||
|
canvasWidth: number;
|
||||||
|
startY: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { ctx, missingNumberData, canvasWidth, startY } = params;
|
||||||
|
const { grids, maxNumber } = missingNumberData;
|
||||||
|
|
||||||
|
const gridMap = {
|
||||||
|
10: {
|
||||||
|
cellSize: 95,
|
||||||
|
colsPerGrid: 5,
|
||||||
|
fontSize: 50,
|
||||||
|
gridSpacing: 40,
|
||||||
|
},
|
||||||
|
20: {
|
||||||
|
cellSize: 80,
|
||||||
|
colsPerGrid: 5,
|
||||||
|
fontSize: 40,
|
||||||
|
gridSpacing: 30,
|
||||||
|
},
|
||||||
|
40: {
|
||||||
|
cellSize: 60,
|
||||||
|
colsPerGrid: 8,
|
||||||
|
fontSize: 35,
|
||||||
|
gridSpacing: 30,
|
||||||
|
},
|
||||||
|
50: {
|
||||||
|
cellSize: 52,
|
||||||
|
colsPerGrid: 10,
|
||||||
|
fontSize: 28,
|
||||||
|
gridSpacing: 40,
|
||||||
|
},
|
||||||
|
80: {
|
||||||
|
cellSize: 60,
|
||||||
|
colsPerGrid: 8,
|
||||||
|
fontSize: 30,
|
||||||
|
gridSpacing: 30,
|
||||||
|
},
|
||||||
|
100: {
|
||||||
|
cellSize: 52,
|
||||||
|
colsPerGrid: 10,
|
||||||
|
fontSize: 28,
|
||||||
|
gridSpacing: 20,
|
||||||
|
},
|
||||||
|
120: {
|
||||||
|
cellSize: 52,
|
||||||
|
colsPerGrid: 10,
|
||||||
|
fontSize: 28,
|
||||||
|
gridSpacing: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算网格布局参数
|
||||||
|
const leftMargin = 40;
|
||||||
|
const rightMargin = 40;
|
||||||
|
const topMargin = 20;
|
||||||
|
|
||||||
|
const config = gridMap[maxNumber as keyof typeof gridMap];
|
||||||
|
const { cellSize, gridSpacing, colsPerGrid, fontSize } = config;
|
||||||
|
|
||||||
|
const gridWidth = colsPerGrid * cellSize;
|
||||||
|
|
||||||
|
// 计算网格的起始X位置(居中)
|
||||||
|
const gridStartX =
|
||||||
|
leftMargin +
|
||||||
|
(canvasWidth - leftMargin - rightMargin - gridWidth) / 2;
|
||||||
|
|
||||||
|
let currentY = startY + topMargin;
|
||||||
|
|
||||||
|
// 绘制每个网格
|
||||||
|
for (let gridIndex = 0; gridIndex < grids.length; gridIndex++) {
|
||||||
|
const grid = grids[gridIndex];
|
||||||
|
const gridX = gridStartX;
|
||||||
|
|
||||||
|
// 根据实际数字数量计算需要的行数
|
||||||
|
const rowsPerGrid = Math.ceil(grid.numbers.length / colsPerGrid);
|
||||||
|
const gridHeight = rowsPerGrid * cellSize;
|
||||||
|
const gridY = currentY;
|
||||||
|
|
||||||
|
// 绘制每个单元格
|
||||||
|
for (let index = 0; index < grid.numbers.length; index++) {
|
||||||
|
const row = Math.floor(index / colsPerGrid);
|
||||||
|
const col = index % colsPerGrid;
|
||||||
|
|
||||||
|
const cellX = gridX + col * cellSize;
|
||||||
|
const cellY = gridY + row * cellSize;
|
||||||
|
|
||||||
|
// 绘制单元格边框
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.strokeRect(cellX, cellY, cellSize, cellSize);
|
||||||
|
|
||||||
|
// 绘制数字或空白
|
||||||
|
const number = grid.numbers[index];
|
||||||
|
const color = grid.colors[index];
|
||||||
|
|
||||||
|
if (number !== null && color !== null) {
|
||||||
|
// 绘制数字(带颜色)
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(
|
||||||
|
String(number),
|
||||||
|
cellX + cellSize / 2,
|
||||||
|
cellY + cellSize / 2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 如果为null,则留空(用户填写)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新下一个网格的Y位置
|
||||||
|
currentY += gridHeight + gridSpacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MissingNumberDraw;
|
export default MissingNumberDraw;
|
||||||
|
|||||||
@@ -1,459 +0,0 @@
|
|||||||
import { getNumberColors } from '../../../constants/colors';
|
|
||||||
|
|
||||||
interface DrawNumberColorContentParams {
|
|
||||||
canvas: WechatMiniprogram.Canvas;
|
|
||||||
ctx: RenderingContext;
|
|
||||||
colorData: {
|
|
||||||
numbers: number[];
|
|
||||||
};
|
|
||||||
canvasWidth: number;
|
|
||||||
startY: number;
|
|
||||||
patternType?: string; // 'circle' 或 'caterpillar'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制圆角矩形(虚线边框)
|
|
||||||
*/
|
|
||||||
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, 6]); // 虚线
|
|
||||||
} else {
|
|
||||||
ctx.setLineDash([]);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.setLineDash([]); // 重置为实线
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制毛毛虫头部
|
|
||||||
*/
|
|
||||||
function drawCaterpillarHead(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
size: number,
|
|
||||||
color: string,
|
|
||||||
) {
|
|
||||||
const headRadius = 18; // 固定头部半径
|
|
||||||
const headCenterX = x + headRadius;
|
|
||||||
const headCenterY = y + size / 2;
|
|
||||||
|
|
||||||
// 绘制头部(圆形)
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 绘制头部圆圈实线边框
|
|
||||||
ctx.strokeStyle = '#000';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.setLineDash([]); // 确保是实线
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 绘制触角(在头部圆圈的正上方,带弧度的弯曲)
|
|
||||||
const antennaLength = headRadius * 0.6;
|
|
||||||
const antennaStartY = headCenterY - headRadius; // 头部圆圈的正上方
|
|
||||||
const antennaOffset = headRadius * 0.3; // 触角左右偏移距离
|
|
||||||
|
|
||||||
// 左侧触角:从顶部左侧开始,先向上,然后向左弯曲(带弧度)
|
|
||||||
const leftAntennaStartX = headCenterX - antennaOffset;
|
|
||||||
const leftAntennaStartY = antennaStartY;
|
|
||||||
const leftAntennaMidY = leftAntennaStartY - antennaLength * 0.5; // 向上延伸的中点
|
|
||||||
const leftAntennaEndX = leftAntennaStartX - antennaLength * 0.4; // 向左弯曲
|
|
||||||
const leftAntennaEndY = leftAntennaMidY - antennaLength * 0.3; // 向上并向左
|
|
||||||
|
|
||||||
// 右侧触角:从顶部右侧开始,先向上,然后向右弯曲(带弧度)
|
|
||||||
const rightAntennaStartX = headCenterX + antennaOffset;
|
|
||||||
const rightAntennaStartY = antennaStartY;
|
|
||||||
const rightAntennaMidY = rightAntennaStartY - antennaLength * 0.5; // 向上延伸的中点
|
|
||||||
const rightAntennaEndX = rightAntennaStartX + antennaLength * 0.4; // 向右弯曲
|
|
||||||
const rightAntennaEndY = rightAntennaMidY - antennaLength * 0.3; // 向上并向右
|
|
||||||
|
|
||||||
ctx.strokeStyle = '#000';
|
|
||||||
ctx.lineWidth = 1.5;
|
|
||||||
ctx.lineCap = 'round'; // 圆角端点
|
|
||||||
|
|
||||||
// 绘制左侧触角(使用二次贝塞尔曲线实现平滑弧度)
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(leftAntennaStartX, leftAntennaStartY);
|
|
||||||
// 使用二次贝塞尔曲线:起点、控制点、终点
|
|
||||||
// 控制点在中间位置,使曲线平滑向左弯曲
|
|
||||||
const leftControlX = leftAntennaStartX - antennaLength * 0.2;
|
|
||||||
const leftControlY = leftAntennaMidY;
|
|
||||||
ctx.quadraticCurveTo(
|
|
||||||
leftControlX,
|
|
||||||
leftControlY,
|
|
||||||
leftAntennaEndX,
|
|
||||||
leftAntennaEndY,
|
|
||||||
);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 绘制右侧触角(使用二次贝塞尔曲线实现平滑弧度)
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(rightAntennaStartX, rightAntennaStartY);
|
|
||||||
// 使用二次贝塞尔曲线:起点、控制点、终点
|
|
||||||
// 控制点在中间位置,使曲线平滑向右弯曲
|
|
||||||
const rightControlX = rightAntennaStartX + antennaLength * 0.2;
|
|
||||||
const rightControlY = rightAntennaMidY;
|
|
||||||
ctx.quadraticCurveTo(
|
|
||||||
rightControlX,
|
|
||||||
rightControlY,
|
|
||||||
rightAntennaEndX,
|
|
||||||
rightAntennaEndY,
|
|
||||||
);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 绘制触角末端的小圆点
|
|
||||||
ctx.fillStyle = '#000';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(leftAntennaEndX, leftAntennaEndY, 2, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(rightAntennaEndX, rightAntennaEndY, 2, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 绘制眼睛
|
|
||||||
const eyeSize = headRadius * 0.15;
|
|
||||||
const eyeOffsetX = headRadius * 0.25;
|
|
||||||
const eyeOffsetY = headRadius * 0.22;
|
|
||||||
ctx.fillStyle = '#fff';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(
|
|
||||||
headCenterX - eyeOffsetX - 2,
|
|
||||||
headCenterY - eyeOffsetY,
|
|
||||||
eyeSize,
|
|
||||||
0,
|
|
||||||
Math.PI * 2,
|
|
||||||
);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(
|
|
||||||
headCenterX + eyeOffsetX - 2,
|
|
||||||
headCenterY - eyeOffsetY,
|
|
||||||
eyeSize,
|
|
||||||
0,
|
|
||||||
Math.PI * 2,
|
|
||||||
);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 绘制眼珠
|
|
||||||
ctx.fillStyle = '#000';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(
|
|
||||||
headCenterX - eyeOffsetX - 2,
|
|
||||||
headCenterY - eyeOffsetY,
|
|
||||||
eyeSize * 0.6,
|
|
||||||
0,
|
|
||||||
Math.PI * 2,
|
|
||||||
);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(
|
|
||||||
headCenterX + eyeOffsetX - 2,
|
|
||||||
headCenterY - eyeOffsetY,
|
|
||||||
eyeSize * 0.6,
|
|
||||||
0,
|
|
||||||
Math.PI * 2,
|
|
||||||
);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 绘制嘴巴(微笑)
|
|
||||||
ctx.strokeStyle = '#000';
|
|
||||||
ctx.lineWidth = 1.5;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(
|
|
||||||
headCenterX - 2,
|
|
||||||
headCenterY + headRadius * 0.22,
|
|
||||||
headRadius * 0.3,
|
|
||||||
0,
|
|
||||||
Math.PI,
|
|
||||||
);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制毛毛虫身体(带弧度的圆圈)
|
|
||||||
*/
|
|
||||||
function drawCaterpillarBody(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
startX: number,
|
|
||||||
startY: number,
|
|
||||||
count: number,
|
|
||||||
bodyRadius: number,
|
|
||||||
color: string,
|
|
||||||
) {
|
|
||||||
const spacing = bodyRadius * 2; // 身体圆圈之间的间距(稍微重叠)
|
|
||||||
const waveAmplitude = bodyRadius * 0.4; // 波浪幅度
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
// 计算每个身体圆圈的位置(带弧度,形成弯曲效果)
|
|
||||||
const baseX = startX + i * spacing;
|
|
||||||
// 使用正弦函数创建波浪效果,使身体有弧度
|
|
||||||
const waveOffset = Math.sin((i * Math.PI) / 2.5) * waveAmplitude;
|
|
||||||
const bodyX = baseX;
|
|
||||||
const bodyY = startY + waveOffset;
|
|
||||||
|
|
||||||
// 绘制身体圆圈
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(bodyX, bodyY, bodyRadius, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 绘制边框(可选,根据设计需求)
|
|
||||||
ctx.strokeStyle = '#000';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制圆圈模式的内容
|
|
||||||
*/
|
|
||||||
function drawCirclePattern(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
number: number,
|
|
||||||
numberColor: string,
|
|
||||||
isFirstRow: boolean,
|
|
||||||
) {
|
|
||||||
const totalCircles = 10;
|
|
||||||
const circleRadius = 16; // 固定半径
|
|
||||||
const circleSpacing = 6; // 固定间距
|
|
||||||
|
|
||||||
// 计算所有圆圈的总宽度
|
|
||||||
const totalWidth =
|
|
||||||
totalCircles * circleRadius * 2 + (totalCircles - 1) * circleSpacing;
|
|
||||||
|
|
||||||
// 计算水平居中位置
|
|
||||||
const startX = x + (width - totalWidth) / 2 + circleRadius;
|
|
||||||
|
|
||||||
// 计算垂直居中位置
|
|
||||||
const centerY = y + height / 2;
|
|
||||||
|
|
||||||
// 绘制10个虚线圆圈(一行)
|
|
||||||
for (let i = 0; i < totalCircles; i++) {
|
|
||||||
const circleX = startX + i * (circleRadius * 2 + circleSpacing);
|
|
||||||
|
|
||||||
// 绘制虚线圆圈
|
|
||||||
ctx.strokeStyle = numberColor;
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.setLineDash([3, 3]);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(circleX, centerY, circleRadius, 0, Math.PI * 2);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.setLineDash([]);
|
|
||||||
|
|
||||||
// 第一行:根据数字数量涂色
|
|
||||||
if (isFirstRow && i < number) {
|
|
||||||
ctx.fillStyle = numberColor;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(circleX, centerY, circleRadius - 1, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制毛毛虫模式的内容
|
|
||||||
*/
|
|
||||||
function drawCaterpillarPattern(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
number: number,
|
|
||||||
numberColor: string,
|
|
||||||
isFirstRow: boolean,
|
|
||||||
) {
|
|
||||||
const padding = 15;
|
|
||||||
const headSize = 45; // 头部区域高度(用于垂直居中)
|
|
||||||
const headRadius = 18; // 固定头部半径
|
|
||||||
const bodyRadius = 16; // 固定身体半径
|
|
||||||
|
|
||||||
if (isFirstRow) {
|
|
||||||
// 第一行:绘制完整的毛毛虫(头部+身体)
|
|
||||||
const headX = x + padding;
|
|
||||||
const headY = y + (height - headSize) / 2;
|
|
||||||
|
|
||||||
// 绘制头部
|
|
||||||
drawCaterpillarHead(ctx, headX, headY, headSize, numberColor);
|
|
||||||
|
|
||||||
// 计算头部的实际宽度(头部半径 * 2)
|
|
||||||
const headWidth = headRadius * 2; // 头部的实际宽度 = 36
|
|
||||||
|
|
||||||
// 绘制身体(从头部右侧边缘开始,留一点间距)
|
|
||||||
const bodyStartX = headX + headWidth + 17; // 头部右边缘 + 5px间距
|
|
||||||
const bodyStartY = headY + headSize / 2; // 与头部中心Y对齐
|
|
||||||
drawCaterpillarBody(
|
|
||||||
ctx,
|
|
||||||
bodyStartX,
|
|
||||||
bodyStartY,
|
|
||||||
number,
|
|
||||||
bodyRadius,
|
|
||||||
numberColor,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 其他行:只绘制头部
|
|
||||||
const headX = x + padding;
|
|
||||||
const headY = y + (height - headSize) / 2;
|
|
||||||
drawCaterpillarHead(ctx, headX, headY, headSize, numberColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制按数字涂颜色内容区域
|
|
||||||
*/
|
|
||||||
export async function drawNumberColorContent({
|
|
||||||
canvas,
|
|
||||||
ctx,
|
|
||||||
colorData,
|
|
||||||
canvasWidth,
|
|
||||||
startY,
|
|
||||||
patternType = 'circle',
|
|
||||||
}: DrawNumberColorContentParams): Promise<void> {
|
|
||||||
const { numbers } = colorData;
|
|
||||||
|
|
||||||
const leftMargin = 24;
|
|
||||||
const rightMargin = 24;
|
|
||||||
const itemSpacing = 115; // 行间距
|
|
||||||
const leftBoxWidth = 100;
|
|
||||||
const rightBoxWidth = 420;
|
|
||||||
const boxHeight = 80;
|
|
||||||
const borderRadius = 12;
|
|
||||||
const startYPos = startY + 20;
|
|
||||||
|
|
||||||
// 获取所有可用颜色值
|
|
||||||
const availableColors = getNumberColors();
|
|
||||||
|
|
||||||
// 为每一行随机分配颜色,确保不重复
|
|
||||||
const shuffledColors = [...availableColors];
|
|
||||||
// Fisher-Yates 洗牌算法
|
|
||||||
for (let i = shuffledColors.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[shuffledColors[i], shuffledColors[j]] = [
|
|
||||||
shuffledColors[j],
|
|
||||||
shuffledColors[i],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为每一行分配颜色(确保不重复)
|
|
||||||
// 如果行数超过颜色数量,从剩余颜色中随机选择,但确保相邻行不同
|
|
||||||
const rowColors: string[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < numbers.length; i++) {
|
|
||||||
if (i < shuffledColors.length) {
|
|
||||||
// 前 N 行(N <= 颜色数量)使用不同的颜色
|
|
||||||
rowColors.push(shuffledColors[i]);
|
|
||||||
} else {
|
|
||||||
// 如果行数超过颜色数量,从剩余颜色中选择(确保与上一行不同)
|
|
||||||
const remainingColors = shuffledColors.filter(
|
|
||||||
(color) => color !== rowColors[i - 1],
|
|
||||||
);
|
|
||||||
const randomColor =
|
|
||||||
remainingColors[
|
|
||||||
Math.floor(Math.random() * remainingColors.length)
|
|
||||||
];
|
|
||||||
rowColors.push(randomColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算左侧和右侧的起始X位置
|
|
||||||
const leftBoxX = leftMargin;
|
|
||||||
const rightBoxX = canvasWidth - rightMargin - rightBoxWidth;
|
|
||||||
|
|
||||||
// 绘制每一行
|
|
||||||
for (let i = 0; i < numbers.length; i++) {
|
|
||||||
const boxY = startYPos + i * itemSpacing;
|
|
||||||
const number = numbers[i];
|
|
||||||
const numberColor = rowColors[i]; // 使用随机分配的颜色
|
|
||||||
const isFirstRow = i === 0;
|
|
||||||
|
|
||||||
// 绘制左侧数字框(虚线)
|
|
||||||
ctx.strokeStyle = '#999';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
drawRoundedRect(
|
|
||||||
ctx,
|
|
||||||
leftBoxX,
|
|
||||||
boxY,
|
|
||||||
leftBoxWidth,
|
|
||||||
boxHeight,
|
|
||||||
borderRadius,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 绘制数字(带颜色)
|
|
||||||
ctx.fillStyle = numberColor;
|
|
||||||
ctx.font = `bold ${56}px "Microsoft Yahei"`;
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText(
|
|
||||||
String(number),
|
|
||||||
leftBoxX + leftBoxWidth / 2,
|
|
||||||
boxY + boxHeight / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 绘制右侧内容框(虚线)
|
|
||||||
ctx.strokeStyle = '#999';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
drawRoundedRect(
|
|
||||||
ctx,
|
|
||||||
rightBoxX,
|
|
||||||
boxY,
|
|
||||||
rightBoxWidth,
|
|
||||||
boxHeight,
|
|
||||||
borderRadius,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 根据模式绘制右侧内容
|
|
||||||
if (patternType === 'circle') {
|
|
||||||
drawCirclePattern(
|
|
||||||
ctx,
|
|
||||||
rightBoxX,
|
|
||||||
boxY,
|
|
||||||
rightBoxWidth,
|
|
||||||
boxHeight,
|
|
||||||
number,
|
|
||||||
numberColor,
|
|
||||||
isFirstRow,
|
|
||||||
);
|
|
||||||
} else if (patternType === 'caterpillar') {
|
|
||||||
drawCaterpillarPattern(
|
|
||||||
ctx,
|
|
||||||
rightBoxX,
|
|
||||||
boxY,
|
|
||||||
rightBoxWidth,
|
|
||||||
boxHeight,
|
|
||||||
number,
|
|
||||||
numberColor,
|
|
||||||
isFirstRow,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 按数字涂颜色绘制服务
|
||||||
|
*/
|
||||||
|
|
||||||
import { BaseDrawService } from '../../../service/baseDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawNumberColorContent } from './numberColorContentDraw';
|
import { getNumberColors } from '../../../constants/colors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按数字涂颜色数据
|
||||||
|
*/
|
||||||
|
export interface NumberColorData {
|
||||||
|
numbers: number[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按数字涂颜色绘制服务
|
* 按数字涂颜色绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
|
||||||
*/
|
*/
|
||||||
class NumberColorDraw extends BaseDrawService {
|
class NumberColorDraw extends BaseDrawService {
|
||||||
colorData: {
|
colorData: NumberColorData | null = null;
|
||||||
numbers: number[];
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
@@ -19,29 +27,21 @@ class NumberColorDraw extends BaseDrawService {
|
|||||||
this.colorData = null;
|
this.colorData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制按数字涂颜色内容
|
||||||
|
*/
|
||||||
async draw(
|
async draw(
|
||||||
colorData: { numbers: number[] },
|
colorData: NumberColorData,
|
||||||
patternType: string = 'circle', // 'circle' 或 'caterpillar'
|
patternType: string = 'circle', // 'circle' 或 'caterpillar'
|
||||||
) {
|
) {
|
||||||
if (!colorData || !colorData.numbers) {
|
if (!colorData || !colorData.numbers) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.colorData = colorData;
|
this.colorData = colorData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
await this.drawContent({
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域(按数字涂颜色)
|
|
||||||
this.drawDivider();
|
|
||||||
await drawNumberColorContent({
|
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
colorData: this.colorData,
|
colorData: this.colorData,
|
||||||
@@ -50,6 +50,466 @@ class NumberColorDraw extends BaseDrawService {
|
|||||||
patternType,
|
patternType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制内容区域
|
||||||
|
*/
|
||||||
|
private async drawContent(params: {
|
||||||
|
canvas: WechatMiniprogram.Canvas;
|
||||||
|
ctx: RenderingContext;
|
||||||
|
colorData: NumberColorData;
|
||||||
|
canvasWidth: number;
|
||||||
|
startY: number;
|
||||||
|
patternType?: string; // 'circle' 或 'caterpillar'
|
||||||
|
}): Promise<void> {
|
||||||
|
const {
|
||||||
|
ctx,
|
||||||
|
colorData,
|
||||||
|
canvasWidth,
|
||||||
|
startY,
|
||||||
|
patternType = 'circle',
|
||||||
|
} = params;
|
||||||
|
const { numbers } = colorData;
|
||||||
|
|
||||||
|
const leftMargin = 24;
|
||||||
|
const rightMargin = 24;
|
||||||
|
const itemSpacing = 115; // 行间距
|
||||||
|
const leftBoxWidth = 100;
|
||||||
|
const rightBoxWidth = 420;
|
||||||
|
const boxHeight = 80;
|
||||||
|
const borderRadius = 12;
|
||||||
|
const startYPos = startY + 20;
|
||||||
|
|
||||||
|
// 获取所有可用颜色值
|
||||||
|
const availableColors = getNumberColors();
|
||||||
|
|
||||||
|
// 为每一行随机分配颜色,确保不重复
|
||||||
|
const shuffledColors = [...availableColors];
|
||||||
|
// Fisher-Yates 洗牌算法
|
||||||
|
for (let i = shuffledColors.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[shuffledColors[i], shuffledColors[j]] = [
|
||||||
|
shuffledColors[j],
|
||||||
|
shuffledColors[i],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每一行分配颜色(确保不重复)
|
||||||
|
// 如果行数超过颜色数量,从剩余颜色中随机选择,但确保相邻行不同
|
||||||
|
const rowColors: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < numbers.length; i++) {
|
||||||
|
if (i < shuffledColors.length) {
|
||||||
|
// 前 N 行(N <= 颜色数量)使用不同的颜色
|
||||||
|
rowColors.push(shuffledColors[i]);
|
||||||
|
} else {
|
||||||
|
// 如果行数超过颜色数量,从剩余颜色中选择(确保与上一行不同)
|
||||||
|
const remainingColors = shuffledColors.filter(
|
||||||
|
(color) => color !== rowColors[i - 1],
|
||||||
|
);
|
||||||
|
const randomColor =
|
||||||
|
remainingColors[
|
||||||
|
Math.floor(Math.random() * remainingColors.length)
|
||||||
|
];
|
||||||
|
rowColors.push(randomColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算左侧和右侧的起始X位置
|
||||||
|
const leftBoxX = leftMargin;
|
||||||
|
const rightBoxX = canvasWidth - rightMargin - rightBoxWidth;
|
||||||
|
|
||||||
|
// 绘制每一行
|
||||||
|
for (let i = 0; i < numbers.length; i++) {
|
||||||
|
const boxY = startYPos + i * itemSpacing;
|
||||||
|
const number = numbers[i];
|
||||||
|
const numberColor = rowColors[i]; // 使用随机分配的颜色
|
||||||
|
const isFirstRow = i === 0;
|
||||||
|
|
||||||
|
// 绘制左侧数字框(虚线)
|
||||||
|
ctx.strokeStyle = '#999';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
this.drawRoundedRectInternal(
|
||||||
|
ctx,
|
||||||
|
leftBoxX,
|
||||||
|
boxY,
|
||||||
|
leftBoxWidth,
|
||||||
|
boxHeight,
|
||||||
|
borderRadius,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 绘制数字(带颜色)
|
||||||
|
ctx.fillStyle = numberColor;
|
||||||
|
ctx.font = `bold ${56}px "Microsoft Yahei"`;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(
|
||||||
|
String(number),
|
||||||
|
leftBoxX + leftBoxWidth / 2,
|
||||||
|
boxY + boxHeight / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 绘制右侧内容框(虚线)
|
||||||
|
ctx.strokeStyle = '#999';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
this.drawRoundedRectInternal(
|
||||||
|
ctx,
|
||||||
|
rightBoxX,
|
||||||
|
boxY,
|
||||||
|
rightBoxWidth,
|
||||||
|
boxHeight,
|
||||||
|
borderRadius,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 根据模式绘制右侧内容
|
||||||
|
if (patternType === 'circle') {
|
||||||
|
this.drawCirclePattern(
|
||||||
|
ctx,
|
||||||
|
rightBoxX,
|
||||||
|
boxY,
|
||||||
|
rightBoxWidth,
|
||||||
|
boxHeight,
|
||||||
|
number,
|
||||||
|
numberColor,
|
||||||
|
isFirstRow,
|
||||||
|
);
|
||||||
|
} else if (patternType === 'caterpillar') {
|
||||||
|
this.drawCaterpillarPattern(
|
||||||
|
ctx,
|
||||||
|
rightBoxX,
|
||||||
|
boxY,
|
||||||
|
rightBoxWidth,
|
||||||
|
boxHeight,
|
||||||
|
number,
|
||||||
|
numberColor,
|
||||||
|
isFirstRow,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制圆圈模式的内容
|
||||||
|
*/
|
||||||
|
private drawCirclePattern(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
number: number,
|
||||||
|
numberColor: string,
|
||||||
|
isFirstRow: boolean,
|
||||||
|
) {
|
||||||
|
const totalCircles = 10;
|
||||||
|
const circleRadius = 16; // 固定半径
|
||||||
|
const circleSpacing = 6; // 固定间距
|
||||||
|
|
||||||
|
// 计算所有圆圈的总宽度
|
||||||
|
const totalWidth =
|
||||||
|
totalCircles * circleRadius * 2 +
|
||||||
|
(totalCircles - 1) * circleSpacing;
|
||||||
|
|
||||||
|
// 计算水平居中位置
|
||||||
|
const startX = x + (width - totalWidth) / 2 + circleRadius;
|
||||||
|
|
||||||
|
// 计算垂直居中位置
|
||||||
|
const centerY = y + height / 2;
|
||||||
|
|
||||||
|
// 绘制10个虚线圆圈(一行)
|
||||||
|
for (let i = 0; i < totalCircles; i++) {
|
||||||
|
const circleX = startX + i * (circleRadius * 2 + circleSpacing);
|
||||||
|
|
||||||
|
// 绘制虚线圆圈
|
||||||
|
ctx.strokeStyle = numberColor;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([3, 3]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(circleX, centerY, circleRadius, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
|
// 第一行:根据数字数量涂色
|
||||||
|
if (isFirstRow && i < number) {
|
||||||
|
ctx.fillStyle = numberColor;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(circleX, centerY, circleRadius - 1, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制毛毛虫模式的内容
|
||||||
|
*/
|
||||||
|
private drawCaterpillarPattern(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
number: number,
|
||||||
|
numberColor: string,
|
||||||
|
isFirstRow: boolean,
|
||||||
|
) {
|
||||||
|
const padding = 15;
|
||||||
|
const headSize = 45; // 头部区域高度(用于垂直居中)
|
||||||
|
const headRadius = 18; // 固定头部半径
|
||||||
|
const bodyRadius = 16; // 固定身体半径
|
||||||
|
|
||||||
|
if (isFirstRow) {
|
||||||
|
// 第一行:绘制完整的毛毛虫(头部+身体)
|
||||||
|
const headX = x + padding;
|
||||||
|
const headY = y + (height - headSize) / 2;
|
||||||
|
|
||||||
|
// 绘制头部
|
||||||
|
this.drawCaterpillarHead(ctx, headX, headY, headSize, numberColor);
|
||||||
|
|
||||||
|
// 计算头部的实际宽度(头部半径 * 2)
|
||||||
|
const headWidth = headRadius * 2; // 头部的实际宽度 = 36
|
||||||
|
|
||||||
|
// 绘制身体(从头部右侧边缘开始,留一点间距)
|
||||||
|
const bodyStartX = headX + headWidth + 17; // 头部右边缘 + 5px间距
|
||||||
|
const bodyStartY = headY + headSize / 2; // 与头部中心Y对齐
|
||||||
|
this.drawCaterpillarBody(
|
||||||
|
ctx,
|
||||||
|
bodyStartX,
|
||||||
|
bodyStartY,
|
||||||
|
number,
|
||||||
|
bodyRadius,
|
||||||
|
numberColor,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 其他行:只绘制头部
|
||||||
|
const headX = x + padding;
|
||||||
|
const headY = y + (height - headSize) / 2;
|
||||||
|
this.drawCaterpillarHead(ctx, headX, headY, headSize, numberColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制毛毛虫头部
|
||||||
|
*/
|
||||||
|
private drawCaterpillarHead(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
size: number,
|
||||||
|
color: string,
|
||||||
|
) {
|
||||||
|
const headRadius = 18; // 固定头部半径
|
||||||
|
const headCenterX = x + headRadius;
|
||||||
|
const headCenterY = y + size / 2;
|
||||||
|
|
||||||
|
// 绘制头部(圆形)
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// 绘制头部圆圈实线边框
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([]); // 确保是实线
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(headCenterX, headCenterY, headRadius, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 绘制触角(在头部圆圈的正上方,带弧度的弯曲)
|
||||||
|
const antennaLength = headRadius * 0.6;
|
||||||
|
const antennaStartY = headCenterY - headRadius; // 头部圆圈的正上方
|
||||||
|
const antennaOffset = headRadius * 0.3; // 触角左右偏移距离
|
||||||
|
|
||||||
|
// 左侧触角:从顶部左侧开始,先向上,然后向左弯曲(带弧度)
|
||||||
|
const leftAntennaStartX = headCenterX - antennaOffset;
|
||||||
|
const leftAntennaStartY = antennaStartY;
|
||||||
|
const leftAntennaMidY = leftAntennaStartY - antennaLength * 0.5; // 向上延伸的中点
|
||||||
|
const leftAntennaEndX = leftAntennaStartX - antennaLength * 0.4; // 向左弯曲
|
||||||
|
const leftAntennaEndY = leftAntennaMidY - antennaLength * 0.3; // 向上并向左
|
||||||
|
|
||||||
|
// 右侧触角:从顶部右侧开始,先向上,然后向右弯曲(带弧度)
|
||||||
|
const rightAntennaStartX = headCenterX + antennaOffset;
|
||||||
|
const rightAntennaStartY = antennaStartY;
|
||||||
|
const rightAntennaMidY = rightAntennaStartY - antennaLength * 0.5; // 向上延伸的中点
|
||||||
|
const rightAntennaEndX = rightAntennaStartX + antennaLength * 0.4; // 向右弯曲
|
||||||
|
const rightAntennaEndY = rightAntennaMidY - antennaLength * 0.3; // 向上并向右
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.lineCap = 'round'; // 圆角端点
|
||||||
|
|
||||||
|
// 绘制左侧触角(使用二次贝塞尔曲线实现平滑弧度)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(leftAntennaStartX, leftAntennaStartY);
|
||||||
|
// 使用二次贝塞尔曲线:起点、控制点、终点
|
||||||
|
// 控制点在中间位置,使曲线平滑向左弯曲
|
||||||
|
const leftControlX = leftAntennaStartX - antennaLength * 0.2;
|
||||||
|
const leftControlY = leftAntennaMidY;
|
||||||
|
ctx.quadraticCurveTo(
|
||||||
|
leftControlX,
|
||||||
|
leftControlY,
|
||||||
|
leftAntennaEndX,
|
||||||
|
leftAntennaEndY,
|
||||||
|
);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 绘制右侧触角(使用二次贝塞尔曲线实现平滑弧度)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(rightAntennaStartX, rightAntennaStartY);
|
||||||
|
// 使用二次贝塞尔曲线:起点、控制点、终点
|
||||||
|
// 控制点在中间位置,使曲线平滑向右弯曲
|
||||||
|
const rightControlX = rightAntennaStartX + antennaLength * 0.2;
|
||||||
|
const rightControlY = rightAntennaMidY;
|
||||||
|
ctx.quadraticCurveTo(
|
||||||
|
rightControlX,
|
||||||
|
rightControlY,
|
||||||
|
rightAntennaEndX,
|
||||||
|
rightAntennaEndY,
|
||||||
|
);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 绘制触角末端的小圆点
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(leftAntennaEndX, leftAntennaEndY, 2, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(rightAntennaEndX, rightAntennaEndY, 2, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// 绘制眼睛
|
||||||
|
const eyeSize = headRadius * 0.15;
|
||||||
|
const eyeOffsetX = headRadius * 0.25;
|
||||||
|
const eyeOffsetY = headRadius * 0.22;
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(
|
||||||
|
headCenterX - eyeOffsetX - 2,
|
||||||
|
headCenterY - eyeOffsetY,
|
||||||
|
eyeSize,
|
||||||
|
0,
|
||||||
|
Math.PI * 2,
|
||||||
|
);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(
|
||||||
|
headCenterX + eyeOffsetX - 2,
|
||||||
|
headCenterY - eyeOffsetY,
|
||||||
|
eyeSize,
|
||||||
|
0,
|
||||||
|
Math.PI * 2,
|
||||||
|
);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// 绘制眼珠
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(
|
||||||
|
headCenterX - eyeOffsetX - 2,
|
||||||
|
headCenterY - eyeOffsetY,
|
||||||
|
eyeSize * 0.6,
|
||||||
|
0,
|
||||||
|
Math.PI * 2,
|
||||||
|
);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(
|
||||||
|
headCenterX + eyeOffsetX - 2,
|
||||||
|
headCenterY - eyeOffsetY,
|
||||||
|
eyeSize * 0.6,
|
||||||
|
0,
|
||||||
|
Math.PI * 2,
|
||||||
|
);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// 绘制嘴巴(微笑)
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(
|
||||||
|
headCenterX - 2,
|
||||||
|
headCenterY + headRadius * 0.22,
|
||||||
|
headRadius * 0.3,
|
||||||
|
0,
|
||||||
|
Math.PI,
|
||||||
|
);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制毛毛虫身体(带弧度的圆圈)
|
||||||
|
*/
|
||||||
|
private drawCaterpillarBody(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
startX: number,
|
||||||
|
startY: number,
|
||||||
|
count: number,
|
||||||
|
bodyRadius: number,
|
||||||
|
color: string,
|
||||||
|
) {
|
||||||
|
const spacing = bodyRadius * 2; // 身体圆圈之间的间距(稍微重叠)
|
||||||
|
const waveAmplitude = bodyRadius * 0.4; // 波浪幅度
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
// 计算每个身体圆圈的位置(带弧度,形成弯曲效果)
|
||||||
|
const baseX = startX + i * spacing;
|
||||||
|
// 使用正弦函数创建波浪效果,使身体有弧度
|
||||||
|
const waveOffset = Math.sin((i * Math.PI) / 2.5) * waveAmplitude;
|
||||||
|
const bodyX = baseX;
|
||||||
|
const bodyY = startY + waveOffset;
|
||||||
|
|
||||||
|
// 绘制身体圆圈
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(bodyX, bodyY, bodyRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// 绘制边框(可选,根据设计需求)
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制圆角矩形(虚线边框)
|
||||||
|
*/
|
||||||
|
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, 6]); // 虚线
|
||||||
|
} else {
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]); // 重置为实线
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NumberColorDraw;
|
export default NumberColorDraw;
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
/**
|
|
||||||
* 绘制内容区域的参数接口
|
|
||||||
*/
|
|
||||||
interface DrawNumberContentParams {
|
|
||||||
ctx: RenderingContext;
|
|
||||||
selectedNumber: number;
|
|
||||||
canvasWidth: number; // 逻辑像素宽度(已除以3)
|
|
||||||
startY: number; // 起始Y坐标
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制数字涂色内容区域服务
|
|
||||||
* 随机绘制1-10的数字,尺寸除以3
|
|
||||||
*/
|
|
||||||
export function drawNumberContent({
|
|
||||||
ctx,
|
|
||||||
selectedNumber,
|
|
||||||
canvasWidth,
|
|
||||||
startY,
|
|
||||||
}: DrawNumberContentParams): void {
|
|
||||||
const rows = 6;
|
|
||||||
const radius = 26; // 约26.67
|
|
||||||
const fontSize = 24; // 24
|
|
||||||
|
|
||||||
const startYPos = startY + 10 + radius; // 约47.33
|
|
||||||
const startX1 = 100 + radius; // 约112.22
|
|
||||||
const startX2 = 60 + radius; // 约75.56
|
|
||||||
const spaceWidthFirst = (canvasWidth - startX1 * 2) / (5 - 1);
|
|
||||||
const spaceWidthSecond = (canvasWidth - startX2 * 2) / (6 - 1);
|
|
||||||
const spaceHeight = 18; // 18
|
|
||||||
|
|
||||||
ctx.moveTo(startX1, startYPos);
|
|
||||||
for (let i = 0; i < rows; i++) {
|
|
||||||
const cols = i % 2 === 0 ? 5 : 6;
|
|
||||||
|
|
||||||
for (let j = 0; j < cols; j++) {
|
|
||||||
const y = startYPos + i * (spaceHeight + radius * 2);
|
|
||||||
const x =
|
|
||||||
i % 2 === 0
|
|
||||||
? startX1 + j * spaceWidthFirst
|
|
||||||
: startX2 + j * spaceWidthSecond;
|
|
||||||
|
|
||||||
// 随机生成1-10的数字,但确保包含选中的数字
|
|
||||||
let number: number;
|
|
||||||
if (i === 0 && j === 0) {
|
|
||||||
// 第一个位置固定为选中的数字
|
|
||||||
number = selectedNumber;
|
|
||||||
} else {
|
|
||||||
// 其他位置随机,但增加选中数字出现的概率
|
|
||||||
const random = Math.random();
|
|
||||||
if (random < 0.3) {
|
|
||||||
// 30%概率是选中的数字
|
|
||||||
number = selectedNumber;
|
|
||||||
} else {
|
|
||||||
// 70%概率是其他随机数字
|
|
||||||
number = Math.floor(Math.random() * 10) + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制圆形背景(线宽除以3)
|
|
||||||
ctx.fillStyle = '#fff';
|
|
||||||
ctx.strokeStyle = '#000';
|
|
||||||
ctx.lineWidth = 1; // 约1.33
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.closePath();
|
|
||||||
|
|
||||||
// 绘制数字(字体大小除以3)
|
|
||||||
ctx.fillStyle = '#333';
|
|
||||||
ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText(String(number), x, y, radius * 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,504 +0,0 @@
|
|||||||
import { getImage } from '../../../utils/index';
|
|
||||||
import { getRandomNumberColor } from '../../../constants/colors';
|
|
||||||
|
|
||||||
interface DrawNumberDecomposeContentParams {
|
|
||||||
canvas: WechatMiniprogram.Canvas;
|
|
||||||
ctx: RenderingContext;
|
|
||||||
decomposeData: {
|
|
||||||
problems: Array<{
|
|
||||||
whole: number | null;
|
|
||||||
part1: number | null;
|
|
||||||
part2: number | null;
|
|
||||||
imageIndex?: number;
|
|
||||||
imageType?: 'fruits' | 'twelve-animals';
|
|
||||||
}>;
|
|
||||||
mode: 'with-image' | 'decompose' | 'compose';
|
|
||||||
};
|
|
||||||
canvasWidth: number;
|
|
||||||
startY: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制圆角矩形(虚线边框)
|
|
||||||
*/
|
|
||||||
function drawRoundedRect(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
radius: number,
|
|
||||||
isDashed: boolean = false,
|
|
||||||
) {
|
|
||||||
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 async function drawNumberDecomposeContent({
|
|
||||||
canvas,
|
|
||||||
ctx,
|
|
||||||
decomposeData,
|
|
||||||
canvasWidth,
|
|
||||||
startY,
|
|
||||||
}: DrawNumberDecomposeContentParams): Promise<void> {
|
|
||||||
const { problems, mode } = decomposeData;
|
|
||||||
|
|
||||||
if (mode === 'with-image') {
|
|
||||||
await drawWithImageMode(canvas, ctx, problems, canvasWidth, startY);
|
|
||||||
} else if (mode === 'decompose') {
|
|
||||||
await drawDecomposeOrComposeMode(
|
|
||||||
ctx,
|
|
||||||
problems,
|
|
||||||
canvasWidth,
|
|
||||||
startY,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
} else if (mode === 'compose') {
|
|
||||||
await drawDecomposeOrComposeMode(
|
|
||||||
ctx,
|
|
||||||
problems,
|
|
||||||
canvasWidth,
|
|
||||||
startY,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 有图片模式:一行三列,总共三行(9个题目)
|
|
||||||
*/
|
|
||||||
async function drawWithImageMode(
|
|
||||||
canvas: WechatMiniprogram.Canvas,
|
|
||||||
ctx: RenderingContext,
|
|
||||||
problems: Array<{
|
|
||||||
whole: number | null;
|
|
||||||
part1: number | null;
|
|
||||||
part2: number | null;
|
|
||||||
imageIndex?: number;
|
|
||||||
imageType?: 'fruits' | 'twelve-animals';
|
|
||||||
}>,
|
|
||||||
canvasWidth: number,
|
|
||||||
startY: number,
|
|
||||||
) {
|
|
||||||
const leftMargin = 30;
|
|
||||||
const rightMargin = 30;
|
|
||||||
const topMargin = 20;
|
|
||||||
const boxSpacing = 20;
|
|
||||||
const rowSpacing = 32;
|
|
||||||
|
|
||||||
// 计算每个框的尺寸
|
|
||||||
const availableWidth = canvasWidth - leftMargin - rightMargin;
|
|
||||||
const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
|
|
||||||
const imageAreaHeight = 120; // 图片区域高度
|
|
||||||
const treeAreaHeight = 80; // 二叉树区域高度
|
|
||||||
|
|
||||||
let currentY = startY + topMargin;
|
|
||||||
|
|
||||||
// 绘制3列3行
|
|
||||||
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 * (imageAreaHeight + treeAreaHeight + rowSpacing);
|
|
||||||
|
|
||||||
// 绘制一个完整的题目框
|
|
||||||
await drawWithImageProblem(
|
|
||||||
ctx,
|
|
||||||
canvas,
|
|
||||||
problem,
|
|
||||||
boxX,
|
|
||||||
boxY,
|
|
||||||
boxWidth,
|
|
||||||
imageAreaHeight,
|
|
||||||
treeAreaHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分模式/组合模式:一行3个,总共5行(15个题目)
|
|
||||||
* @param isInverted true 为组合模式(倒置二叉树),false 为分模式(正常二叉树)
|
|
||||||
*/
|
|
||||||
async function drawDecomposeOrComposeMode(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
problems: Array<{
|
|
||||||
whole: number | null;
|
|
||||||
part1: number | null;
|
|
||||||
part2: number | null;
|
|
||||||
}>,
|
|
||||||
canvasWidth: number,
|
|
||||||
startY: number,
|
|
||||||
isInverted: boolean,
|
|
||||||
) {
|
|
||||||
const leftMargin = 30;
|
|
||||||
const rightMargin = 30;
|
|
||||||
const topMargin = 20;
|
|
||||||
const boxSpacing = 15;
|
|
||||||
const rowSpacing = 140;
|
|
||||||
|
|
||||||
// 计算每个框的尺寸
|
|
||||||
const availableWidth = canvasWidth - leftMargin - rightMargin;
|
|
||||||
const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
|
|
||||||
const boxHeight = 80; // 二叉树区域高度
|
|
||||||
|
|
||||||
let currentY = startY + topMargin;
|
|
||||||
|
|
||||||
// 绘制3列5行
|
|
||||||
for (let row = 0; row < 5; 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 * rowSpacing;
|
|
||||||
|
|
||||||
// 绘制二叉树
|
|
||||||
drawTree(ctx, problem, boxX, boxY, boxWidth, boxHeight, isInverted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制有图片模式的单个题目
|
|
||||||
*/
|
|
||||||
async function drawWithImageProblem(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
canvas: WechatMiniprogram.Canvas,
|
|
||||||
problem: {
|
|
||||||
whole: number | null;
|
|
||||||
part1: number | null;
|
|
||||||
part2: number | null;
|
|
||||||
imageIndex?: number;
|
|
||||||
imageType?: 'fruits' | 'twelve-animals';
|
|
||||||
},
|
|
||||||
boxX: number,
|
|
||||||
boxY: number,
|
|
||||||
boxWidth: number,
|
|
||||||
imageAreaHeight: number,
|
|
||||||
treeAreaHeight: number,
|
|
||||||
) {
|
|
||||||
const borderRadius = 12;
|
|
||||||
|
|
||||||
// 绘制实线框(只绘制图片区域的框)
|
|
||||||
ctx.strokeStyle = '#333';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
drawRoundedRect(
|
|
||||||
ctx,
|
|
||||||
boxX,
|
|
||||||
boxY,
|
|
||||||
boxWidth,
|
|
||||||
imageAreaHeight,
|
|
||||||
borderRadius,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 图片区域(增加底部间距,避免与二叉树根节点重叠)
|
|
||||||
const imageBottomPadding = 20; // 图片区域底部额外间距
|
|
||||||
const effectiveImageHeight = imageAreaHeight - imageBottomPadding;
|
|
||||||
|
|
||||||
if (problem.imageIndex && problem.imageType && problem.whole) {
|
|
||||||
await drawImagesInBox(
|
|
||||||
ctx,
|
|
||||||
canvas,
|
|
||||||
problem.whole,
|
|
||||||
problem.imageIndex,
|
|
||||||
problem.imageType,
|
|
||||||
boxX,
|
|
||||||
boxY,
|
|
||||||
boxWidth,
|
|
||||||
effectiveImageHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 二叉树区域(增加与图片区域的间距)
|
|
||||||
const treeTopSpacing = -5; // 二叉树区域顶部间距
|
|
||||||
const treeY = boxY + imageAreaHeight + treeTopSpacing;
|
|
||||||
drawTree(
|
|
||||||
ctx,
|
|
||||||
problem,
|
|
||||||
boxX,
|
|
||||||
treeY,
|
|
||||||
boxWidth,
|
|
||||||
treeAreaHeight,
|
|
||||||
false, // 不是倒置
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制二叉树结构
|
|
||||||
* @param isInverted 是否倒置(组合模式为true)
|
|
||||||
*/
|
|
||||||
function drawTree(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
problem: {
|
|
||||||
whole: number | null;
|
|
||||||
part1: number | null;
|
|
||||||
part2: number | null;
|
|
||||||
},
|
|
||||||
boxX: number,
|
|
||||||
boxY: number,
|
|
||||||
boxWidth: number,
|
|
||||||
boxHeight: number,
|
|
||||||
isInverted: boolean,
|
|
||||||
) {
|
|
||||||
const nodeRadius = 18;
|
|
||||||
const nodeSpacing = 40; // 节点之间的水平间距
|
|
||||||
const verticalSpacing = 70; // 根节点和子节点的垂直间距
|
|
||||||
|
|
||||||
if (isInverted) {
|
|
||||||
// 组合模式:倒置二叉树
|
|
||||||
// 顶部:两个子节点(part1, part2)
|
|
||||||
// 底部:根节点(whole,需要填写)
|
|
||||||
|
|
||||||
const centerX = boxX + boxWidth / 2;
|
|
||||||
const topY = boxY + boxHeight / 2 - verticalSpacing / 2;
|
|
||||||
const bottomY = boxY + boxHeight / 2 + verticalSpacing / 2;
|
|
||||||
|
|
||||||
// 绘制两个子节点(顶部)
|
|
||||||
const leftNodeX = centerX - nodeSpacing / 2 - nodeRadius;
|
|
||||||
const rightNodeX = centerX + nodeSpacing / 2 + nodeRadius;
|
|
||||||
|
|
||||||
// 左子节点
|
|
||||||
drawNode(ctx, leftNodeX, topY, nodeRadius, problem.part1);
|
|
||||||
// 右子节点
|
|
||||||
drawNode(ctx, rightNodeX, topY, nodeRadius, problem.part2);
|
|
||||||
|
|
||||||
// 绘制根节点(底部)
|
|
||||||
drawNode(ctx, centerX, bottomY, nodeRadius, problem.whole);
|
|
||||||
|
|
||||||
// 绘制连接线(从子节点到根节点)
|
|
||||||
ctx.strokeStyle = '#333';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(leftNodeX, topY + nodeRadius);
|
|
||||||
ctx.lineTo(centerX, bottomY - nodeRadius);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(rightNodeX, topY + nodeRadius);
|
|
||||||
ctx.lineTo(centerX, bottomY - nodeRadius);
|
|
||||||
ctx.stroke();
|
|
||||||
} else {
|
|
||||||
// 分解模式:正常二叉树
|
|
||||||
// 顶部:根节点(whole)
|
|
||||||
// 底部:两个子节点(part1, part2,其中一个需要填写)
|
|
||||||
|
|
||||||
const centerX = boxX + boxWidth / 2;
|
|
||||||
const topY = boxY + boxHeight / 2 - verticalSpacing / 2;
|
|
||||||
const bottomY = boxY + boxHeight / 2 + verticalSpacing / 2;
|
|
||||||
|
|
||||||
// 绘制根节点(顶部)
|
|
||||||
drawNode(ctx, centerX, topY, nodeRadius, problem.whole);
|
|
||||||
|
|
||||||
// 绘制两个子节点(底部)
|
|
||||||
const leftNodeX = centerX - nodeSpacing / 2 - nodeRadius;
|
|
||||||
const rightNodeX = centerX + nodeSpacing / 2 + nodeRadius;
|
|
||||||
|
|
||||||
// 左子节点
|
|
||||||
drawNode(ctx, leftNodeX, bottomY, nodeRadius, problem.part1);
|
|
||||||
// 右子节点
|
|
||||||
drawNode(ctx, rightNodeX, bottomY, nodeRadius, problem.part2);
|
|
||||||
|
|
||||||
// 绘制连接线(从根节点到子节点)
|
|
||||||
ctx.strokeStyle = '#333';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(centerX, topY + nodeRadius);
|
|
||||||
ctx.lineTo(leftNodeX, bottomY - nodeRadius);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(centerX, topY + nodeRadius);
|
|
||||||
ctx.lineTo(rightNodeX, bottomY - nodeRadius);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制节点(圆圈和数字)
|
|
||||||
*/
|
|
||||||
function drawNode(
|
|
||||||
ctx: RenderingContext,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
radius: number,
|
|
||||||
value: number | null,
|
|
||||||
) {
|
|
||||||
// 绘制圆角正方形
|
|
||||||
const size = radius * 2; // 正方形边长
|
|
||||||
const cornerRadius = 8; // 圆角半径
|
|
||||||
const halfSize = size / 2;
|
|
||||||
|
|
||||||
ctx.fillStyle = '#fff';
|
|
||||||
ctx.strokeStyle = '#333';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
|
|
||||||
// 绘制圆角矩形
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x - halfSize + cornerRadius, y - halfSize);
|
|
||||||
ctx.lineTo(x + halfSize - cornerRadius, y - halfSize);
|
|
||||||
ctx.quadraticCurveTo(
|
|
||||||
x + halfSize,
|
|
||||||
y - halfSize,
|
|
||||||
x + halfSize,
|
|
||||||
y - halfSize + cornerRadius,
|
|
||||||
);
|
|
||||||
ctx.lineTo(x + halfSize, y + halfSize - cornerRadius);
|
|
||||||
ctx.quadraticCurveTo(
|
|
||||||
x + halfSize,
|
|
||||||
y + halfSize,
|
|
||||||
x + halfSize - cornerRadius,
|
|
||||||
y + halfSize,
|
|
||||||
);
|
|
||||||
ctx.lineTo(x - halfSize + cornerRadius, y + halfSize);
|
|
||||||
ctx.quadraticCurveTo(
|
|
||||||
x - halfSize,
|
|
||||||
y + halfSize,
|
|
||||||
x - halfSize,
|
|
||||||
y + halfSize - cornerRadius,
|
|
||||||
);
|
|
||||||
ctx.lineTo(x - halfSize, y - halfSize + cornerRadius);
|
|
||||||
ctx.quadraticCurveTo(
|
|
||||||
x - halfSize,
|
|
||||||
y - halfSize,
|
|
||||||
x - halfSize + cornerRadius,
|
|
||||||
y - halfSize,
|
|
||||||
);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 如果有值,绘制数字
|
|
||||||
if (value !== null) {
|
|
||||||
ctx.fillStyle = getRandomNumberColor();
|
|
||||||
ctx.font = `bold ${24}px "Microsoft Yahei"`;
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText(String(value), x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在框内绘制多张图片
|
|
||||||
*/
|
|
||||||
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 horizontalPadding = 5;
|
|
||||||
const verticalPadding = 10;
|
|
||||||
const availableWidth = boxWidth - horizontalPadding * 2;
|
|
||||||
const availableHeight = boxHeight - verticalPadding * 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 +
|
|
||||||
horizontalPadding +
|
|
||||||
imageSpacing +
|
|
||||||
col * (imageSize + imageSpacing);
|
|
||||||
const imageY =
|
|
||||||
boxY +
|
|
||||||
verticalPadding +
|
|
||||||
// 这两段代码是用于计算每一张图片在Y轴方向上的实际位置(imageY 变量)
|
|
||||||
// 第一行:如果有多于一行,行间用 rowSpacing,否则单行时图片整体垂直居中排列。
|
|
||||||
(rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) +
|
|
||||||
// 第二行:加上具体第 row 行的纵向偏移(每张图片的高度加上行间距,再乘以行号)。
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 10以内数的分与合绘制服务
|
||||||
|
*/
|
||||||
|
|
||||||
import { BaseDrawService } from '../../../service/baseDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawNumberDecomposeContent } from './numberDecomposeContentDraw';
|
import { getImage } from '../../../utils/index';
|
||||||
|
import { getRandomNumberColor } from '../../../constants/colors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 10以内数的分与合数据
|
||||||
|
*/
|
||||||
|
export interface NumberDecomposeData {
|
||||||
|
problems: Array<{
|
||||||
|
whole: number | null;
|
||||||
|
part1: number | null;
|
||||||
|
part2: number | null;
|
||||||
|
imageIndex?: number;
|
||||||
|
imageType?: 'fruits' | 'twelve-animals';
|
||||||
|
}>;
|
||||||
|
mode: 'with-image' | 'decompose' | 'compose';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 10以内数的分与合绘制服务
|
* 10以内数的分与合绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
|
||||||
*/
|
*/
|
||||||
class NumberDecomposeDraw extends BaseDrawService {
|
class NumberDecomposeDraw extends BaseDrawService {
|
||||||
decomposeData: {
|
decomposeData: NumberDecomposeData | null = null;
|
||||||
problems: Array<{
|
|
||||||
whole: number | null;
|
|
||||||
part1: number | null;
|
|
||||||
part2: number | null;
|
|
||||||
imageIndex?: number;
|
|
||||||
imageType?: 'fruits' | 'twelve-animals';
|
|
||||||
}>;
|
|
||||||
mode: 'with-image' | 'decompose' | 'compose';
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
@@ -26,35 +35,18 @@ class NumberDecomposeDraw extends BaseDrawService {
|
|||||||
this.decomposeData = null;
|
this.decomposeData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async draw(decomposeData: {
|
/**
|
||||||
problems: Array<{
|
* 绘制10以内数的分与合内容
|
||||||
whole: number | null;
|
*/
|
||||||
part1: number | null;
|
async draw(decomposeData: NumberDecomposeData) {
|
||||||
part2: number | null;
|
|
||||||
imageIndex?: number;
|
|
||||||
imageType?: 'fruits' | 'twelve-animals';
|
|
||||||
}>;
|
|
||||||
mode: 'with-image' | 'decompose' | 'compose';
|
|
||||||
}) {
|
|
||||||
if (!decomposeData || !decomposeData.problems) {
|
if (!decomposeData || !decomposeData.problems) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.decomposeData = decomposeData;
|
this.decomposeData = decomposeData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
await this.drawContent({
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域
|
|
||||||
this.drawDivider();
|
|
||||||
await drawNumberDecomposeContent({
|
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
decomposeData: this.decomposeData,
|
decomposeData: this.decomposeData,
|
||||||
@@ -62,6 +54,519 @@ class NumberDecomposeDraw extends BaseDrawService {
|
|||||||
startY: this.currentY,
|
startY: this.currentY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制内容区域
|
||||||
|
*/
|
||||||
|
private async drawContent(params: {
|
||||||
|
canvas: WechatMiniprogram.Canvas;
|
||||||
|
ctx: RenderingContext;
|
||||||
|
decomposeData: NumberDecomposeData;
|
||||||
|
canvasWidth: number;
|
||||||
|
startY: number;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { canvas, ctx, decomposeData, canvasWidth, startY } = params;
|
||||||
|
const { problems, mode } = decomposeData;
|
||||||
|
|
||||||
|
if (mode === 'with-image') {
|
||||||
|
await this.drawWithImageMode(
|
||||||
|
canvas,
|
||||||
|
ctx,
|
||||||
|
problems,
|
||||||
|
canvasWidth,
|
||||||
|
startY,
|
||||||
|
);
|
||||||
|
} else if (mode === 'decompose') {
|
||||||
|
await this.drawDecomposeOrComposeMode(
|
||||||
|
ctx,
|
||||||
|
problems,
|
||||||
|
canvasWidth,
|
||||||
|
startY,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
} else if (mode === 'compose') {
|
||||||
|
await this.drawDecomposeOrComposeMode(
|
||||||
|
ctx,
|
||||||
|
problems,
|
||||||
|
canvasWidth,
|
||||||
|
startY,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有图片模式:一行三列,总共三行(9个题目)
|
||||||
|
*/
|
||||||
|
private async drawWithImageMode(
|
||||||
|
canvas: WechatMiniprogram.Canvas,
|
||||||
|
ctx: RenderingContext,
|
||||||
|
problems: Array<{
|
||||||
|
whole: number | null;
|
||||||
|
part1: number | null;
|
||||||
|
part2: number | null;
|
||||||
|
imageIndex?: number;
|
||||||
|
imageType?: 'fruits' | 'twelve-animals';
|
||||||
|
}>,
|
||||||
|
canvasWidth: number,
|
||||||
|
startY: number,
|
||||||
|
) {
|
||||||
|
const leftMargin = 30;
|
||||||
|
const rightMargin = 30;
|
||||||
|
const topMargin = 20;
|
||||||
|
const boxSpacing = 20;
|
||||||
|
const rowSpacing = 32;
|
||||||
|
|
||||||
|
// 计算每个框的尺寸
|
||||||
|
const availableWidth = canvasWidth - leftMargin - rightMargin;
|
||||||
|
const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
|
||||||
|
const imageAreaHeight = 120; // 图片区域高度
|
||||||
|
const treeAreaHeight = 80; // 二叉树区域高度
|
||||||
|
|
||||||
|
let currentY = startY + topMargin;
|
||||||
|
|
||||||
|
// 绘制3列3行
|
||||||
|
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 * (imageAreaHeight + treeAreaHeight + rowSpacing);
|
||||||
|
|
||||||
|
// 绘制一个完整的题目框
|
||||||
|
await this.drawWithImageProblem(
|
||||||
|
ctx,
|
||||||
|
canvas,
|
||||||
|
problem,
|
||||||
|
boxX,
|
||||||
|
boxY,
|
||||||
|
boxWidth,
|
||||||
|
imageAreaHeight,
|
||||||
|
treeAreaHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分模式/组合模式:一行3个,总共5行(15个题目)
|
||||||
|
* @param isInverted true 为组合模式(倒置二叉树),false 为分模式(正常二叉树)
|
||||||
|
*/
|
||||||
|
private async drawDecomposeOrComposeMode(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
problems: Array<{
|
||||||
|
whole: number | null;
|
||||||
|
part1: number | null;
|
||||||
|
part2: number | null;
|
||||||
|
}>,
|
||||||
|
canvasWidth: number,
|
||||||
|
startY: number,
|
||||||
|
isInverted: boolean,
|
||||||
|
) {
|
||||||
|
const leftMargin = 30;
|
||||||
|
const rightMargin = 30;
|
||||||
|
const topMargin = 20;
|
||||||
|
const boxSpacing = 15;
|
||||||
|
const rowSpacing = 140;
|
||||||
|
|
||||||
|
// 计算每个框的尺寸
|
||||||
|
const availableWidth = canvasWidth - leftMargin - rightMargin;
|
||||||
|
const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
|
||||||
|
const boxHeight = 80; // 二叉树区域高度
|
||||||
|
|
||||||
|
let currentY = startY + topMargin;
|
||||||
|
|
||||||
|
// 绘制3列5行
|
||||||
|
for (let row = 0; row < 5; 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 * rowSpacing;
|
||||||
|
|
||||||
|
// 绘制二叉树
|
||||||
|
this.drawTree(
|
||||||
|
ctx,
|
||||||
|
problem,
|
||||||
|
boxX,
|
||||||
|
boxY,
|
||||||
|
boxWidth,
|
||||||
|
boxHeight,
|
||||||
|
isInverted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制有图片模式的单个题目
|
||||||
|
*/
|
||||||
|
private async drawWithImageProblem(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
canvas: WechatMiniprogram.Canvas,
|
||||||
|
problem: {
|
||||||
|
whole: number | null;
|
||||||
|
part1: number | null;
|
||||||
|
part2: number | null;
|
||||||
|
imageIndex?: number;
|
||||||
|
imageType?: 'fruits' | 'twelve-animals';
|
||||||
|
},
|
||||||
|
boxX: number,
|
||||||
|
boxY: number,
|
||||||
|
boxWidth: number,
|
||||||
|
imageAreaHeight: number,
|
||||||
|
treeAreaHeight: number,
|
||||||
|
) {
|
||||||
|
const borderRadius = 12;
|
||||||
|
|
||||||
|
// 绘制实线框(只绘制图片区域的框)
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
this.drawRoundedRectInternal(
|
||||||
|
ctx,
|
||||||
|
boxX,
|
||||||
|
boxY,
|
||||||
|
boxWidth,
|
||||||
|
imageAreaHeight,
|
||||||
|
borderRadius,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 图片区域(增加底部间距,避免与二叉树根节点重叠)
|
||||||
|
const imageBottomPadding = 20; // 图片区域底部额外间距
|
||||||
|
const effectiveImageHeight = imageAreaHeight - imageBottomPadding;
|
||||||
|
|
||||||
|
if (problem.imageIndex && problem.imageType && problem.whole) {
|
||||||
|
await this.drawImagesInBox(
|
||||||
|
ctx,
|
||||||
|
canvas,
|
||||||
|
problem.whole,
|
||||||
|
problem.imageIndex,
|
||||||
|
problem.imageType,
|
||||||
|
boxX,
|
||||||
|
boxY,
|
||||||
|
boxWidth,
|
||||||
|
effectiveImageHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二叉树区域(增加与图片区域的间距)
|
||||||
|
const treeTopSpacing = -5; // 二叉树区域顶部间距
|
||||||
|
const treeY = boxY + imageAreaHeight + treeTopSpacing;
|
||||||
|
this.drawTree(
|
||||||
|
ctx,
|
||||||
|
problem,
|
||||||
|
boxX,
|
||||||
|
treeY,
|
||||||
|
boxWidth,
|
||||||
|
treeAreaHeight,
|
||||||
|
false, // 不是倒置
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制二叉树结构
|
||||||
|
* @param isInverted 是否倒置(组合模式为true)
|
||||||
|
*/
|
||||||
|
private drawTree(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
problem: {
|
||||||
|
whole: number | null;
|
||||||
|
part1: number | null;
|
||||||
|
part2: number | null;
|
||||||
|
},
|
||||||
|
boxX: number,
|
||||||
|
boxY: number,
|
||||||
|
boxWidth: number,
|
||||||
|
boxHeight: number,
|
||||||
|
isInverted: boolean,
|
||||||
|
) {
|
||||||
|
const nodeRadius = 18;
|
||||||
|
const nodeSpacing = 40; // 节点之间的水平间距
|
||||||
|
const verticalSpacing = 70; // 根节点和子节点的垂直间距
|
||||||
|
|
||||||
|
if (isInverted) {
|
||||||
|
// 组合模式:倒置二叉树
|
||||||
|
// 顶部:两个子节点(part1, part2)
|
||||||
|
// 底部:根节点(whole,需要填写)
|
||||||
|
|
||||||
|
const centerX = boxX + boxWidth / 2;
|
||||||
|
const topY = boxY + boxHeight / 2 - verticalSpacing / 2;
|
||||||
|
const bottomY = boxY + boxHeight / 2 + verticalSpacing / 2;
|
||||||
|
|
||||||
|
// 绘制两个子节点(顶部)
|
||||||
|
const leftNodeX = centerX - nodeSpacing / 2 - nodeRadius;
|
||||||
|
const rightNodeX = centerX + nodeSpacing / 2 + nodeRadius;
|
||||||
|
|
||||||
|
// 左子节点
|
||||||
|
this.drawNode(ctx, leftNodeX, topY, nodeRadius, problem.part1);
|
||||||
|
// 右子节点
|
||||||
|
this.drawNode(ctx, rightNodeX, topY, nodeRadius, problem.part2);
|
||||||
|
|
||||||
|
// 绘制根节点(底部)
|
||||||
|
this.drawNode(ctx, centerX, bottomY, nodeRadius, problem.whole);
|
||||||
|
|
||||||
|
// 绘制连接线(从子节点到根节点)
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(leftNodeX, topY + nodeRadius);
|
||||||
|
ctx.lineTo(centerX, bottomY - nodeRadius);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(rightNodeX, topY + nodeRadius);
|
||||||
|
ctx.lineTo(centerX, bottomY - nodeRadius);
|
||||||
|
ctx.stroke();
|
||||||
|
} else {
|
||||||
|
// 分解模式:正常二叉树
|
||||||
|
// 顶部:根节点(whole)
|
||||||
|
// 底部:两个子节点(part1, part2,其中一个需要填写)
|
||||||
|
|
||||||
|
const centerX = boxX + boxWidth / 2;
|
||||||
|
const topY = boxY + boxHeight / 2 - verticalSpacing / 2;
|
||||||
|
const bottomY = boxY + boxHeight / 2 + verticalSpacing / 2;
|
||||||
|
|
||||||
|
// 绘制根节点(顶部)
|
||||||
|
this.drawNode(ctx, centerX, topY, nodeRadius, problem.whole);
|
||||||
|
|
||||||
|
// 绘制两个子节点(底部)
|
||||||
|
const leftNodeX = centerX - nodeSpacing / 2 - nodeRadius;
|
||||||
|
const rightNodeX = centerX + nodeSpacing / 2 + nodeRadius;
|
||||||
|
|
||||||
|
// 左子节点
|
||||||
|
this.drawNode(ctx, leftNodeX, bottomY, nodeRadius, problem.part1);
|
||||||
|
// 右子节点
|
||||||
|
this.drawNode(ctx, rightNodeX, bottomY, nodeRadius, problem.part2);
|
||||||
|
|
||||||
|
// 绘制连接线(从根节点到子节点)
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(centerX, topY + nodeRadius);
|
||||||
|
ctx.lineTo(leftNodeX, bottomY - nodeRadius);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(centerX, topY + nodeRadius);
|
||||||
|
ctx.lineTo(rightNodeX, bottomY - nodeRadius);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制节点(圆圈和数字)
|
||||||
|
*/
|
||||||
|
private drawNode(
|
||||||
|
ctx: RenderingContext,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
radius: number,
|
||||||
|
value: number | null,
|
||||||
|
) {
|
||||||
|
// 绘制圆角正方形
|
||||||
|
const size = radius * 2; // 正方形边长
|
||||||
|
const cornerRadius = 8; // 圆角半径
|
||||||
|
const halfSize = size / 2;
|
||||||
|
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
|
||||||
|
// 绘制圆角矩形
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x - halfSize + cornerRadius, y - halfSize);
|
||||||
|
ctx.lineTo(x + halfSize - cornerRadius, y - halfSize);
|
||||||
|
ctx.quadraticCurveTo(
|
||||||
|
x + halfSize,
|
||||||
|
y - halfSize,
|
||||||
|
x + halfSize,
|
||||||
|
y - halfSize + cornerRadius,
|
||||||
|
);
|
||||||
|
ctx.lineTo(x + halfSize, y + halfSize - cornerRadius);
|
||||||
|
ctx.quadraticCurveTo(
|
||||||
|
x + halfSize,
|
||||||
|
y + halfSize,
|
||||||
|
x + halfSize - cornerRadius,
|
||||||
|
y + halfSize,
|
||||||
|
);
|
||||||
|
ctx.lineTo(x - halfSize + cornerRadius, y + halfSize);
|
||||||
|
ctx.quadraticCurveTo(
|
||||||
|
x - halfSize,
|
||||||
|
y + halfSize,
|
||||||
|
x - halfSize,
|
||||||
|
y + halfSize - cornerRadius,
|
||||||
|
);
|
||||||
|
ctx.lineTo(x - halfSize, y - halfSize + cornerRadius);
|
||||||
|
ctx.quadraticCurveTo(
|
||||||
|
x - halfSize,
|
||||||
|
y - halfSize,
|
||||||
|
x - halfSize + cornerRadius,
|
||||||
|
y - halfSize,
|
||||||
|
);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 如果有值,绘制数字
|
||||||
|
if (value !== null) {
|
||||||
|
ctx.fillStyle = getRandomNumberColor();
|
||||||
|
ctx.font = `bold ${24}px "Microsoft Yahei"`;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(String(value), x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在框内绘制多张图片
|
||||||
|
*/
|
||||||
|
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 horizontalPadding = 5;
|
||||||
|
const verticalPadding = 10;
|
||||||
|
const availableWidth = boxWidth - horizontalPadding * 2;
|
||||||
|
const availableHeight = boxHeight - verticalPadding * 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 +
|
||||||
|
horizontalPadding +
|
||||||
|
imageSpacing +
|
||||||
|
col * (imageSize + imageSpacing);
|
||||||
|
const imageY =
|
||||||
|
boxY +
|
||||||
|
verticalPadding +
|
||||||
|
// 这两段代码是用于计算每一张图片在Y轴方向上的实际位置(imageY 变量)
|
||||||
|
// 第一行:如果有多于一行,行间用 rowSpacing,否则单行时图片整体垂直居中排列。
|
||||||
|
(rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) +
|
||||||
|
// 第二行:加上具体第 row 行的纵向偏移(每张图片的高度加上行间距,再乘以行号)。
|
||||||
|
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 = false,
|
||||||
|
) {
|
||||||
|
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 NumberDecomposeDraw;
|
export default NumberDecomposeDraw;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { BaseDrawService } from '../../../service/baseDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawNumberPreview } from './numberPreviewDraw';
|
import { drawNumberPreview } from './numberPreviewDraw';
|
||||||
import { drawNumberContent } from './numberContentDraw';
|
|
||||||
import { drawNumberWriteContent } from './numberWriteDraw';
|
import { drawNumberWriteContent } from './numberWriteDraw';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,20 +25,11 @@ class NumberFindDraw extends BaseDrawService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.selectedNumber = selectedNumber;
|
this.selectedNumber = selectedNumber;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制预览区域
|
// 绘制预览区域
|
||||||
this.drawDivider();
|
|
||||||
await drawNumberPreview({
|
await drawNumberPreview({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
@@ -63,7 +53,7 @@ class NumberFindDraw extends BaseDrawService {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 默认类型(number-find):绘制数字涂色内容
|
// 默认类型(number-find):绘制数字涂色内容
|
||||||
drawNumberContent({
|
this.drawNumberContent({
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
selectedNumber: this.selectedNumber,
|
selectedNumber: this.selectedNumber,
|
||||||
canvasWidth: this.canvasWidth,
|
canvasWidth: this.canvasWidth,
|
||||||
@@ -71,6 +61,75 @@ class NumberFindDraw extends BaseDrawService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制数字涂色内容区域
|
||||||
|
*/
|
||||||
|
private drawNumberContent(params: {
|
||||||
|
ctx: RenderingContext;
|
||||||
|
selectedNumber: number;
|
||||||
|
canvasWidth: number;
|
||||||
|
startY: number;
|
||||||
|
}): void {
|
||||||
|
const { ctx, selectedNumber, canvasWidth, startY } = params;
|
||||||
|
const rows = 6;
|
||||||
|
const radius = 26; // 约26.67
|
||||||
|
const fontSize = 24; // 24
|
||||||
|
|
||||||
|
const startYPos = startY + 10 + radius; // 约47.33
|
||||||
|
const startX1 = 100 + radius; // 约112.22
|
||||||
|
const startX2 = 60 + radius; // 约75.56
|
||||||
|
const spaceWidthFirst = (canvasWidth - startX1 * 2) / (5 - 1);
|
||||||
|
const spaceWidthSecond = (canvasWidth - startX2 * 2) / (6 - 1);
|
||||||
|
const spaceHeight = 18; // 18
|
||||||
|
|
||||||
|
ctx.moveTo(startX1, startYPos);
|
||||||
|
for (let i = 0; i < rows; i++) {
|
||||||
|
const cols = i % 2 === 0 ? 5 : 6;
|
||||||
|
|
||||||
|
for (let j = 0; j < cols; j++) {
|
||||||
|
const y = startYPos + i * (spaceHeight + radius * 2);
|
||||||
|
const x =
|
||||||
|
i % 2 === 0
|
||||||
|
? startX1 + j * spaceWidthFirst
|
||||||
|
: startX2 + j * spaceWidthSecond;
|
||||||
|
|
||||||
|
// 随机生成1-10的数字,但确保包含选中的数字
|
||||||
|
let number: number;
|
||||||
|
if (i === 0 && j === 0) {
|
||||||
|
// 第一个位置固定为选中的数字
|
||||||
|
number = selectedNumber;
|
||||||
|
} else {
|
||||||
|
// 其他位置随机,但增加选中数字出现的概率
|
||||||
|
const random = Math.random();
|
||||||
|
if (random < 0.3) {
|
||||||
|
// 30%概率是选中的数字
|
||||||
|
number = selectedNumber;
|
||||||
|
} else {
|
||||||
|
// 70%概率是其他随机数字
|
||||||
|
number = Math.floor(Math.random() * 10) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制圆形背景(线宽除以3)
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 1; // 约1.33
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
// 绘制数字(字体大小除以3)
|
||||||
|
ctx.fillStyle = '#333';
|
||||||
|
ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(String(number), x, y, radius * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NumberFindDraw;
|
export default NumberFindDraw;
|
||||||
|
|||||||
@@ -41,20 +41,9 @@ class NumberSortDraw extends BaseDrawService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPrintConfig();
|
|
||||||
this.sortData = sortData;
|
this.sortData = sortData;
|
||||||
this.clear();
|
this.prepareDraw();
|
||||||
this.setPaper();
|
await this.drawHeaderAndDivider();
|
||||||
|
|
||||||
// 绘制Header
|
|
||||||
if (this.headerType !== 'minimal') {
|
|
||||||
await this.drawHeader();
|
|
||||||
} else {
|
|
||||||
this.drawMiniHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制内容区域
|
|
||||||
this.drawDivider();
|
|
||||||
this.drawContent({
|
this.drawContent({
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
sortData: this.sortData,
|
sortData: this.sortData,
|
||||||
|
|||||||
@@ -397,4 +397,31 @@ export class BaseDrawService {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备绘制(公共初始化逻辑)
|
||||||
|
* 执行:setPrintConfig -> clear -> setPaper
|
||||||
|
* 子类可以在调用此方法前后执行自定义逻辑(如数据验证、异步资源加载等)
|
||||||
|
*/
|
||||||
|
prepareDraw() {
|
||||||
|
this.setPrintConfig();
|
||||||
|
this.clear();
|
||||||
|
this.setPaper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制 Header 和 Divider(公共绘制逻辑)
|
||||||
|
* 根据 headerType 自动选择绘制完整 Header 或迷你 Header,然后绘制分割线
|
||||||
|
*/
|
||||||
|
async drawHeaderAndDivider() {
|
||||||
|
// 绘制Header
|
||||||
|
if (this.headerType !== 'minimal') {
|
||||||
|
await this.drawHeader();
|
||||||
|
} else {
|
||||||
|
this.drawMiniHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制内容区域分割线
|
||||||
|
this.drawDivider();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user