Files
doodle-mini/miniprogram/service/shapeDrawService.ts
2026-04-10 15:06:11 +08:00

195 lines
6.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 图形涂色服务
* 根据图形类型绘制到Canvas
*
* 支持的图形类型:
* - circle:圆形
* - ellipse:椭圆
* - square:正方形
* - rectangle:矩形
* - triangle:三角形
*/
import { BaseDrawService } from '../core/draw/baseDraw';
import { ShapeCard } from '../constants/shapes';
import { drawShape } from './drawShape';
class ShapeDrawService extends BaseDrawService {
shapes: ShapeCard[];
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
options = options || {};
super(canvas, ctx, {
title: '找一找 涂 色',
subTitle: '给图形涂上相同的颜色',
...options,
});
this.shapes = [];
}
async draw(shapes: ShapeCard[]) {
this.setPrintConfig();
this.shapes = shapes.slice(0, 6); // 最多6个图形
this.clear();
this.setPaper();
await this.drawHeader();
this.drawDivider();
this.drawLegend();
this.drawContent();
}
drawLegend() {
const { ctx, shapes } = this;
if (shapes.length <= 0) return;
const legendTopY = this.currentY;
const shapeSize = 67; // 200/3≈67
const rectWidth = 60; // 180/3=60
const rectHeight = 27; // 80/3≈27
const startY = legendTopY + 42; // 125/3≈42
const len = shapes.length;
// 计算示例图形的间距
const totalWidth = len * shapeSize + (len - 1) * 13; // 40/3≈13
const startX = (this.canvasWidth - totalWidth) / 2 + shapeSize / 2;
// 绘制所有图形
shapes.forEach((shape: ShapeCard, index: number) => {
const x = startX + index * (shapeSize + 13); // 40/3≈13
const y = startY;
// 绘制示例图形
drawShape(ctx, shape, x, y, shapeSize, shape.fillColor);
const rectangleX = x - rectWidth / 2;
const rectangleY = y + shapeSize / 2 + 3; // 10/3≈3
// 绘制图形名称
ctx.fillStyle = '#333';
ctx.font = 'bold 12px "Microsoft Yahei"'; // 36/3=12
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(
shape.name,
rectangleX + rectWidth / 2,
rectangleY + rectHeight / 2,
rectWidth,
);
});
this.currentY = startY + shapeSize / 2 + 3 + rectHeight + 28;
this.drawDivider();
}
drawContent() {
const { ctx, shapes } = this;
if (shapes.length <= 0) return;
// 逻辑像素尺寸(原始尺寸除以3
const ROW_COUNT = 6;
const VERTICAL_GAP = 40; // 120/3=40
const shapeSize = 60; // 180/3=60
const leftMargin = 53; // 160/3≈53
const rightMargin = 53; // 160/3≈53
const legendBottomY = this.currentY;
const topGap = 20; // 60/3=20
const contentTop = legendBottomY + topGap;
// 可用宽度
const contentWidth = this.canvasWidth - leftMargin - rightMargin;
// 每行最多能放多少个图形(考虑最小间距,逻辑像素)
const minGap = 13; // 40/3≈13
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 已由基类的 drawDivider 替代,保留此方法以保持兼容
drawLine(linY: number) {
this.drawDivider();
}
}
export default ShapeDrawService;