feat(elevator): 为电梯状态渲染添加动画效果,包括虚线流动、呼吸脉动和方向箭头移动,提升视觉反馈
This commit is contained in:
parent
1fe2df5fc6
commit
1294a437b4
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user