336 lines
11 KiB
TypeScript
336 lines
11 KiB
TypeScript
import { PAPER_SIZE } from '../constants/colors';
|
|
import { ShapeCard } from '../constants/shapes';
|
|
import { drawShape } from './drawShape';
|
|
import {
|
|
drawHeader as drawHeaderCommon,
|
|
drawMiniHeader as drawMiniHeaderCommon,
|
|
} from './headerDrawService';
|
|
|
|
/**
|
|
* 计算图形在画布上的位置,避免重叠
|
|
* @param canvasWidth 画布宽度
|
|
* @param canvasHeight 画布高度
|
|
* @param shapeCount 图形数量
|
|
* @param shapeSize 图形大小
|
|
* @returns 图形位置数组
|
|
*/
|
|
function calculateShapePositions(
|
|
canvasWidth: number,
|
|
canvasHeight: number,
|
|
shapeCount: number,
|
|
shapeSize: number,
|
|
) {
|
|
const positions = [];
|
|
const padding = 80;
|
|
const minSpacing = shapeSize * 1.5; // 最小间距为图形大小的1.5倍
|
|
|
|
const availableWidth = canvasWidth - 2 * padding;
|
|
const availableHeight = canvasHeight - 2 * padding;
|
|
|
|
// 计算网格布局
|
|
const cols = Math.ceil(Math.sqrt(shapeCount));
|
|
const rows = Math.ceil(shapeCount / cols);
|
|
|
|
const cellWidth = availableWidth / cols;
|
|
const cellHeight = availableHeight / rows;
|
|
|
|
for (let i = 0; i < shapeCount; i++) {
|
|
const row = Math.floor(i / cols);
|
|
const col = i % cols;
|
|
|
|
// 在单元格内随机位置
|
|
const x =
|
|
padding +
|
|
col * cellWidth +
|
|
(cellWidth - shapeSize) / 2 +
|
|
(Math.random() - 0.5) * (cellWidth - shapeSize) * 0.3;
|
|
const y =
|
|
padding +
|
|
row * cellHeight +
|
|
(cellHeight - shapeSize) / 2 +
|
|
(Math.random() - 0.5) * (cellHeight - shapeSize) * 0.3;
|
|
|
|
positions.push({ x, y });
|
|
}
|
|
|
|
return positions;
|
|
}
|
|
|
|
class ShapeDrawService {
|
|
headerType: PrintHeader;
|
|
canvas: WechatMiniprogram.Canvas;
|
|
ctx: RenderingContext;
|
|
options: Record<string, any>;
|
|
paperSize: PaperSize;
|
|
currentX: number;
|
|
currentY: number;
|
|
shapes: ShapeCard[];
|
|
|
|
constructor(
|
|
canvas: Canvas,
|
|
ctx: RenderingContext,
|
|
options?: Record<string, any>,
|
|
) {
|
|
options = options || {};
|
|
this.canvas = canvas;
|
|
this.ctx = ctx;
|
|
this.paperSize = 'A4';
|
|
this.options = {
|
|
appName: '涂鸦丫小程序',
|
|
appHint: '涂色|识字|画画|打印',
|
|
title: '找一找 涂 色',
|
|
subTitle: '给图形涂上相同的颜色',
|
|
...options,
|
|
};
|
|
this.currentX = 0;
|
|
this.currentY = 0;
|
|
this.shapes = [];
|
|
this.headerType = 'wechat'; // 默认值
|
|
this.setPrintConfig();
|
|
}
|
|
|
|
setPrintConfig() {
|
|
const printConfig = getApp().getPrintConfig();
|
|
this.headerType = printConfig.header;
|
|
this.options.appName = printConfig.appName;
|
|
}
|
|
|
|
draw(shapes: ShapeCard[]) {
|
|
this.setPrintConfig();
|
|
this.shapes = shapes.slice(0, 6); // 最多6个图形
|
|
this.clear();
|
|
this.setPaper();
|
|
|
|
if (this.headerType !== 'minimal') {
|
|
this.drawHeader();
|
|
} else {
|
|
this.drawMiniHeader();
|
|
}
|
|
|
|
this.drawLegend();
|
|
this.drawContent();
|
|
}
|
|
|
|
async drawHeader() {
|
|
this.currentX = 80;
|
|
this.currentY = 80;
|
|
await drawHeaderCommon({
|
|
canvas: this.canvas,
|
|
ctx: this.ctx,
|
|
headerType: this.headerType,
|
|
options: {
|
|
appName: this.options.appName || '涂鸦丫小程序',
|
|
appHint: this.options.appHint || '涂色|识字|画画|打印',
|
|
title: this.options.title || '找一找 涂 色',
|
|
subTitle: this.options.subTitle || '给图形涂上相同的颜色',
|
|
},
|
|
onHeaderDrawn: (currentY) => {
|
|
this.currentY = currentY;
|
|
this.drawLine(this.currentY);
|
|
},
|
|
});
|
|
}
|
|
|
|
drawMiniHeader() {
|
|
drawMiniHeaderCommon({
|
|
canvas: this.canvas,
|
|
ctx: this.ctx,
|
|
options: {
|
|
appName: this.options.appName || '涂鸦丫小程序',
|
|
title: this.options.title || '找一找 涂 色',
|
|
},
|
|
onHeaderDrawn: (currentY) => {
|
|
this.currentY = currentY;
|
|
this.drawLine(this.currentY);
|
|
},
|
|
});
|
|
}
|
|
|
|
drawLegend() {
|
|
const { canvas, ctx, shapes } = this;
|
|
if (shapes.length <= 0) return;
|
|
|
|
this.currentY = this.headerType === 'minimal' ? 200 : 304;
|
|
const shapeSize = 200;
|
|
const rectWidth = 180;
|
|
const rectHeight = 80;
|
|
// 固定图例的Y位置,不依赖shapeSize
|
|
const startY = this.currentY + 125; // 固定距离,不依赖shapeSize
|
|
const len = shapes.length;
|
|
const canvasWidth = canvas.width;
|
|
|
|
// 计算示例图形的间距
|
|
const totalWidth = len * shapeSize + (len - 1) * 40;
|
|
const startX = (canvasWidth - totalWidth) / 2 + shapeSize / 2;
|
|
|
|
// 绘制所有图形
|
|
shapes.forEach((shape: ShapeCard, index: number) => {
|
|
const x = startX + index * (shapeSize + 40);
|
|
const y = startY;
|
|
|
|
// 绘制示例图形
|
|
drawShape(ctx, shape, x, y, shapeSize, shape.fillColor);
|
|
|
|
// // 绘制长方形
|
|
// ctx.fillStyle = '#fff';
|
|
// ctx.strokeStyle = '#000';
|
|
// ctx.lineWidth = 4;
|
|
const rectangleX = x - rectWidth / 2;
|
|
const rectangleY = y + shapeSize / 2 + 10;
|
|
// ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight);
|
|
|
|
// 绘制图形名称
|
|
ctx.fillStyle = '#333';
|
|
ctx.font = 'bold 36px "Microsoft Yahei"';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(
|
|
shape.name,
|
|
rectangleX + rectWidth / 2,
|
|
rectangleY + rectHeight / 2,
|
|
rectWidth,
|
|
);
|
|
});
|
|
|
|
this.currentY = this.headerType === 'minimal' ? 532 : 622;
|
|
this.drawLine(this.currentY);
|
|
}
|
|
|
|
drawContent() {
|
|
const { canvas, ctx, shapes } = this;
|
|
if (shapes.length <= 0) return;
|
|
|
|
// 固定可渲染的总行数
|
|
const ROW_COUNT = 6;
|
|
// 每行之间的垂直间距
|
|
const VERTICAL_GAP = 120;
|
|
// 图形大小
|
|
const shapeSize = 180;
|
|
// 左右边距
|
|
const leftMargin = 160;
|
|
const rightMargin = 160;
|
|
// 以 drawLegend 画完后的分割线作为基准,内容区顶部间距 50
|
|
const legendBottomY = this.currentY;
|
|
const topGap = 60;
|
|
const contentTop = legendBottomY + topGap;
|
|
|
|
// 可用宽度
|
|
const contentWidth = canvas.width - leftMargin - rightMargin;
|
|
|
|
// 每行最多能放多少个图形(考虑最小间距40)
|
|
const minGap = 40;
|
|
const maxPerRow = Math.floor(
|
|
(contentWidth + minGap) / (shapeSize + minGap),
|
|
);
|
|
|
|
// 总共要绘制多少个图形(每行都填满)
|
|
const totalShapes = maxPerRow * ROW_COUNT;
|
|
|
|
// 生成所有要绘制的图形(打乱顺序,保证随机性)
|
|
let allShapes: ShapeCard[] = [];
|
|
for (let i = 0; i < Math.ceil(totalShapes / shapes.length); i++) {
|
|
allShapes = allShapes.concat(shapes);
|
|
}
|
|
allShapes = allShapes.slice(0, totalShapes);
|
|
// 洗牌算法彻底打乱
|
|
for (let i = allShapes.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[allShapes[i], allShapes[j]] = [allShapes[j], allShapes[i]];
|
|
}
|
|
|
|
// 生成所有图形的位置
|
|
let positions: { x: number; y: number }[] = [];
|
|
for (let row = 0; row < ROW_COUNT; row++) {
|
|
// 本行起始Y
|
|
const y =
|
|
contentTop + row * (shapeSize + VERTICAL_GAP) + shapeSize / 2;
|
|
// 本行实际间距
|
|
const gap =
|
|
maxPerRow > 1
|
|
? Math.max(
|
|
minGap,
|
|
(contentWidth - maxPerRow * shapeSize) /
|
|
(maxPerRow - 1),
|
|
)
|
|
: 0;
|
|
// 本行起始X(水平居中)
|
|
let startX = leftMargin;
|
|
if (maxPerRow > 1) {
|
|
const rowWidth = maxPerRow * shapeSize + (maxPerRow - 1) * gap;
|
|
startX = leftMargin + (contentWidth - rowWidth) / 2;
|
|
} else {
|
|
startX = leftMargin + (contentWidth - shapeSize) / 2;
|
|
}
|
|
for (let col = 0; col < maxPerRow; col++) {
|
|
positions.push({
|
|
x: startX + col * (shapeSize + gap) + shapeSize / 2,
|
|
y: y,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 修正:只保留前 totalShapes 个位置,防止多出一行
|
|
positions = positions.slice(0, totalShapes);
|
|
|
|
// 再次彻底打乱位置顺序,保证排列无规律
|
|
for (let i = positions.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[positions[i], positions[j]] = [positions[j], positions[i]];
|
|
}
|
|
|
|
// 记录实际绘制的图形数量
|
|
console.log(
|
|
`绘制图形:计划${totalShapes}个,实际${positions.length}个;每行${maxPerRow}个,共${ROW_COUNT}行;边距 左右=${leftMargin}/${rightMargin},顶部=${topGap}(基于legend底线)`,
|
|
);
|
|
|
|
// 绘制所有图形
|
|
positions.forEach((pos, index) => {
|
|
const shape = allShapes[index];
|
|
|
|
// 随机旋转角度,范围为±90度(即最大180度)
|
|
const rotation = (Math.random() - 0.5) * Math.PI; // ±90度 = ±π/2 弧度,Math.PI为180度
|
|
|
|
ctx.save();
|
|
ctx.translate(pos.x, pos.y);
|
|
ctx.rotate(rotation);
|
|
|
|
drawShape(ctx, shape, 0, 0, shapeSize, 'transparent');
|
|
|
|
ctx.restore();
|
|
});
|
|
}
|
|
|
|
drawLine(linY: number) {
|
|
const { canvas, ctx } = this;
|
|
|
|
ctx.strokeStyle = '#000';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(80, linY);
|
|
ctx.lineTo(canvas.width - 80, linY);
|
|
ctx.stroke();
|
|
}
|
|
|
|
setPaper() {
|
|
const { ctx, canvas } = this;
|
|
const { pixelRatio: dpr } = wx.getWindowInfo();
|
|
let { width, height } = PAPER_SIZE[this.paperSize];
|
|
width = width * dpr;
|
|
height = height * dpr;
|
|
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
this.clear();
|
|
ctx.fillStyle = '#fff';
|
|
ctx.fillRect(0, 0, width, height);
|
|
}
|
|
|
|
clear() {
|
|
const canvas = this.canvas;
|
|
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
}
|
|
|
|
export default ShapeDrawService;
|