feat:V2.5.2 颜色找规律

This commit is contained in:
R524809
2025-12-17 15:30:14 +08:00
parent d6ec0eccd8
commit 4a3ad82d08
13 changed files with 703 additions and 127 deletions
@@ -0,0 +1,164 @@
/**
* 颜色找规律绘制服务
*/
import { BaseDrawService } from '../../../service/baseDraw';
import {
drawShapeSymbolShape,
DrawShapeOptions,
} from '../shapes/shapeSymbolShapes';
/**
* 颜色找规律数据
*/
export interface ColorPatternData {
/** 8组数据,每组包含图形ID和10个格子的颜色(前4个有颜色,后6个为null) */
groups: Array<{
shapeId: string;
colors: Array<string | null>; // 长度为10,前4个有颜色,后6个为null
}>;
}
/**
* 颜色找规律绘制服务
*/
class ColorPatternDraw extends BaseDrawService {
gridData: ColorPatternData | null = null;
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
}
/**
* 绘制颜色找规律内容
*/
async draw(gridData: ColorPatternData) {
if (!gridData || !gridData.groups) {
return;
}
this.setPrintConfig();
this.gridData = gridData;
this.clear();
this.setPaper();
// 绘制Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
this.drawMiniHeader();
}
// 绘制内容区域
this.drawDivider();
this.drawContent({
ctx: this.ctx,
gridData: this.gridData,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
/**
* 绘制内容区域
*/
private drawContent(params: {
ctx: RenderingContext;
gridData: ColorPatternData;
canvasWidth: number;
startY: number;
}): void {
const { ctx, gridData, canvasWidth } = params;
let { startY } = params;
startY = startY + 25;
const margin = 40; // 左右边距
const cols = 10; // 每行10列
const cellSize = 50; // 每个格子的宽度
const gridWidth = cellSize * cols;
const groupSpacing = 40; // 每组之间的间距
// 计算起始X位置(居中)
const startX = margin + (canvasWidth - margin * 2 - gridWidth) / 2;
// 绘制8组
for (
let groupIndex = 0;
groupIndex < gridData.groups.length;
groupIndex++
) {
const group = gridData.groups[groupIndex];
if (!group) continue;
const groupY = startY + groupIndex * (cellSize + groupSpacing);
// 绘制网格线(只绘制这一行的网格)
this.drawGridLines(
startX,
groupY,
cellSize,
cellSize,
cols,
1, // 只有1行
);
// 绘制图形和颜色
for (let col = 0; col < cols; col++) {
const color = group.colors[col];
const colX = startX + col * cellSize;
const cellCenterX = colX + cellSize / 2;
const cellCenterY = groupY + cellSize / 2;
const shapeSize = 35;
if (color) {
// 前4个图形:绘制带颜色的图形
drawShapeSymbolShape(
ctx,
group.shapeId,
cellCenterX,
cellCenterY,
shapeSize,
color,
);
} else {
// 根据图形ID绘制轮廓
this.drawShapeOutline(
ctx,
group.shapeId,
cellCenterX,
cellCenterY,
shapeSize,
);
}
}
}
}
/**
* 绘制图形轮廓(用于空白图形)
* 复用 shapeSymbolShapes.ts 中的绘制逻辑
*/
private drawShapeOutline(
ctx: RenderingContext,
shapeId: string,
x: number,
y: number,
size: number,
): void {
const options: DrawShapeOptions = {
fillColor: null,
strokeColor: '#000',
strokeWidth: 1.5,
};
// 使用新的调用方式,直接传入 options
drawShapeSymbolShape(ctx, shapeId, x, y, size, options);
}
}
export default ColorPatternDraw;
@@ -80,7 +80,7 @@ class PositionColoringDraw extends BaseDrawService {
const margin = 40; // 左右边距
const gridSize = 3; // 3x3网格
const cellSize = 65; // 每个格子的大小
const cellSize = 65; // 每个格子的宽度
const gridWidth = cellSize * gridSize;
const gridHeight = cellSize * gridSize;
@@ -91,12 +91,11 @@ class PositionColoringDraw extends BaseDrawService {
// 绘制参考网格的网格线
this.drawGridLines(
ctx,
referenceGridX,
referenceGridY,
gridWidth,
gridHeight,
cellSize,
cellSize,
gridSize,
gridSize,
);
@@ -105,7 +104,7 @@ class PositionColoringDraw extends BaseDrawService {
const row = Math.floor(i / 3);
const col = i % 3;
const imageIndex = gridData.referenceGrid[i];
const imageX = referenceGridX + col * cellSize + cellSize / 2 - 25; // 图片居中,假设图片大小50x50
const imageX = referenceGridX + col * cellSize + cellSize / 2 - 25;
const imageY = referenceGridY + row * cellSize + cellSize / 2 - 25;
try {
@@ -179,12 +178,11 @@ class PositionColoringDraw extends BaseDrawService {
// 绘制网格线
this.drawGridLines(
ctx,
taskGridX,
taskGridY,
taskGridWidth,
taskGridHeight,
taskGridSize,
taskGridSize,
gridSize,
gridSize,
);
@@ -206,41 +204,6 @@ class PositionColoringDraw extends BaseDrawService {
}
}
}
/**
* 绘制网格线
*/
private drawGridLines(
ctx: RenderingContext,
gridStartX: number,
gridStartY: number,
gridWidth: number,
gridHeight: number,
cellSize: number,
gridSize: number,
): void {
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.setLineDash([]); // 实线
// 绘制垂直线
for (let i = 0; i <= gridSize; i++) {
const x = gridStartX + i * cellSize;
ctx.beginPath();
ctx.moveTo(x, gridStartY);
ctx.lineTo(x, gridStartY + gridHeight);
ctx.stroke();
}
// 绘制水平线
for (let i = 0; i <= gridSize; i++) {
const y = gridStartY + i * cellSize;
ctx.beginPath();
ctx.moveTo(gridStartX, y);
ctx.lineTo(gridStartX + gridWidth, y);
ctx.stroke();
}
}
}
export default PositionColoringDraw;
@@ -84,9 +84,8 @@ class ShapeSymbolDraw extends BaseDrawService {
const margin = 40; // 左右边距
const legendCols = 4; // 图例列数
const legendRows = 2; // 图例行数
const cellSize = 65; // 每个格子的大小
const cellSize = 65; // 每个格子的宽度
const gridWidth = cellSize * legendCols;
const gridHeight = cellSize * legendRows;
// 绘制参考网格(上面的九宫格)
const legendStartX =
@@ -95,11 +94,9 @@ class ShapeSymbolDraw extends BaseDrawService {
// 绘制参考网格的网格线
this.drawGridLines(
ctx,
legendStartX,
legendStartY,
gridWidth,
gridHeight,
cellSize,
cellSize,
legendCols,
legendRows,
@@ -113,7 +110,7 @@ class ShapeSymbolDraw extends BaseDrawService {
gridData.legendMapping.forEach((mapping, index: number) => {
if (!mapping) return;
const shapeX = legendStartX + index * cellSize + cellSize / 2; // 图片居中,假设图片大小50x50
const shapeX = legendStartX + index * cellSize + cellSize / 2;
const shapeY = legendStartY + cellSize / 2;
const shapeSize = 42;
@@ -128,8 +125,8 @@ class ShapeSymbolDraw extends BaseDrawService {
);
// 第二行:绘制符号(使用 emoji)
const symbolX = legendStartX + index * cellSize + cellSize / 2; // 图片居中,假设图片大小50x50
const symbolY = legendStartY + cellSize + cellSize / 2 + 5;
const symbolX = legendStartX + index * cellSize + cellSize / 2;
const symbolY = legendStartY + cellSize + cellSize / 2;
this.drawSymbol(ctx, symbolX, symbolY, mapping.symbol, 30);
});
@@ -140,9 +137,8 @@ class ShapeSymbolDraw extends BaseDrawService {
const practiceStartY = dividerY + 30;
const practiceCols = 8;
const practiceRows = 8;
const practiceCellSize = 60; // 练习格子大小(正方形)
const practiceCellSize = 60; // 练习格子宽度
const practiceGridWidth = practiceCellSize * practiceCols;
const practiceGridHeight = practiceCellSize * practiceRows;
// 计算练习区域起始X位置(居中)
const practiceStartX =
@@ -150,11 +146,9 @@ class ShapeSymbolDraw extends BaseDrawService {
// 绘制练习区域网格线(使用 drawGridLines
this.drawGridLines(
ctx,
practiceStartX,
practiceStartY,
practiceGridWidth,
practiceGridHeight,
practiceCellSize,
practiceCellSize,
practiceCols,
practiceRows,
@@ -195,71 +189,88 @@ class ShapeSymbolDraw extends BaseDrawService {
}
/**
* 绘制符号(使用 emoji 文字
* 绘制符号(使用路径绘制,类似 shapeSymbolShapes.ts
*/
private drawSymbol(
ctx: RenderingContext,
x: number,
y: number,
symbol: string,
fontSize: number,
size: number,
): void {
ctx.save();
ctx.translate(x, y);
// 符号映射:将符号转换为 emoji
const symbolMap: Record<string, string> = {
'+': '',
'-': '',
'×': '✖️',
'✓': '✔️',
};
const lineWidth = size * 0.15; // 线条宽度
const halfSize = size / 2;
const strokeLength = halfSize * 0.7; // 线条长度
const emojiSymbol = symbolMap[symbol] || symbol;
ctx.strokeStyle = '#000';
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.font = `bold ${fontSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#000';
ctx.fillText(emojiSymbol, x, y);
switch (symbol) {
case '+':
// 加号:横线和竖线
ctx.beginPath();
// 横线
ctx.moveTo(-strokeLength, 0);
ctx.lineTo(strokeLength, 0);
// 竖线
ctx.moveTo(0, -strokeLength);
ctx.lineTo(0, strokeLength);
ctx.stroke();
break;
case '-':
// 减号:横线
ctx.beginPath();
ctx.moveTo(-strokeLength, 0);
ctx.lineTo(strokeLength, 0);
ctx.stroke();
break;
case '×':
// 乘号:两条斜线
ctx.beginPath();
// 左上到右下
ctx.moveTo(-strokeLength * 0.7, -strokeLength * 0.7);
ctx.lineTo(strokeLength * 0.7, strokeLength * 0.7);
// 右上到左下
ctx.moveTo(strokeLength * 0.7, -strokeLength * 0.7);
ctx.lineTo(-strokeLength * 0.7, strokeLength * 0.7);
ctx.stroke();
break;
case '✓':
// 对号:勾,整体更大,右侧的线更长
const checkScale = 1.2; // 对号整体放大1.2倍
ctx.beginPath();
const checkStartX = -strokeLength * 0.5 * checkScale;
const checkStartY = -strokeLength * 0.2 * checkScale;
const checkMidX = -strokeLength * 0.1 * checkScale;
const checkMidY = strokeLength * 0.3 * checkScale;
const checkEndX = strokeLength * 1.0 * checkScale; // 增加右侧长度
const checkEndY = -strokeLength * 0.4 * checkScale; // 稍微向上调整
ctx.moveTo(checkStartX, checkStartY);
ctx.lineTo(checkMidX, checkMidY);
ctx.lineTo(checkEndX, checkEndY);
ctx.stroke();
break;
default:
// 默认绘制加号
ctx.beginPath();
ctx.moveTo(-strokeLength, 0);
ctx.lineTo(strokeLength, 0);
ctx.moveTo(0, -strokeLength);
ctx.lineTo(0, strokeLength);
ctx.stroke();
}
ctx.restore();
}
/**
* 绘制网格线
*/
private drawGridLines(
ctx: RenderingContext,
gridStartX: number,
gridStartY: number,
gridWidth: number,
gridHeight: number,
cellSize: number,
legendCols: number,
legendRows: number,
): void {
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.setLineDash([]); // 实线
// 绘制垂直线
for (let i = 0; i <= legendCols; i++) {
const x = gridStartX + i * cellSize;
ctx.beginPath();
ctx.moveTo(x, gridStartY);
ctx.lineTo(x, gridStartY + gridHeight);
ctx.stroke();
}
// 绘制水平线
for (let i = 0; i <= legendRows; i++) {
const y = gridStartY + i * cellSize;
ctx.beginPath();
ctx.moveTo(gridStartX, y);
ctx.lineTo(gridStartX + gridWidth, y);
ctx.stroke();
}
}
}
export default ShapeSymbolDraw;
@@ -17,12 +17,30 @@ export const SHAPE_SYMBOL_SHAPES: ShapeSymbolShape[] = [
{ id: 'ellipse', name: '椭圆' },
{ id: 'star', name: '五角星' },
{ id: 'pentagon', name: '五边形' },
{ id: 'hexagon', name: '六边形' },
{ id: 'octagon', name: '八边形' },
{ id: 'cross', name: '十字形' },
{ id: 'rectangle', name: '长方形' },
{ id: 'sector', name: '扇形' },
{ id: 'heart', name: '心形' },
];
/**
* 绘制图形(无边框,仅填充)
* 绘制图形选项
*/
export interface DrawShapeOptions {
/** 填充颜色,如果为 null 则不填充 */
fillColor?: string | null;
/** 边框颜色,如果为 null 则不绘制边框 */
strokeColor?: string | null;
/** 边框宽度,默认为 1.5 */
strokeWidth?: number;
}
/**
* 绘制图形(支持边框和填充控制)
* 所有图形保持相同的大小
* @overload 旧的方式:drawShapeSymbolShape(ctx, shapeId, x, y, size, fillColor)
*/
export function drawShapeSymbolShape(
ctx: RenderingContext,
@@ -31,32 +49,110 @@ export function drawShapeSymbolShape(
y: number,
size: number,
fillColor: string,
options?: DrawShapeOptions,
): void;
/**
* 绘制图形(支持边框和填充控制)
* 所有图形保持相同的大小
* @overload 新的方式:drawShapeSymbolShape(ctx, shapeId, x, y, size, options)
*/
export function drawShapeSymbolShape(
ctx: RenderingContext,
shapeId: string,
x: number,
y: number,
size: number,
options: DrawShapeOptions,
): void;
/**
* 绘制图形(支持边框和填充控制)
* 所有图形保持相同的大小
*/
export function drawShapeSymbolShape(
ctx: RenderingContext,
shapeId: string,
x: number,
y: number,
size: number,
fillColorOrOptions: string | DrawShapeOptions,
options?: DrawShapeOptions,
): void {
ctx.save();
ctx.translate(x, y);
const radius = size / 2;
ctx.fillStyle = fillColor;
// 兼容旧的调用方式:drawShapeSymbolShape(ctx, shapeId, x, y, size, fillColor)
let fillColor: string | null = null;
let drawOptions: DrawShapeOptions = {};
if (typeof fillColorOrOptions === 'string') {
// 旧的方式:fillColor 是字符串
fillColor = fillColorOrOptions;
drawOptions = options || {};
} else {
// 新的方式:第一个参数是 options
drawOptions = fillColorOrOptions || {};
fillColor = drawOptions.fillColor || null;
}
// 设置填充和描边样式
if (fillColor) {
ctx.fillStyle = fillColor;
}
if (drawOptions.strokeColor) {
ctx.strokeStyle = drawOptions.strokeColor;
ctx.lineWidth = drawOptions.strokeWidth || 1.5;
}
// 统一的填充和描边处理函数
const drawPath = () => {
if (fillColor) {
ctx.fill();
}
if (drawOptions.strokeColor) {
ctx.stroke();
}
};
switch (shapeId) {
case 'circle':
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fill();
drawPath();
break;
case 'square':
ctx.fillRect(-radius, -radius, size, size);
// 正方形缩小为原来的0.9倍
const squareSize = size * 0.9;
const squareRadius = squareSize / 2;
if (fillColor) {
ctx.fillRect(
-squareRadius,
-squareRadius,
squareSize,
squareSize,
);
}
if (drawOptions.strokeColor) {
ctx.strokeRect(
-squareRadius,
-squareRadius,
squareSize,
squareSize,
);
}
break;
case 'triangle':
// 三角形缩小为原来的0.9倍
const triangleRadius = radius * 0.9;
ctx.beginPath();
ctx.moveTo(0, -radius);
ctx.lineTo(-radius, radius);
ctx.lineTo(radius, radius);
ctx.moveTo(0, -triangleRadius);
ctx.lineTo(-triangleRadius, triangleRadius);
ctx.lineTo(triangleRadius, triangleRadius);
ctx.closePath();
ctx.fill();
drawPath();
break;
case 'diamond':
@@ -66,7 +162,7 @@ export function drawShapeSymbolShape(
ctx.lineTo(0, radius);
ctx.lineTo(-radius, 0);
ctx.closePath();
ctx.fill();
drawPath();
break;
case 'trapezoid':
@@ -77,13 +173,13 @@ export function drawShapeSymbolShape(
ctx.lineTo(radius, radius * 0.7);
ctx.lineTo(-radius, radius * 0.7);
ctx.closePath();
ctx.fill();
drawPath();
break;
case 'ellipse':
ctx.beginPath();
ctx.ellipse(0, 0, radius * 0.8, radius * 0.6, 0, 0, Math.PI * 2);
ctx.fill();
drawPath();
break;
case 'star':
@@ -106,7 +202,7 @@ export function drawShapeSymbolShape(
ctx.lineTo(x2, y2);
}
ctx.closePath();
ctx.fill();
drawPath();
break;
case 'pentagon':
@@ -119,7 +215,94 @@ export function drawShapeSymbolShape(
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
drawPath();
break;
case 'hexagon':
// 六边形
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (i * 2 * Math.PI) / 6 - Math.PI / 2;
const px = Math.cos(angle) * radius;
const py = Math.sin(angle) * radius;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
drawPath();
break;
case 'octagon':
// 八边形
ctx.beginPath();
for (let i = 0; i < 8; i++) {
const angle = (i * 2 * Math.PI) / 8 - Math.PI / 2;
const px = Math.cos(angle) * radius;
const py = Math.sin(angle) * radius;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
drawPath();
break;
case 'cross':
// 十字形
const crossWidth = radius * 0.3; // 十字的宽度
if (fillColor) {
// 竖条
ctx.fillRect(-crossWidth, -radius, crossWidth * 2, size);
// 横条
ctx.fillRect(-radius, -crossWidth, size, crossWidth * 2);
}
if (drawOptions.strokeColor) {
// 竖条
ctx.strokeRect(-crossWidth, -radius, crossWidth * 2, size);
// 横条
ctx.strokeRect(-radius, -crossWidth, size, crossWidth * 2);
}
break;
case 'rectangle':
// 长方形(横向,宽度是高度的1.2倍)
const rectWidth = radius * 1.1;
const rectHeight = radius * 0.7;
if (fillColor) {
ctx.fillRect(
-rectWidth,
-rectHeight,
rectWidth * 2,
rectHeight * 2,
);
}
if (drawOptions.strokeColor) {
ctx.strokeRect(
-rectWidth,
-rectHeight,
rectWidth * 2,
rectHeight * 2,
);
}
break;
// 扇形
case 'sector':
// 扇形(90度扇形),圆心在格子中心点偏左二分之一的位置
const sectorCenterX = -radius / 2; // 向左偏移半径的一半
const sectorCenterY = 0;
ctx.beginPath();
ctx.moveTo(sectorCenterX, sectorCenterY);
ctx.arc(
sectorCenterX,
sectorCenterY,
radius,
-Math.PI / 4,
Math.PI / 4,
false,
);
ctx.lineTo(sectorCenterX, sectorCenterY);
ctx.closePath();
drawPath();
break;
case 'heart':
@@ -141,14 +324,14 @@ export function drawShapeSymbolShape(
ctx.bezierCurveTo(tx(50), ty(80), tx(70), ty(65), tx(75), ty(50));
ctx.bezierCurveTo(tx(80), ty(35), tx(65), ty(20), tx(50), ty(35));
ctx.closePath();
ctx.fill();
drawPath();
break;
default:
// 默认绘制圆形
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fill();
drawPath();
}
ctx.restore();