feat:2.6.1增加 数物对应 和 根据颜色画图形

This commit is contained in:
R524809
2025-12-23 13:35:19 +08:00
parent 9b6a0a26cc
commit 20496c3cc8
28 changed files with 940 additions and 277 deletions
@@ -0,0 +1,265 @@
/**
* 数物对应绘制服务
*/
import { BaseDrawService } from '../../../service/baseDraw';
import { getImage } from '../../../utils/index';
/**
* 数物对应数据
*/
export interface NumberObjectMatchData {
/** 上面区域的图片配置(4行4列,共16张) */
gridImages: Array<{
imageIndex: number;
folder: 'fruits' | 'twelve-animals';
offsetX: number; // 随机左右偏移
offsetY: number; // 随机上下偏移
}>;
/** 下面区域第一行的4张图片配置 */
bottomImages: Array<{
imageIndex: number;
folder: 'fruits' | 'twelve-animals';
}>;
/** 第二行要显示的数量(乱序排列) */
bottomNumbers: number[];
}
/**
* 数物对应绘制服务
*/
class NumberObjectMatchDraw extends BaseDrawService {
matchData: NumberObjectMatchData | null = null;
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
this.matchData = null;
}
/**
* 绘制数物对应内容
*/
async draw(matchData: NumberObjectMatchData) {
if (
!matchData ||
!matchData.gridImages ||
!matchData.bottomImages ||
!matchData.bottomNumbers
) {
return;
}
this.matchData = matchData;
this.prepareDraw();
await this.drawHeaderAndDivider();
await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
matchData: this.matchData,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
/**
* 绘制内容区域
*/
private async drawContent(params: {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
matchData: NumberObjectMatchData;
canvasWidth: number;
startY: number;
}): Promise<void> {
const { canvas, ctx, matchData, canvasWidth } = params;
let { startY } = params;
startY = startY + 25;
const margin = 0; // 左右边距
const imagePadding = 10; // 图片内边距
const imageSize = 75; // 图片大小
const cellSize = imageSize + imagePadding * 2; // 每个格子的大小
// ========== 绘制上面区域(4行4列) ==========
const gridCols = 5;
const gridRows = 4;
const gridWidth = cellSize * gridCols;
const gridStartX = margin + (canvasWidth - margin * 2 - gridWidth) / 2; // 居中
const gridStartY = startY;
// 加载并绘制所有图片
const imagePromises = matchData.gridImages.map(async (item, index) => {
const row = Math.floor(index / gridCols);
const col = index % gridCols;
const baseX = gridStartX + col * cellSize + cellSize / 2;
const baseY = gridStartY + row * cellSize + cellSize / 2;
// 加上随机偏移
const imageX = baseX + item.offsetX;
const imageY = baseY + item.offsetY;
// 加载图片
let image: any = null;
try {
const imagePath = `/mathPages/shared/assets/${item.folder}/${item.imageIndex}.png`;
image = await getImage(canvas, imagePath);
} catch (error) {
console.error(
`加载图片失败: ${item.folder}/${item.imageIndex}`,
error,
);
return;
}
if (image) {
// 计算图片高度(等比例缩放)
// @ts-ignore - 微信小程序图片对象有 width 和 height 属性
const scaledHeight = (image.height / image.width) * imageSize;
// 绘制图片(居中)
ctx.drawImage(
image,
imageX - imageSize / 2,
imageY - scaledHeight / 2,
imageSize,
scaledHeight,
);
}
});
await Promise.all(imagePromises);
// ========== 绘制虚线分割线 ==========
const dividerY = gridStartY + gridRows * cellSize + 30;
this.drawDashedDivider(dividerY, margin);
// ========== 绘制下面区域 ==========
const bottomStartY = dividerY + 30;
const bottomImageSize = 50; // 下面图片大小
const bottomBoxSize = bottomImageSize + imagePadding * 2; // 图片框大小
const bottomSpacing = 40; // 图片间距
const bottomRowSpacing = 80; // 两行间距
// 计算下面区域的总宽度(4个图片框 + 3个间距)
const bottomTotalWidth = bottomBoxSize * 4 + bottomSpacing * 3;
const bottomStartX =
margin + (canvasWidth - margin * 2 - bottomTotalWidth) / 2; // 居中
// 第一行:绘制图片框和图片
for (let i = 0; i < matchData.bottomImages.length; i++) {
const item = matchData.bottomImages[i];
const boxX = bottomStartX + i * (bottomBoxSize + bottomSpacing);
const boxY = bottomStartY;
const imageX = boxX + imagePadding;
const imageY = boxY + imagePadding;
// 绘制方框(边线宽度2
ctx.strokeStyle = '#999';
ctx.lineWidth = 2;
ctx.strokeRect(boxX, boxY, bottomBoxSize, bottomBoxSize);
// 加载并绘制图片
let image: any = null;
try {
const imagePath = `/mathPages/shared/assets/${item.folder}/${item.imageIndex}.png`;
image = await getImage(canvas, imagePath);
} catch (error) {
console.error(
`加载图片失败: ${item.folder}/${item.imageIndex}`,
error,
);
continue;
}
if (image) {
// 计算图片高度(等比例缩放)
// @ts-ignore - 微信小程序图片对象有 width 和 height 属性
const scaledHeight =
(image.height / image.width) * bottomImageSize;
// 绘制图片(居中)
const drawImageX =
imageX + (bottomImageSize - bottomImageSize) / 2;
const drawImageY =
imageY + (bottomImageSize - scaledHeight) / 2;
ctx.drawImage(
image,
drawImageX,
drawImageY,
bottomImageSize,
scaledHeight,
);
}
}
// 绘制第一行方框下方的圆点(间隔5)
const dotRadius = 4; // 圆点半径
const dotSpacing = 10; // 圆点与方框的间距
const topDotY = bottomStartY + bottomBoxSize + dotSpacing;
for (let i = 0; i < matchData.bottomImages.length; i++) {
const boxX = bottomStartX + i * (bottomBoxSize + bottomSpacing);
const dotX = boxX + bottomBoxSize / 2; // 方框正中间
// 绘制圆点
ctx.fillStyle = '#93D333';
ctx.beginPath();
ctx.arc(dotX, topDotY, dotRadius, 0, Math.PI * 2);
ctx.fill();
}
// 第二行:绘制数字输入框(4个方框)
const numberBoxY = bottomStartY + bottomBoxSize + bottomRowSpacing;
const numberBoxSize = bottomBoxSize; // 数字框大小和图片框一样
// 绘制第二行方框上方的圆点(间隔5)
const bottomDotY = numberBoxY - dotSpacing;
// 先绘制所有圆点
for (let i = 0; i < 4; i++) {
const boxX = bottomStartX + i * (numberBoxSize + bottomSpacing);
const dotX = boxX + numberBoxSize / 2; // 方框正中间
// 绘制圆点
ctx.fillStyle = '#93D333';
ctx.beginPath();
ctx.arc(dotX, bottomDotY, dotRadius, 0, Math.PI * 2);
ctx.fill();
}
// 再绘制方框和数字
for (let i = 0; i < 4; i++) {
const boxX = bottomStartX + i * (numberBoxSize + bottomSpacing);
// 绘制方框(边线宽度2
ctx.strokeStyle = '#999';
ctx.lineWidth = 2;
ctx.strokeRect(boxX, numberBoxY, numberBoxSize, numberBoxSize);
// 绘制数字(如果存在)
if (
matchData.bottomNumbers &&
matchData.bottomNumbers[i] !== undefined
) {
const number = matchData.bottomNumbers[i];
ctx.fillStyle = '#141414';
ctx.font = `bold ${48}px "Microsoft Yahei"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
String(number),
boxX + numberBoxSize / 2,
numberBoxY + numberBoxSize / 2,
);
}
}
}
}
export default NumberObjectMatchDraw;
@@ -0,0 +1,78 @@
<!--
Canvas 页面模板
用于统一管理 Canvas 绘制页面的通用结构
使用方式:
<import src="../../templates/canvas-page-template.wxml" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
参数说明:
- boxWidth: Canvas 宽度
- boxHeight: Canvas 高度
- hasTypeSelector: 是否显示类型选择器(布尔值)
- showTypeSelector: 条件显示类型选择器(可选,用于 countMatch、missingNumber 等页面)
- currentModeName: 当前模式名称(类型选择器使用)
- typeActions: 类型选项数组(类型选择器使用)
- adType: 广告位类型('focusDraw' 或 'mathDraw'
- disabled: 底部按钮是否禁用
- showShareDialog: 是否显示分享弹窗
-->
<template name="canvasPageTemplate">
<view class="page-container">
<view class="wrapper">
<text class="wrapper-title">预览打印效果</text>
<!-- 预览打印效果 -->
<view id="canvasWrapper" class="canvas-wrapper">
<canvas
type="2d"
id="canvasContent"
class="canvas-content"
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
</view>
<!-- 类型选择器和随机生成按钮 -->
<!-- 支持两种方式:hasTypeSelector(布尔值)或 showTypeSelector(条件显示) -->
<math-type-selector
wx:if="{{hasTypeSelector || showTypeSelector}}"
current-type-name="{{currentModeName}}"
type-actions="{{typeActions}}"
bind:select="onSelectType"
bind:random="onRandom" />
<!-- 随机生成按钮(当没有类型选择器时显示) -->
<view
class="random-button-area"
wx:if="{{!hasTypeSelector && !showTypeSelector}}">
<toy-button
class="random-button"
type="primary"
bind:click="onRandom"
width="100%"
height="80rpx"
icon="refresh"
icon-class-prefix="toy-icon">
随机生成
</toy-button>
</view>
<!-- 广告位 -->
<draw-ad type="{{adType}}"></draw-ad>
</view>
<view class="empty"></view>
</view>
<!-- 底部按钮 -->
<math-bottom-buttons
disabled="{{disabled}}"
bind:share="onShareAppMessage"
bind:export="exportToPrint" />
<!-- 分享引导弹窗 -->
<share-guide-popup
show="{{showShareDialog}}"
bind:onClose="onCloseShareDialog"
bind:onShareSuccess="onShareSuccess" />
</template>