feat:V2.5.3 修改下载分享弹窗问题、新增连连看
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
/**
|
||||
* 连连看绘制服务
|
||||
*/
|
||||
|
||||
import { BaseDrawService } from '../../../service/baseDraw';
|
||||
import { getImage } from '../../../utils/index';
|
||||
|
||||
/**
|
||||
* 连连看数据
|
||||
*/
|
||||
export interface MatchConnectData {
|
||||
/** 参考序列:5张图片的索引数组 */
|
||||
referenceSequence: number[];
|
||||
/** 6个框,每个框包含5张图片的索引(位置由绘制服务随机生成) */
|
||||
boxes: Array<{
|
||||
imageIndices: number[];
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连连看绘制服务
|
||||
*/
|
||||
class MatchConnectDraw extends BaseDrawService {
|
||||
matchData: MatchConnectData | null = null;
|
||||
|
||||
constructor(
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) {
|
||||
super(canvas, ctx, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制连连看内容
|
||||
*/
|
||||
async draw(matchData: MatchConnectData) {
|
||||
if (!matchData || !matchData.referenceSequence || !matchData.boxes) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setPrintConfig();
|
||||
this.matchData = matchData;
|
||||
this.clear();
|
||||
this.setPaper();
|
||||
|
||||
// 绘制Header
|
||||
if (this.headerType !== 'minimal') {
|
||||
await this.drawHeader();
|
||||
} else {
|
||||
this.drawMiniHeader();
|
||||
}
|
||||
|
||||
// 绘制内容区域
|
||||
this.drawDivider();
|
||||
await this.drawContent({
|
||||
canvas: this.canvas,
|
||||
ctx: this.ctx,
|
||||
matchData: this.matchData,
|
||||
canvasWidth: this.canvasWidth,
|
||||
startY: this.currentY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成不重叠的图片位置
|
||||
* @param count 图片数量
|
||||
* @param contentWidth 内容区域宽度
|
||||
* @param contentHeight 内容区域高度
|
||||
* @param imageSize 图片大小
|
||||
* @param imagePadding 图片padding
|
||||
* @returns 图片位置数组,每个位置包含x和y(相对于内容区域的坐标)
|
||||
*/
|
||||
private generateNonOverlappingPositions(
|
||||
count: number,
|
||||
contentWidth: number,
|
||||
contentHeight: number,
|
||||
imageSize: number,
|
||||
imagePadding: number,
|
||||
): Array<{ x: number; y: number }> {
|
||||
// 计算最大可放置位置(图片左上角的位置)
|
||||
const maxX = contentWidth - imageSize;
|
||||
const maxY = contentHeight - imageSize;
|
||||
const positions: Array<{ x: number; y: number }> = [];
|
||||
const placedRects: Array<{
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}> = [];
|
||||
|
||||
/**
|
||||
* 检查两个矩形是否重叠
|
||||
*/
|
||||
const isRectOverlap = (
|
||||
rect1: {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
},
|
||||
rect2: {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
},
|
||||
): boolean => {
|
||||
return !(
|
||||
rect1.right < rect2.left ||
|
||||
rect1.left > rect2.right ||
|
||||
rect1.bottom < rect2.top ||
|
||||
rect1.top > rect2.bottom
|
||||
);
|
||||
};
|
||||
|
||||
// 计算图片的实际占用大小(包含padding)
|
||||
const occupiedSize = imageSize + imagePadding * 2;
|
||||
|
||||
// 生成候选位置网格,然后随机打乱
|
||||
// 这样可以确保位置分布更均匀,同时保持随机性
|
||||
const gridCols = Math.max(3, Math.floor(contentWidth / occupiedSize));
|
||||
const gridRows = Math.max(2, Math.floor(contentHeight / occupiedSize));
|
||||
const cellWidth = maxX / Math.max(1, gridCols - 1);
|
||||
const cellHeight = maxY / Math.max(1, gridRows - 1);
|
||||
|
||||
// 生成所有可能的网格位置
|
||||
const candidatePositions: Array<{ x: number; y: number }> = [];
|
||||
for (let row = 0; row < gridRows; row++) {
|
||||
for (let col = 0; col < gridCols; col++) {
|
||||
const x = col * cellWidth;
|
||||
const y = row * cellHeight;
|
||||
candidatePositions.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
// 随机打乱候选位置
|
||||
const shuffledCandidates = [...candidatePositions].sort(
|
||||
() => Math.random() - 0.5,
|
||||
);
|
||||
|
||||
// 从打乱的候选位置中选择不重叠的位置
|
||||
for (let i = 0; i < count; i++) {
|
||||
let found = false;
|
||||
|
||||
// 先尝试从网格候选位置中选择
|
||||
for (const candidate of shuffledCandidates) {
|
||||
const x = candidate.x;
|
||||
const y = candidate.y;
|
||||
|
||||
// 计算当前图片的实际占用区域(包含padding)
|
||||
const currentRect = {
|
||||
left: x - imagePadding,
|
||||
top: y - imagePadding,
|
||||
right: x + imageSize + imagePadding,
|
||||
bottom: y + imageSize + imagePadding,
|
||||
};
|
||||
|
||||
// 确保不超出内容区域的范围
|
||||
if (
|
||||
currentRect.left < 0 ||
|
||||
currentRect.top < 0 ||
|
||||
currentRect.right > contentWidth ||
|
||||
currentRect.bottom > contentHeight
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否与已放置的图片重叠
|
||||
let overlaps = false;
|
||||
for (const placedRect of placedRects) {
|
||||
if (isRectOverlap(currentRect, placedRect)) {
|
||||
overlaps = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!overlaps) {
|
||||
// 存储实际占用区域
|
||||
placedRects.push({
|
||||
left: x - imagePadding,
|
||||
top: y - imagePadding,
|
||||
right: x + imageSize + imagePadding,
|
||||
bottom: y + imageSize + imagePadding,
|
||||
});
|
||||
|
||||
positions.push({ x, y });
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果网格位置都不行,尝试完全随机的位置
|
||||
if (!found) {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 1000;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
// 生成随机位置,添加一些随机偏移
|
||||
const randomOffsetX =
|
||||
(Math.random() - 0.5) * cellWidth * 0.5;
|
||||
const randomOffsetY =
|
||||
(Math.random() - 0.5) * cellHeight * 0.5;
|
||||
const baseX = Math.random() * maxX;
|
||||
const baseY = Math.random() * maxY;
|
||||
const x = Math.max(
|
||||
0,
|
||||
Math.min(maxX, baseX + randomOffsetX),
|
||||
);
|
||||
const y = Math.max(
|
||||
0,
|
||||
Math.min(maxY, baseY + randomOffsetY),
|
||||
);
|
||||
|
||||
// 计算当前图片的实际占用区域(包含padding)
|
||||
const currentRect = {
|
||||
left: x - imagePadding,
|
||||
top: y - imagePadding,
|
||||
right: x + imageSize + imagePadding,
|
||||
bottom: y + imageSize + imagePadding,
|
||||
};
|
||||
|
||||
// 确保不超出内容区域的范围
|
||||
if (
|
||||
currentRect.left < 0 ||
|
||||
currentRect.top < 0 ||
|
||||
currentRect.right > contentWidth ||
|
||||
currentRect.bottom > contentHeight
|
||||
) {
|
||||
attempts++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否与已放置的图片重叠
|
||||
let overlaps = false;
|
||||
for (const placedRect of placedRects) {
|
||||
if (isRectOverlap(currentRect, placedRect)) {
|
||||
overlaps = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!overlaps) {
|
||||
// 存储实际占用区域
|
||||
placedRects.push({
|
||||
left: x - imagePadding,
|
||||
top: y - imagePadding,
|
||||
right: x + imageSize + imagePadding,
|
||||
bottom: y + imageSize + imagePadding,
|
||||
});
|
||||
|
||||
positions.push({ x, y });
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
// 最后的后备方案:使用网格布局,但添加随机偏移
|
||||
if (!found) {
|
||||
const gridCols = 3;
|
||||
const gridRows = 2;
|
||||
const gridCellWidth = maxX / Math.max(1, gridCols - 1);
|
||||
const gridCellHeight = maxY / Math.max(1, gridRows - 1);
|
||||
const baseX = (i % gridCols) * gridCellWidth;
|
||||
const baseY = Math.floor(i / gridCols) * gridCellHeight;
|
||||
// 添加随机偏移,但确保不重叠
|
||||
const offsetX = (Math.random() - 0.5) * gridCellWidth * 0.3;
|
||||
const offsetY = (Math.random() - 0.5) * gridCellHeight * 0.3;
|
||||
const x = Math.max(0, Math.min(maxX, baseX + offsetX));
|
||||
const y = Math.max(0, Math.min(maxY, baseY + offsetY));
|
||||
positions.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
// 最后再次随机打乱位置顺序,增加随机性
|
||||
return positions.sort(() => Math.random() - 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制箭头
|
||||
*/
|
||||
private drawArrow(
|
||||
ctx: RenderingContext,
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
) {
|
||||
ctx.strokeStyle = '#FF69B4'; // 粉色
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([]); // 实线
|
||||
|
||||
// 绘制箭头线
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制箭头头部
|
||||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||||
const arrowLength = 8;
|
||||
const arrowAngle = Math.PI / 6; // 30度
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x2, y2);
|
||||
ctx.lineTo(
|
||||
x2 - arrowLength * Math.cos(angle - arrowAngle),
|
||||
y2 - arrowLength * Math.sin(angle - arrowAngle),
|
||||
);
|
||||
ctx.moveTo(x2, y2);
|
||||
ctx.lineTo(
|
||||
x2 - arrowLength * Math.cos(angle + arrowAngle),
|
||||
y2 - arrowLength * Math.sin(angle + arrowAngle),
|
||||
);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制内容区域
|
||||
*/
|
||||
private async drawContent(params: {
|
||||
canvas: WechatMiniprogram.Canvas;
|
||||
ctx: RenderingContext;
|
||||
matchData: MatchConnectData;
|
||||
canvasWidth: number;
|
||||
startY: number;
|
||||
}): Promise<void> {
|
||||
const { ctx, matchData, canvasWidth } = params;
|
||||
let { startY } = params;
|
||||
|
||||
startY = startY + 25;
|
||||
|
||||
const margin = 40; // 左右边距
|
||||
const imageSize = 50; // 图片大小
|
||||
const arrowLength = 30; // 箭头长度
|
||||
const arrowSpacing = 8; // 箭头左右间距
|
||||
|
||||
// 计算参考序列的总宽度
|
||||
// 总宽度 = 所有图片宽度 + 所有箭头长度 + 所有箭头间距(左右各8px)
|
||||
const arrowCount = matchData.referenceSequence.length - 1;
|
||||
const referenceWidth =
|
||||
matchData.referenceSequence.length * imageSize +
|
||||
arrowCount * arrowLength +
|
||||
arrowCount * arrowSpacing * 2; // 每个箭头左右各8px
|
||||
|
||||
// 绘制参考序列(上面部分)- 居中显示
|
||||
const referenceStartX =
|
||||
margin + (canvasWidth - margin * 2 - referenceWidth) / 2;
|
||||
const referenceY = startY;
|
||||
|
||||
// 绘制参考序列的图片和箭头
|
||||
let currentX = referenceStartX;
|
||||
for (let i = 0; i < matchData.referenceSequence.length; i++) {
|
||||
const imageIndex = matchData.referenceSequence[i];
|
||||
|
||||
// 绘制图片
|
||||
try {
|
||||
const imagePath = `/focusPages/shared/aeests/material/${imageIndex}.png`;
|
||||
const image = await getImage(this.canvas, imagePath);
|
||||
ctx.drawImage(
|
||||
image,
|
||||
currentX,
|
||||
referenceY,
|
||||
imageSize,
|
||||
imageSize,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`加载参考图片失败: ${imageIndex}`, error);
|
||||
}
|
||||
|
||||
// 如果不是最后一张,绘制箭头
|
||||
if (i < matchData.referenceSequence.length - 1) {
|
||||
// 箭头左边间距8px
|
||||
const arrowStartX = currentX + imageSize + arrowSpacing;
|
||||
const arrowEndX = arrowStartX + arrowLength;
|
||||
const arrowY = referenceY + imageSize / 2;
|
||||
this.drawArrow(ctx, arrowStartX, arrowY, arrowEndX, arrowY);
|
||||
// 箭头右边间距8px,然后开始下一张图片
|
||||
currentX = arrowEndX + arrowSpacing;
|
||||
} else {
|
||||
currentX += imageSize;
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制虚线分割线
|
||||
const dividerY = referenceY + imageSize + 30;
|
||||
this.drawDashedDivider(dividerY, margin);
|
||||
|
||||
// 绘制下面的6个框(3行2列)
|
||||
const boxStartY = dividerY + 30;
|
||||
const boxWidth = (canvasWidth - margin * 2 - 20) / 2; // 减去列间距
|
||||
const boxHeight = 165; // 框的高度
|
||||
const boxSpacingX = 20; // 列间距
|
||||
const boxSpacingY = 30; // 行间距
|
||||
const boxPadding = 10; // 框内边距
|
||||
const boxImageSize = 35; // 框内图片大小
|
||||
const imagePadding = 20; // 每张图片的内边距(padding)
|
||||
|
||||
for (let row = 0; row < 3; row++) {
|
||||
for (let col = 0; col < 2; col++) {
|
||||
const boxIndex = row * 2 + col;
|
||||
const box = matchData.boxes[boxIndex];
|
||||
if (!box || !box.imageIndices) continue;
|
||||
|
||||
const boxX = margin + col * (boxWidth + boxSpacingX);
|
||||
const boxY = boxStartY + row * (boxHeight + boxSpacingY);
|
||||
|
||||
// 绘制框(圆角实线)
|
||||
this.drawRoundedRect(boxX, boxY, boxWidth, boxHeight, {
|
||||
isDashed: false,
|
||||
radius: 16,
|
||||
color: '#000',
|
||||
lineWidth: 1,
|
||||
});
|
||||
|
||||
// 计算框内可用的内容区域
|
||||
const contentWidth = boxWidth - boxPadding * 2;
|
||||
const contentHeight = boxHeight - boxPadding * 2;
|
||||
|
||||
// 为这个框的图片随机分配位置,确保图片+padding不重叠
|
||||
const imagePositions = this.generateNonOverlappingPositions(
|
||||
box.imageIndices.length,
|
||||
contentWidth,
|
||||
contentHeight,
|
||||
boxImageSize,
|
||||
imagePadding,
|
||||
);
|
||||
|
||||
// 绘制框内的图片
|
||||
for (let i = 0; i < box.imageIndices.length; i++) {
|
||||
const imageIndex = box.imageIndices[i];
|
||||
const position = imagePositions[i];
|
||||
|
||||
try {
|
||||
const imagePath = `/focusPages/shared/aeests/material/${imageIndex}.png`;
|
||||
const image = await getImage(this.canvas, imagePath);
|
||||
|
||||
// 计算图片位置
|
||||
const imageX = boxX + boxPadding + position.x;
|
||||
const imageY = boxY + boxPadding + position.y;
|
||||
|
||||
ctx.drawImage(
|
||||
image,
|
||||
imageX,
|
||||
imageY,
|
||||
boxImageSize,
|
||||
boxImageSize,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`加载框内图片失败: ${imageIndex}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MatchConnectDraw;
|
||||
Reference in New Issue
Block a user