feat(elevator): 为电梯状态渲染添加动画效果,包括虚线流动、呼吸脉动和方向箭头移动,提升视觉反馈

This commit is contained in:
xudan 2025-12-11 16:26:53 +08:00
parent 1fe2df5fc6
commit 1294a437b4
2 changed files with 181 additions and 37 deletions

View File

@ -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;

View File

@ -47,6 +47,10 @@ export const useElevatorStore = defineStore('elevator', () => {
const elevators = ref<Map<string, ElevatorData>>(new Map());
const elevatorPoints = ref<Map<string, ElevatorPoint>>(new Map());
const editorService = ref<EditorService | null>(null);
// 动画相关状态
const movingElevators = ref<Set<string>>(new Set());
const animationFrameId = ref<number | null>(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);