/** * 线条识别绘制服务 */ import { BaseDrawService } from '../../../core/draw/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 | null = null; // 图片加载Promise constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { 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.lineData = lineData; this.prepareDraw(); await this.drawHeaderAndDivider(); 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 { 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;