diff --git a/src/services/editor/editor-drawers.ts b/src/services/editor/editor-drawers.ts index 655d926..d5b054e 100644 --- a/src/services/editor/editor-drawers.ts +++ b/src/services/editor/editor-drawers.ts @@ -84,6 +84,7 @@ function drawElevatorMinimal( ctx: CanvasRenderingContext2D, pen: MapPen, elevatorData: any, + time: number = 0, ): void { // 提取后端字段 const { @@ -106,8 +107,8 @@ function drawElevatorMinimal( const rectW = w + padding * 2; const rectH = h + padding * 2; - // 辅助函数:绘制极简矩形边框 - const drawMinimalFrame = (color: string, alpha: number = 1, useDashedLine: boolean = false) => { + // 辅助函数:绘制极简矩形边框(支持动画虚线) + const drawMinimalFrame = (color: string, alpha: number = 1, useDashedLine: boolean = false, time: number = 0) => { ctx.save(); // 设置边框样式 @@ -120,6 +121,36 @@ function drawElevatorMinimal( const dashLength = Math.max(3, baseSize * 0.08); // 虚线长度:最小3px,否则按8%比例 const gapLength = Math.max(3, baseSize * 0.05); // 间隙长度:最小3px,否则按5%比例 ctx.setLineDash([dashLength, gapLength]); + + // 虚线流动动画:当电梯正在开关门时(elevatorFrontDoorStatus === 2) + if (elevatorFrontDoorStatus === 2) { + // 计算虚线流动偏移 + const dashPatternLength = dashLength + gapLength; + const flowSpeed = 0.05; // 流动速度(像素/毫秒) + const flowOffset = (time * flowSpeed) % dashPatternLength; + ctx.lineDashOffset = -flowOffset; // 负值使虚线顺时针流动 + + // 添加呼吸效果:透明度周期性变化 + const breathPeriod = 2000; // 呼吸周期2000ms + const breathPhase = (time % breathPeriod) / breathPeriod; + const breathAlpha = 0.6 + 0.2 * Math.sin(breathPhase * 2 * Math.PI); + ctx.globalAlpha = alpha * breathAlpha; + + // 添加颜色脉动效果:蓝色深浅变化 + if (color === '#69c6f5') { + const colorPulse = 0.8 + 0.2 * Math.sin(breathPhase * 2 * Math.PI + Math.PI / 4); + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + const pulsedR = Math.min(255, Math.floor(r * colorPulse)); + const pulsedG = Math.min(255, Math.floor(g * colorPulse)); + const pulsedB = Math.min(255, Math.floor(b * colorPulse)); + ctx.strokeStyle = `rgb(${pulsedR}, ${pulsedG}, ${pulsedB})`; + } + } else { + // 其他虚线状态(如离线状态)保持静态 + ctx.lineDashOffset = 0; + } } // 绘制圆角矩形 @@ -132,8 +163,8 @@ function drawElevatorMinimal( - // 辅助函数:在边框右侧绘制静态方向箭头 - const drawDirectionSide = (direction: 'up' | 'down', color: string) => { + // 辅助函数:在边框右侧绘制动态方向箭头(根据时间动画) + const drawDirectionSide = (direction: 'up' | 'down', color: string, time: number) => { ctx.save(); ctx.fillStyle = color; ctx.strokeStyle = color; @@ -147,43 +178,55 @@ function drawElevatorMinimal( // 箭头位置:在矩形右侧垂直居中 const arrowX = rectX + rectW + sideMargin; const centerY = y + h / 2; // 垂直居中 + // 动画偏移:当电梯上下移动时,箭头沿方向移动 + let offset = 0; + if (elevatorDirection === 2 || elevatorDirection === 3) { + // 周期1000ms,振幅为箭头大小的50% + const amplitude = arrowSize * 0.5; + const period = 1000; // 毫秒 + const phase = (time % period) / period; + // 正弦波产生平滑往复运动,方向决定符号 + const sign = direction === 'up' ? -1 : 1; + offset = Math.sin(phase * 2 * Math.PI) * amplitude * sign; + } + const animatedCenterY = centerY + offset; if (direction === 'up') { // 向上箭头 - 在右侧绘制 ctx.beginPath(); // 箭头主体(竖线) - ctx.moveTo(arrowX, centerY + arrowSize); - ctx.lineTo(arrowX, centerY - arrowSize); + ctx.moveTo(arrowX, animatedCenterY + arrowSize); + ctx.lineTo(arrowX, animatedCenterY - arrowSize); // 箭头尖端(三角形) - ctx.lineTo(arrowX - arrowSize * 0.6, centerY - arrowSize * 0.4); - ctx.moveTo(arrowX, centerY - arrowSize); - ctx.lineTo(arrowX + arrowSize * 0.6, centerY - arrowSize * 0.4); + ctx.lineTo(arrowX - arrowSize * 0.6, animatedCenterY - arrowSize * 0.4); + ctx.moveTo(arrowX, animatedCenterY - arrowSize); + ctx.lineTo(arrowX + arrowSize * 0.6, animatedCenterY - arrowSize * 0.4); ctx.stroke(); // 填充箭头头部 ctx.beginPath(); - ctx.moveTo(arrowX, centerY - arrowSize); - ctx.lineTo(arrowX - arrowSize * 0.5, centerY - arrowSize * 0.7); - ctx.lineTo(arrowX + arrowSize * 0.5, centerY - arrowSize * 0.7); + ctx.moveTo(arrowX, animatedCenterY - arrowSize); + ctx.lineTo(arrowX - arrowSize * 0.5, animatedCenterY - arrowSize * 0.7); + ctx.lineTo(arrowX + arrowSize * 0.5, animatedCenterY - arrowSize * 0.7); ctx.closePath(); ctx.fill(); } else { // 向下箭头 - 在右侧绘制 ctx.beginPath(); // 箭头主体(竖线) - ctx.moveTo(arrowX, centerY - arrowSize); - ctx.lineTo(arrowX, centerY + arrowSize); + ctx.moveTo(arrowX, animatedCenterY - arrowSize); + ctx.lineTo(arrowX, animatedCenterY + arrowSize); // 箭头尖端(三角形) - ctx.lineTo(arrowX - arrowSize * 0.6, centerY + arrowSize * 0.4); - ctx.moveTo(arrowX, centerY + arrowSize); - ctx.lineTo(arrowX + arrowSize * 0.6, centerY + arrowSize * 0.4); + ctx.lineTo(arrowX - arrowSize * 0.6, animatedCenterY + arrowSize * 0.4); + ctx.moveTo(arrowX, animatedCenterY + arrowSize); + ctx.lineTo(arrowX + arrowSize * 0.6, animatedCenterY + arrowSize * 0.4); ctx.stroke(); // 填充箭头头部 ctx.beginPath(); - ctx.moveTo(arrowX, centerY + arrowSize); - ctx.lineTo(arrowX - arrowSize * 0.5, centerY + arrowSize * 0.7); - ctx.lineTo(arrowX + arrowSize * 0.5, centerY + arrowSize * 0.7); + ctx.moveTo(arrowX, animatedCenterY + arrowSize); + ctx.lineTo(arrowX - arrowSize * 0.5, animatedCenterY + arrowSize * 0.7); + ctx.lineTo(arrowX + arrowSize * 0.5, animatedCenterY + arrowSize * 0.7); ctx.closePath(); ctx.fill(); } @@ -195,29 +238,29 @@ function drawElevatorMinimal( // 状态渲染逻辑 - 根据需求优化配色方案 if (!isConnected) { - // 离线状态 - 红色虚线 - drawMinimalFrame('#FF4757', 0.8, true); + // 离线状态 - 红色虚线(静态,不带动画) + drawMinimalFrame('#FF4757', 0.8, true, time); } else { // 在线状态根据具体状态渲染 switch (elevatorFrontDoorStatus) { case 2: { // 正在开关门 - 蓝色虚线边框 + 绿色箭头 - // 蓝色虚线边框表示操作中 - drawMinimalFrame('#69c6f5', 0.8, true); + // 蓝色虚线边框表示操作中(带动画) + drawMinimalFrame('#69c6f5', 0.8, true, time); if (elevatorDirection === 2) { // 向上 - drawDirectionSide('up', '#00D26A'); // 绿色箭头 + drawDirectionSide('up', '#00D26A', time); // 绿色箭头 } else if (elevatorDirection === 3) { // 向下 - drawDirectionSide('down', '#00D26A'); // 绿色箭头 + drawDirectionSide('down', '#00D26A', time); // 绿色箭头 } break; } case 3: { // 门已开 - 实线绿色边框 + 绿色箭头 // 实线绿色边框表示可通行状态 - drawMinimalFrame('#00D26A', 0.8); + drawMinimalFrame('#00D26A', 0.8, false, time); if (elevatorDirection === 2) { // 向上 - drawDirectionSide('up', '#00D26A'); // 绿色箭头 + drawDirectionSide('up', '#00D26A', time); // 绿色箭头 } else if (elevatorDirection === 3) { // 向下 - drawDirectionSide('down', '#00D26A'); // 绿色箭头 + drawDirectionSide('down', '#00D26A', time); // 绿色箭头 } break; } @@ -225,22 +268,22 @@ function drawElevatorMinimal( case 1: { // 门已关 - 根据方向显示 if (elevatorDirection === 2) { // 向上 // 蓝色边框 + 绿色向上箭头 - drawMinimalFrame('#69c6f5', 0.8); - drawDirectionSide('up', '#00D26A'); // 绿色箭头 + drawMinimalFrame('#69c6f5', 0.8, false, time); + drawDirectionSide('up', '#00D26A', time); // 绿色箭头 } else if (elevatorDirection === 3) { // 向下 // 蓝色边框 + 绿色向下箭头 - drawMinimalFrame('#69c6f5', 0.8); - drawDirectionSide('down', '#00D26A'); // 绿色箭头 + drawMinimalFrame('#69c6f5', 0.8, false, time); + drawDirectionSide('down', '#00D26A', time); // 绿色箭头 } else { // 停止 // 金色静态边框 - 中性状态 - drawMinimalFrame('#FFB700', 0.7); + drawMinimalFrame('#FFB700', 0.7, false, time); } break; } default: { // 未知状态 // 灰色静态边框 - drawMinimalFrame('#8B8B8B', 0.5); + drawMinimalFrame('#8B8B8B', 0.5, false, time); break; } } @@ -361,6 +404,7 @@ function drawDoorAreaDisconnectedPulse(ctx: CanvasRenderingContext2D, pen: MapPe } export function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { + const currentTime = performance.now(); const theme = sTheme.editor; const { active, iconSize: r = 0, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {}; const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {}; @@ -373,7 +417,6 @@ export function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { if (type === MapPointType.自动门点 && pointActive) { if (isConnected === false) { // 未连接:使用新的脉冲效果 - const currentTime = performance.now(); drawAutoDoorDisconnectedPulse(ctx, pen, currentTime); ctx.restore(); // 提前返回,不执行后续渲染 return; @@ -485,7 +528,7 @@ export function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { if (type === MapPointType.电梯点) { // 直接传入包含后端字段的 pen.point 数据 if (pen.point) { - drawElevatorMinimal(ctx, pen, pen.point); + drawElevatorMinimal(ctx, pen, pen.point, currentTime); } } break; diff --git a/src/stores/elevator.store.ts b/src/stores/elevator.store.ts index 0cf2295..f6e84f7 100644 --- a/src/stores/elevator.store.ts +++ b/src/stores/elevator.store.ts @@ -47,6 +47,10 @@ export const useElevatorStore = defineStore('elevator', () => { const elevators = ref>(new Map()); const elevatorPoints = ref>(new Map()); const editorService = ref(null); + // 动画相关状态 + const movingElevators = ref>(new Set()); + const animationFrameId = ref(null); + const frameCount = ref(0); // 调试用帧计数器 // ========== 方法 ========== @@ -61,6 +65,100 @@ export const useElevatorStore = defineStore('elevator', () => { console.log('🛗 电梯Store: 编辑器服务已设置,等待场景加载后构建映射'); }; + /** + * 清理无效的运动电梯(连接断开或方向非运动状态) + */ + const cleanupInvalidMovingElevators = () => { + let hasChanges = false; + + for (const deviceId of movingElevators.value) { + const elevatorData = elevators.value.get(deviceId); + // 如果电梯数据不存在、连接断开、或方向不是运动状态,则移除 + if (!elevatorData || !elevatorData.isConnected || + (elevatorData.elevatorDirection !== 2 && elevatorData.elevatorDirection !== 3)) { + movingElevators.value.delete(deviceId); + hasChanges = true; + console.log(`🛗 清理无效运动电梯: ${deviceId}, 连接: ${elevatorData?.isConnected}, 方向: ${elevatorData?.elevatorDirection}`); + } + } + + return hasChanges; + }; + + /** + * 启动动画循环(如果尚未启动) + */ + const startAnimationLoop = () => { + if (animationFrameId.value) return; // 循环已启动 + frameCount.value = 0; + const loop = () => { + frameCount.value++; + if (editorService.value) { + // 清理无效的运动电梯 + const hadInvalid = cleanupInvalidMovingElevators(); + + if (movingElevators.value.size > 0) { + // 有运动电梯,触发渲染 + editorService.value.render(); + + // 每60帧打印一次调试信息 + if (frameCount.value % 60 === 0) { + console.log(`🛗 动画循环运行中,帧: ${frameCount.value}, 运动电梯: ${movingElevators.value.size}`); + } + + animationFrameId.value = requestAnimationFrame(loop); + } else { + // 无运动电梯,停止循环 + if (hadInvalid) { + console.log('🛗 所有运动电梯已无效,停止动画循环'); + } + console.log(`🛗 停止电梯动画循环,总帧数: ${frameCount.value}`); + animationFrameId.value = null; + frameCount.value = 0; + } + } else { + animationFrameId.value = null; + frameCount.value = 0; + } + }; + animationFrameId.value = requestAnimationFrame(loop); + console.log('🛗 启动电梯动画循环'); + }; + + /** + * 停止动画循环 + */ + const stopAnimationLoop = () => { + if (animationFrameId.value) { + cancelAnimationFrame(animationFrameId.value); + animationFrameId.value = null; + } + }; + + /** + * 更新运动电梯集合,并根据需要启动/停止动画循环 + */ + const updateMovingElevators = (deviceId: string) => { + const elevatorData = elevators.value.get(deviceId); + const wasMoving = movingElevators.value.has(deviceId); + + // 判断是否为运动状态:连接正常且方向为上行(2)或下行(3) + const isMoving = elevatorData?.isConnected && + (elevatorData.elevatorDirection === 2 || elevatorData.elevatorDirection === 3); + + if (isMoving && !wasMoving) { + movingElevators.value.add(deviceId); + startAnimationLoop(); + console.log(`🛗 添加运动电梯: ${deviceId}, 方向: ${elevatorData.elevatorDirection}`); + } else if (!isMoving && wasMoving) { + movingElevators.value.delete(deviceId); + console.log(`🛗 移除运动电梯: ${deviceId}, 原因: ${elevatorData ? '连接断开或方向改变' : '数据不存在'}`); + if (movingElevators.value.size === 0) { + stopAnimationLoop(); + } + } + }; + /** * 构建设备ID到电梯点的映射 */ @@ -144,6 +242,9 @@ export const useElevatorStore = defineStore('elevator', () => { // 存储到Map elevators.value.set(deviceId, elevatorData); + // 更新运动电梯集合,控制动画循环 + updateMovingElevators(deviceId); + // 更新画布上的电梯点显示 if (elevatorData.penId && editorService.value) { updateElevatorPointDisplay(elevatorData);