feat(elevator): 使用Canvas绘制电梯图标并集成动画效果,移除外部边框和箭头以简化状态显示

This commit is contained in:
xudan 2025-12-11 18:13:04 +08:00
parent d975357f04
commit 0a1608262d
2 changed files with 277 additions and 123 deletions

View File

@ -53,10 +53,265 @@ function drawDisconnectedIcon(ctx: CanvasRenderingContext2D, cx: number, cy: num
ctx.restore();
}
/**
* 使Canvas绘制电梯图标48*60
* @param ctx Canvas上下文
* @param cx X坐标
* @param cy Y坐标
* @param width 48
* @param height 60
* @param doorStatus 1-2-3-
* @param time
* @param direction 0-1-2-3-
*/
function drawElevatorIcon(
ctx: CanvasRenderingContext2D,
cx: number,
cy: number,
width: number = 48,
height: number = 60,
doorStatus: number = 1,
time: number = 0,
direction: number = 0
): void {
const x = cx - width / 2;
const y = cy - height / 2;
// 电梯外框 - 金属质感
const gradient = ctx.createLinearGradient(x, y, x + width, y + height);
gradient.addColorStop(0, '#9ca3af'); // 浅灰色
gradient.addColorStop(0.5, '#6b7280'); // 中灰色
gradient.addColorStop(1, '#4b5563'); // 深灰色
ctx.fillStyle = gradient;
ctx.fillRect(x, y, width, height);
// 电梯内框 - 深金属色
const innerGradient = ctx.createLinearGradient(x + 2, y + 2, x + width - 2, y + height - 2);
innerGradient.addColorStop(0, '#374151');
innerGradient.addColorStop(0.5, '#1f2937');
innerGradient.addColorStop(1, '#111827');
ctx.fillStyle = innerGradient;
ctx.fillRect(x + 2, y + 2, width - 4, height - 4);
// 计算门的开合程度0-1之间
let doorProgress = 0;
if (doorStatus === 1) {
doorProgress = 0; // 完全关闭
} else if (doorStatus === 3) {
doorProgress = 1; // 完全打开
} else if (doorStatus === 2) {
// 正在开关门,使用动画
const animationDuration = 2000; // 2秒完成开关门
const phase = (time % animationDuration) / animationDuration;
// 使用正弦函数创建平滑的往复运动
doorProgress = (Math.sin(phase * Math.PI * 2 - Math.PI / 2) + 1) / 2;
}
// 门的位置
const doorWidth = (width - 8) / 2;
const doorHeight = height - 8;
const doorX = x + 4;
const doorY = y + 4;
const doorGap = doorWidth * doorProgress; // 两扇门之间的间隙
// 保持金属质感,不根据运动状态改变门的颜色
const doorColor1 = '#94a3b8'; // 浅金属蓝灰
const doorColor2 = '#64748b'; // 中金属蓝灰
const doorColor3 = '#475569'; // 深金属蓝灰
const doorColor4 = '#334155'; // 最深金属蓝灰
// 绘制左门 - 金属门质感(无门把手)
if (doorWidth - doorGap / 2 > 0) {
// 门主体渐变
const doorGradient = ctx.createLinearGradient(doorX, doorY, doorX + doorWidth - doorGap / 2, doorY + doorHeight);
doorGradient.addColorStop(0, doorColor1);
doorGradient.addColorStop(0.3, doorColor2);
doorGradient.addColorStop(0.7, doorColor3);
doorGradient.addColorStop(1, doorColor4);
ctx.fillStyle = doorGradient;
ctx.fillRect(doorX, doorY, doorWidth - doorGap / 2, doorHeight);
// 门边缘高光
ctx.strokeStyle = 'rgba(203, 213, 225, 0.6)';
ctx.lineWidth = 1;
ctx.strokeRect(doorX + 0.5, doorY + 0.5, doorWidth - doorGap / 2 - 1, doorHeight - 1);
// 如果在运动中,添加动态线条效果
if (direction === 2 || direction === 3) {
ctx.save();
ctx.strokeStyle = direction === 2 ? 'rgba(16, 185, 129, 0.4)' : 'rgba(239, 68, 68, 0.4)';
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
const lineOffset = (time % 1000) / 1000 * 6;
ctx.lineDashOffset = direction === 2 ? -lineOffset : lineOffset;
// 绘制动态线条
for (let i = 0; i < 3; i++) {
const lineY = doorY + doorHeight * (0.3 + i * 0.2);
ctx.beginPath();
ctx.moveTo(doorX + 5, lineY);
ctx.lineTo(doorX + doorWidth - doorGap / 2 - 5, lineY);
ctx.stroke();
}
ctx.restore();
}
}
// 绘制右门 - 金属门质感(无门把手)
if (doorWidth - doorGap / 2 > 0) {
// 门主体渐变
const doorGradient = ctx.createLinearGradient(doorX + doorWidth + doorGap / 2, doorY, doorX + doorWidth * 2 + doorGap / 2, doorY + doorHeight);
doorGradient.addColorStop(0, doorColor1);
doorGradient.addColorStop(0.3, doorColor2);
doorGradient.addColorStop(0.7, doorColor3);
doorGradient.addColorStop(1, doorColor4);
ctx.fillStyle = doorGradient;
ctx.fillRect(doorX + doorWidth + doorGap / 2, doorY, doorWidth - doorGap / 2, doorHeight);
// 门边缘高光
ctx.strokeStyle = 'rgba(203, 213, 225, 0.6)';
ctx.lineWidth = 1;
ctx.strokeRect(doorX + doorWidth + doorGap / 2 + 0.5, doorY + 0.5, doorWidth - doorGap / 2 - 1, doorHeight - 1);
// 如果在运动中,添加动态线条效果
if (direction === 2 || direction === 3) {
ctx.save();
ctx.strokeStyle = direction === 2 ? 'rgba(16, 185, 129, 0.4)' : 'rgba(239, 68, 68, 0.4)';
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
const lineOffset = (time % 1000) / 1000 * 6;
ctx.lineDashOffset = direction === 2 ? -lineOffset : lineOffset;
// 绘制动态线条
for (let i = 0; i < 3; i++) {
const lineY = doorY + doorHeight * (0.3 + i * 0.2);
ctx.beginPath();
ctx.moveTo(doorX + doorWidth + doorGap / 2 + 5, lineY);
ctx.lineTo(doorX + doorWidth * 2 - 5, lineY);
ctx.stroke();
}
ctx.restore();
}
}
// 绘制电梯顶部指示器 - 基于电梯宽度自适应,增加距离避免被遮挡
const indicatorWidth = width * 0.6; // 指示器宽度为电梯宽度的60%
const indicatorHeight = Math.max(6, width * 0.12); // 指示器高度为电梯宽度的12%最小6px
const indicatorY = y - Math.max(10, width * 0.25); // 增加距离电梯宽度的25%最小10px
// 顶部指示器背景 - 金属质感
const indicatorGradient = ctx.createLinearGradient(x + width/2 - indicatorWidth/2, indicatorY, x + width/2 + indicatorWidth/2, indicatorY + indicatorHeight);
indicatorGradient.addColorStop(0, '#6b7280');
indicatorGradient.addColorStop(0.5, '#4b5563');
indicatorGradient.addColorStop(1, '#374151');
ctx.fillStyle = indicatorGradient;
ctx.fillRect(x + width/2 - indicatorWidth/2, indicatorY, indicatorWidth, indicatorHeight);
// 根据方向在顶部指示器内显示状态,使用更大的箭头
if (direction === 2) {
// 向上状态 - 绿色LED效果
// 绘制发光背景
const glowGradient = ctx.createRadialGradient(cx, indicatorY + indicatorHeight/2, 0, cx, indicatorY + indicatorHeight/2, indicatorHeight * 1.5);
glowGradient.addColorStop(0, 'rgba(16, 185, 129, 0.4)');
glowGradient.addColorStop(1, 'rgba(16, 185, 129, 0)');
ctx.fillStyle = glowGradient;
ctx.fillRect(x + width/2 - indicatorWidth/2 - 3, indicatorY - 3, indicatorWidth + 6, indicatorHeight + 6);
// 绘制向上箭头 - 更大更明显
ctx.fillStyle = '#10b981';
ctx.shadowColor = '#10b981';
ctx.shadowBlur = 8;
// 使用更大的箭头,占据整个指示器高度
const arrowSize = Math.min(indicatorWidth * 0.4, indicatorHeight * 0.9);
const arrowTop = indicatorY + indicatorHeight * 0.1;
const arrowBottom = indicatorY + indicatorHeight * 0.9;
// 主箭头 - 加粗版本
ctx.beginPath();
ctx.moveTo(cx, arrowTop);
ctx.lineTo(cx - arrowSize, arrowBottom);
ctx.lineTo(cx - arrowSize * 0.4, arrowBottom - arrowSize * 0.2);
ctx.lineTo(cx, arrowBottom - arrowSize * 0.3);
ctx.lineTo(cx + arrowSize * 0.4, arrowBottom - arrowSize * 0.2);
ctx.lineTo(cx + arrowSize, arrowBottom);
ctx.closePath();
ctx.fill();
ctx.shadowBlur = 0;
} else if (direction === 3) {
// 向下状态 - 红色LED效果
// 绘制发光背景
const glowGradient = ctx.createRadialGradient(cx, indicatorY + indicatorHeight/2, 0, cx, indicatorY + indicatorHeight/2, indicatorHeight * 1.5);
glowGradient.addColorStop(0, 'rgba(239, 68, 68, 0.4)');
glowGradient.addColorStop(1, 'rgba(239, 68, 68, 0)');
ctx.fillStyle = glowGradient;
ctx.fillRect(x + width/2 - indicatorWidth/2 - 3, indicatorY - 3, indicatorWidth + 6, indicatorHeight + 6);
// 绘制向下箭头 - 更大更明显
ctx.fillStyle = '#ef4444';
ctx.shadowColor = '#ef4444';
ctx.shadowBlur = 8;
// 使用更大的箭头,占据整个指示器高度
const arrowSize = Math.min(indicatorWidth * 0.4, indicatorHeight * 0.9);
const arrowTop = indicatorY + indicatorHeight * 0.1;
const arrowBottom = indicatorY + indicatorHeight * 0.9;
// 主箭头 - 加粗版本
ctx.beginPath();
ctx.moveTo(cx, arrowBottom);
ctx.lineTo(cx - arrowSize, arrowTop);
ctx.lineTo(cx - arrowSize * 0.4, arrowTop + arrowSize * 0.2);
ctx.lineTo(cx, arrowTop + arrowSize * 0.3);
ctx.lineTo(cx + arrowSize * 0.4, arrowTop + arrowSize * 0.2);
ctx.lineTo(cx + arrowSize, arrowTop);
ctx.closePath();
ctx.fill();
ctx.shadowBlur = 0;
} else if (direction === 1) {
// 停止状态 - 黄色LED效果
// 绘制发光背景
const glowGradient = ctx.createRadialGradient(cx, indicatorY + indicatorHeight/2, 0, cx, indicatorY + indicatorHeight/2, indicatorHeight);
glowGradient.addColorStop(0, 'rgba(245, 158, 11, 0.3)');
glowGradient.addColorStop(1, 'rgba(245, 158, 11, 0)');
ctx.fillStyle = glowGradient;
ctx.fillRect(x + width/2 - indicatorWidth/2 - 2, indicatorY - 2, indicatorWidth + 4, indicatorHeight + 4);
// 绘制停止标志
ctx.fillStyle = '#f59e0b';
ctx.shadowColor = '#f59e0b';
ctx.shadowBlur = 6;
const stopSize = Math.min(indicatorHeight * 0.6, indicatorWidth * 0.18);
// 绘制矩形停止标志
ctx.fillRect(cx - stopSize/2, indicatorY + (indicatorHeight - stopSize)/2, stopSize, stopSize);
// 添加脉冲效果
const pulsePhase = (time % 2000) / 2000 * Math.PI * 2;
const pulseAlpha = 0.3 + 0.2 * Math.sin(pulsePhase);
ctx.fillStyle = `rgba(245, 158, 11, ${pulseAlpha})`;
ctx.fillRect(cx - stopSize/2 - 2, indicatorY + (indicatorHeight - stopSize)/2 - 2, stopSize + 4, stopSize + 4);
ctx.shadowBlur = 0;
}
// 添加电梯边框 - 金属边缘效果
ctx.strokeStyle = '#1f2937';
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);
// 添加金属边缘高光
ctx.strokeStyle = 'rgba(156, 163, 175, 0.4)';
ctx.lineWidth = 1;
ctx.strokeRect(x + 1, y + 1, width - 2, height - 2);
}
/**
* -
* 1. 使
* 2. 48*60
* 2. Canvas绘制电梯图标使48*60
* 3. pen元素宽高动态计算
* 4.
* - 线线
@ -66,7 +321,7 @@ function drawDisconnectedIcon(ctx: CanvasRenderingContext2D, cx: number, cy: num
* - 绿/
* -
* -
* 5.
* 5.
*
*
* - 4pxpen较小边的15%
@ -163,131 +418,29 @@ function drawElevatorMinimal(
// 辅助函数:在边框右侧绘制动态方向箭头(根据时间动画)
const drawDirectionSide = (direction: 'up' | 'down', color: string, time: number) => {
ctx.save();
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.globalAlpha = 0.8;
// 箭头大小基于pen尺寸动态调整分别考虑宽度和高度
const arrowSize = Math.max(5, Math.min(w, h) * 0.20); // 使用较小的边作为基准最小5px否则按20%比例
const sideMargin = Math.max(5, Math.min(w, h) * 0.20); // 距离右侧边框的距离,增加间距,使用较小的边作为基准
ctx.lineWidth = Math.max(2, Math.min(w, h) * 0.04); // 箭头线条宽度,使用较小的边作为基准
// 箭头位置:在矩形右侧垂直居中
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, animatedCenterY + arrowSize);
ctx.lineTo(arrowX, animatedCenterY - arrowSize);
// 箭头尖端(三角形)
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, 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, animatedCenterY - arrowSize);
ctx.lineTo(arrowX, animatedCenterY + arrowSize);
// 箭头尖端(三角形)
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, 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();
}
ctx.restore();
};
// 箭头绘制函数已移除,因为箭头现在集成在电梯图标内部
// 方向指示通过电梯顶部的LED指示器显示
// 状态渲染逻辑 - 根据需求优化配色方案
// 绘制电梯图标(包含内部的方向指示器)
drawElevatorIcon(
ctx,
x + w / 2,
y + h / 2,
Math.min(48, w * 0.8), // 根据pen大小调整电梯图标宽度最大48像素
Math.min(60, h * 0.8), // 根据pen大小调整电梯图标高度最大60像素
elevatorFrontDoorStatus,
time,
elevatorDirection
);
// 状态渲染逻辑 - 只有断连状态才显示边框
if (!isConnected) {
// 离线状态 - 红色虚线(静态,不带动画)
drawMinimalFrame('#FF4757', 0.8, true, time);
} else {
// 在线状态根据具体状态渲染
switch (elevatorFrontDoorStatus) {
case 2: { // 正在开关门 - 蓝色虚线边框 + 绿色箭头
// 蓝色虚线边框表示操作中(带动画)
drawMinimalFrame('#69c6f5', 0.8, true, time);
if (elevatorDirection === 2) { // 向上
drawDirectionSide('up', '#00D26A', time); // 绿色箭头
} else if (elevatorDirection === 3) { // 向下
drawDirectionSide('down', '#00D26A', time); // 绿色箭头
}
break;
}
case 3: { // 门已开 - 实线绿色边框 + 绿色箭头
// 实线绿色边框表示可通行状态
drawMinimalFrame('#00D26A', 0.8, false, time);
if (elevatorDirection === 2) { // 向上
drawDirectionSide('up', '#00D26A', time); // 绿色箭头
} else if (elevatorDirection === 3) { // 向下
drawDirectionSide('down', '#00D26A', time); // 绿色箭头
}
break;
}
case 1: { // 门已关 - 根据方向显示
if (elevatorDirection === 2) { // 向上
// 蓝色边框 + 绿色向上箭头
drawMinimalFrame('#69c6f5', 0.8, false, time);
drawDirectionSide('up', '#00D26A', time); // 绿色箭头
} else if (elevatorDirection === 3) { // 向下
// 蓝色边框 + 绿色向下箭头
drawMinimalFrame('#69c6f5', 0.8, false, time);
drawDirectionSide('down', '#00D26A', time); // 绿色箭头
} else { // 停止
// 金色静态边框 - 中性状态
drawMinimalFrame('#FFB700', 0.7, false, time);
}
break;
}
default: { // 未知状态
// 灰色静态边框
drawMinimalFrame('#8B8B8B', 0.5, false, time);
break;
}
}
}
// 其他状态(在线)不需要额外的边框和箭头,所有信息都集成在电梯图标内部
}
/**
@ -524,9 +677,9 @@ export function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
ctx.strokeStyle = largeTypeStrokeColor || largeGeneralStrokeColor || largeThemeStrokeColor || '';
ctx.stroke();
// 电梯点:使用极简状态渲染
// 电梯点使用Canvas绘制的电梯图标
if (type === MapPointType.) {
// 直接传入包含后端字段的 pen.point 数据
// 绘制Canvas电梯图标和状态
if (pen.point) {
drawElevatorMinimal(ctx, pen, pen.point, currentTime);
}

View File

@ -111,7 +111,8 @@ export class PointManager {
private mapPointImage(type: MapPointType): Required<Pick<MapPen, 'image' | 'canvasLayer'>> {
const theme = this.editor.data().theme;
const image = type < 10 ? '' : `${import.meta.env.BASE_URL}/point/${type}-${theme}.png`;
// 电梯点(type=11)不加载默认图标使用Canvas绘制
const image = type < 10 || type === MapPointType. ? '' : `${import.meta.env.BASE_URL}/point/${type}-${theme}.png`;
return { image, canvasLayer: CanvasLayer.CanvasMain };
}
}