feat:V2.5.4增加线条识别
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
/**
|
||||
* 线条识别绘制服务
|
||||
*/
|
||||
|
||||
import { BaseDrawService } from '../../../service/baseDraw';
|
||||
import { getImage } from '../../../utils/index';
|
||||
|
||||
/**
|
||||
* 线条类型
|
||||
*/
|
||||
export type LineType = 'straight' | 'dashed' | 'wavy' | 'zigzag' | 'spiral';
|
||||
|
||||
/**
|
||||
* 线条类型配置
|
||||
*/
|
||||
export interface LineTypeConfig {
|
||||
type: LineType;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线条识别数据
|
||||
*/
|
||||
export interface LineRecognitionData {
|
||||
/** 预览区域的5个线条配置(随机排列) */
|
||||
referenceLines: LineTypeConfig[];
|
||||
/** 练习区域的16个色块(2列×8行) */
|
||||
practiceBlocks: Array<{
|
||||
color: string;
|
||||
lineType: LineType;
|
||||
x: number; // 相对于列的X偏移(错落感)
|
||||
y: number; // 相对于行的Y偏移(错落感)
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线条识别绘制服务
|
||||
*/
|
||||
class LineRecognitionDraw extends BaseDrawService {
|
||||
lineData: LineRecognitionData | null = null;
|
||||
lineStrokeStyle: string = '#222'; // 线条颜色
|
||||
lineWidth: number = 2; // 线条宽度
|
||||
spiralLineImage: any = null; // 电话线图片对象
|
||||
private spiralLineImagePromise: Promise<any> | null = null; // 图片加载Promise
|
||||
|
||||
constructor(
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) {
|
||||
super(canvas, ctx, options);
|
||||
// 在构造函数中启动图片加载
|
||||
this.loadSpiralLineImage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载电话线图片
|
||||
*/
|
||||
private loadSpiralLineImage() {
|
||||
if (!this.spiralLineImagePromise) {
|
||||
const imagePath = `/focusPages/shared/aeests/imgs/spiral-line.png`;
|
||||
this.spiralLineImagePromise = getImage(this.canvas, imagePath).then(
|
||||
(image) => {
|
||||
this.spiralLineImage = image;
|
||||
return image;
|
||||
},
|
||||
);
|
||||
}
|
||||
return this.spiralLineImagePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制线条识别内容
|
||||
*/
|
||||
async draw(lineData: LineRecognitionData) {
|
||||
if (!lineData || !lineData.referenceLines || !lineData.practiceBlocks) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保电话线图片已加载
|
||||
if (!this.spiralLineImage) {
|
||||
await this.loadSpiralLineImage();
|
||||
}
|
||||
|
||||
this.setPrintConfig();
|
||||
this.lineData = lineData;
|
||||
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,
|
||||
lineData: this.lineData,
|
||||
canvasWidth: this.canvasWidth,
|
||||
startY: this.currentY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制直线
|
||||
*/
|
||||
private drawStraightLine(
|
||||
ctx: RenderingContext,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
) {
|
||||
ctx.strokeStyle = this.lineStrokeStyle;
|
||||
ctx.lineWidth = this.lineWidth;
|
||||
ctx.setLineDash([]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x + width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制虚线
|
||||
*/
|
||||
private drawDashedLine(
|
||||
ctx: RenderingContext,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
) {
|
||||
ctx.strokeStyle = this.lineStrokeStyle;
|
||||
ctx.lineWidth = this.lineWidth;
|
||||
ctx.setLineDash([6, 4]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x + width, y);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制波浪线
|
||||
*/
|
||||
private drawWavyLine(
|
||||
ctx: RenderingContext,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
) {
|
||||
ctx.strokeStyle = this.lineStrokeStyle;
|
||||
ctx.lineWidth = this.lineWidth;
|
||||
ctx.setLineDash([]);
|
||||
ctx.beginPath();
|
||||
const radius = 8; // 波浪半径
|
||||
const turns = 5; // 波浪圈数
|
||||
const points = 120; // 点数
|
||||
const startX = x;
|
||||
const endX = x + width;
|
||||
|
||||
ctx.moveTo(startX, y);
|
||||
for (let i = 0; i <= points; i++) {
|
||||
const progress = i / points;
|
||||
const currentX = startX + (endX - startX) * progress;
|
||||
const angle = progress * turns * Math.PI * 2;
|
||||
const currentY = y + Math.sin(angle) * radius;
|
||||
ctx.lineTo(currentX, currentY);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制锯齿线
|
||||
*/
|
||||
private drawZigzagLine(
|
||||
ctx: RenderingContext,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
) {
|
||||
ctx.strokeStyle = this.lineStrokeStyle;
|
||||
ctx.lineWidth = this.lineWidth;
|
||||
ctx.setLineDash([]); // 实线
|
||||
ctx.beginPath();
|
||||
|
||||
const segmentLength = 12; // 每段长度
|
||||
const amplitude = 7; // 波峰/波谷高度
|
||||
|
||||
// 第一个点为波峰顶点
|
||||
let currentX = x;
|
||||
let currentY = y - amplitude;
|
||||
ctx.moveTo(currentX, currentY);
|
||||
|
||||
// 计算一共能画多少完整段,两点之间斜线斜率为±1(45度)
|
||||
const nSegments = Math.ceil(width / segmentLength);
|
||||
|
||||
for (let i = 0; i < nSegments; i++) {
|
||||
// 下一点横坐标
|
||||
let nextX = currentX + segmentLength;
|
||||
// 最后一个点不能超过终点,且如正好到终点,高度需计算
|
||||
if (nextX > x + width) {
|
||||
nextX = x + width;
|
||||
}
|
||||
// 交替:偶数索引连向波谷,奇数索引连向波峰
|
||||
let nextY;
|
||||
if (i % 2 === 0) {
|
||||
nextY = y + amplitude; // 波谷
|
||||
} else {
|
||||
nextY = y - amplitude; // 波峰
|
||||
}
|
||||
// 如果已经到最后一段,最后一个点应当总是在波谷
|
||||
if (i === nSegments - 1) {
|
||||
nextY = y + amplitude;
|
||||
}
|
||||
|
||||
ctx.lineTo(nextX, nextY);
|
||||
|
||||
currentX = nextX;
|
||||
currentY = nextY;
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制电话线(使用图片)
|
||||
*/
|
||||
private drawSpiralLine(
|
||||
ctx: RenderingContext,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
) {
|
||||
// 使用预加载的图片对象
|
||||
if (!this.spiralLineImage) {
|
||||
console.warn('电话线图片未加载完成');
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算图片高度,保持原始宽高比,或者使用固定高度
|
||||
const imageHeight = 22; // 固定高度,与线条宽度匹配
|
||||
const drawY = y - imageHeight / 2; // 垂直居中
|
||||
|
||||
// 绘制图片
|
||||
ctx.drawImage(this.spiralLineImage, x, drawY, width, imageHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制线条
|
||||
*/
|
||||
private drawLine(
|
||||
ctx: RenderingContext,
|
||||
lineType: LineType,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
) {
|
||||
switch (lineType) {
|
||||
case 'straight':
|
||||
this.drawStraightLine(ctx, x, y, width);
|
||||
break;
|
||||
case 'dashed':
|
||||
this.drawDashedLine(ctx, x, y, width);
|
||||
break;
|
||||
case 'wavy':
|
||||
this.drawWavyLine(ctx, x, y, width);
|
||||
break;
|
||||
case 'zigzag':
|
||||
this.drawZigzagLine(ctx, x, y, width);
|
||||
break;
|
||||
case 'spiral':
|
||||
this.drawSpiralLine(ctx, x, y, width);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制内容区域
|
||||
*/
|
||||
private async drawContent(params: {
|
||||
canvas: WechatMiniprogram.Canvas;
|
||||
ctx: RenderingContext;
|
||||
lineData: LineRecognitionData;
|
||||
canvasWidth: number;
|
||||
startY: number;
|
||||
}): Promise<void> {
|
||||
const { ctx, lineData, canvasWidth } = params;
|
||||
let { startY } = params;
|
||||
|
||||
startY = startY + 25;
|
||||
|
||||
const margin = 40; // 左右边距
|
||||
const referenceBlockWidth = 140; // 预览区域色块宽度(加长)
|
||||
const referenceBlockHeight = 35; // 预览区域色块高度(缩减)
|
||||
const referenceBlockSpacing = 30; // 色块间距
|
||||
const textHeight = 30; // 文字高度
|
||||
const lineMargin = 15; // 线条距离色块边缘的距离
|
||||
const rowSpacing = 15; // 行间距
|
||||
|
||||
// 绘制预览区域(两行:第一行2个,第二行3个)
|
||||
const referenceStartY = startY;
|
||||
|
||||
// 第一行:2个色块
|
||||
const firstRowWidth = 2 * referenceBlockWidth + referenceBlockSpacing;
|
||||
const firstRowStartX =
|
||||
margin + (canvasWidth - margin * 2 - firstRowWidth) / 2;
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const config = lineData.referenceLines[i];
|
||||
const blockX =
|
||||
firstRowStartX +
|
||||
i * (referenceBlockWidth + referenceBlockSpacing);
|
||||
const blockY = referenceStartY;
|
||||
|
||||
// 绘制色块(无边框)
|
||||
ctx.fillStyle = config.color;
|
||||
ctx.fillRect(
|
||||
blockX,
|
||||
blockY,
|
||||
referenceBlockWidth,
|
||||
referenceBlockHeight,
|
||||
);
|
||||
|
||||
// 绘制线条
|
||||
const lineX = blockX + lineMargin;
|
||||
const lineY = blockY + referenceBlockHeight / 2;
|
||||
const lineWidth = referenceBlockWidth - lineMargin * 2;
|
||||
this.drawLine(ctx, config.type, lineX, lineY, lineWidth);
|
||||
|
||||
// 绘制文字标签
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = 'bold 16px "Microsoft Yahei"';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(
|
||||
config.name,
|
||||
blockX + referenceBlockWidth / 2,
|
||||
blockY + referenceBlockHeight + 5,
|
||||
);
|
||||
}
|
||||
|
||||
// 第二行:3个色块
|
||||
const secondRowWidth =
|
||||
3 * referenceBlockWidth + 2 * referenceBlockSpacing;
|
||||
const secondRowStartX =
|
||||
margin + (canvasWidth - margin * 2 - secondRowWidth) / 2;
|
||||
const secondRowY =
|
||||
referenceStartY + referenceBlockHeight + textHeight + rowSpacing;
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const config = lineData.referenceLines[i + 2];
|
||||
const blockX =
|
||||
secondRowStartX +
|
||||
i * (referenceBlockWidth + referenceBlockSpacing);
|
||||
const blockY = secondRowY;
|
||||
|
||||
// 绘制色块(无边框)
|
||||
ctx.fillStyle = config.color;
|
||||
ctx.fillRect(
|
||||
blockX,
|
||||
blockY,
|
||||
referenceBlockWidth,
|
||||
referenceBlockHeight,
|
||||
);
|
||||
|
||||
// 绘制线条
|
||||
const lineX = blockX + lineMargin;
|
||||
const lineY = blockY + referenceBlockHeight / 2;
|
||||
const lineWidth = referenceBlockWidth - lineMargin * 2;
|
||||
this.drawLine(ctx, config.type, lineX, lineY, lineWidth);
|
||||
|
||||
// 绘制文字标签
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = 'bold 16px "Microsoft Yahei"';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(
|
||||
config.name,
|
||||
blockX + referenceBlockWidth / 2,
|
||||
blockY + referenceBlockHeight + 5,
|
||||
);
|
||||
}
|
||||
|
||||
// 绘制虚线分割线
|
||||
const dividerY = secondRowY + referenceBlockHeight + textHeight + 30;
|
||||
this.drawDashedDivider(dividerY, margin);
|
||||
|
||||
const practiceMargin = 60;
|
||||
// 绘制练习区域(2列×8行)
|
||||
const practiceStartY = dividerY + 30;
|
||||
// 练习区域色块尺寸与预览区域一致
|
||||
const practiceBlockWidth = referenceBlockWidth + 50; // 140
|
||||
const practiceBlockHeight = referenceBlockHeight + 5; // 35
|
||||
const practiceRowHeight = 60; // 每行高度(增加间距,确保不重叠)
|
||||
const practiceColWidth = (canvasWidth - practiceMargin * 2 - 30) / 2; // 列宽度(减去列间距)
|
||||
const practiceColSpacing = 30; // 列间距
|
||||
|
||||
for (let row = 0; row < 8; row++) {
|
||||
for (let col = 0; col < 2; col++) {
|
||||
const blockIndex = row * 2 + col;
|
||||
const block = lineData.practiceBlocks[blockIndex];
|
||||
if (!block) continue;
|
||||
|
||||
// 计算基础位置(考虑错落感)
|
||||
const baseX =
|
||||
practiceMargin +
|
||||
col * (practiceColWidth + practiceColSpacing);
|
||||
const baseY = practiceStartY + row * practiceRowHeight;
|
||||
|
||||
// 添加随机错落感(限制范围,确保不重叠)
|
||||
const blockX = baseX + block.x;
|
||||
const blockY = baseY + block.y;
|
||||
|
||||
// 绘制色块(无边框)
|
||||
ctx.fillStyle = block.color;
|
||||
ctx.fillRect(
|
||||
blockX,
|
||||
blockY,
|
||||
practiceBlockWidth,
|
||||
practiceBlockHeight,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default LineRecognitionDraw;
|
||||
Reference in New Issue
Block a user