feat(editor-drawers): 为断连的自动门点和门区域添加脉冲动画效果,提升离线状态的视觉反馈和用户体验

This commit is contained in:
xudan 2025-12-05 18:21:29 +08:00
parent 97bbd47e3c
commit 7dba81550e

View File

@ -12,6 +12,159 @@ __doorImgOpen.src = (doorOpenUrl as unknown as string) || '';
const __doorImgClosed = new Image(); const __doorImgClosed = new Image();
__doorImgClosed.src = (doorClosedUrl as unknown as string) || ''; __doorImgClosed.src = (doorClosedUrl as unknown as string) || '';
/**
*
* @param ctx Canvas上下文
* @param cx X坐标
* @param cy Y坐标
* @param size
* @param opacity
*/
function drawDisconnectedIcon(ctx: CanvasRenderingContext2D, cx: number, cy: number, size: number, opacity: number): void {
ctx.save();
// 绘制WiFi断连图标
ctx.strokeStyle = `rgba(255, 255, 255, ${opacity * 0.8})`;
ctx.fillStyle = `rgba(255, 255, 255, ${opacity * 0.8})`;
ctx.lineWidth = 1.5;
ctx.lineCap = 'round';
const radius = size / 2;
const baseY = cy - radius * 0.3;
// 绘制三层WiFi信号从外到内
for (let i = 2; i >= 0; i--) {
const r = radius * (0.2 + i * 0.25);
const startAngle = -Math.PI * 0.75;
const endAngle = -Math.PI * 0.25;
ctx.beginPath();
ctx.arc(cx, baseY, r, startAngle, endAngle);
ctx.stroke();
}
// 绘制删除线表示断连
ctx.beginPath();
ctx.moveTo(cx - radius * 0.6, baseY - radius * 0.4);
ctx.lineTo(cx + radius * 0.6, baseY + radius * 0.4);
ctx.stroke();
ctx.restore();
}
/**
*
* @param ctx Canvas上下文
* @param pen
* @param time
*/
function drawAutoDoorDisconnectedPulse(ctx: CanvasRenderingContext2D, pen: MapPen, time: number): void {
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
const base = Math.min(w, h);
const padding = Math.max(2, Math.min(10, base * 0.2));
// 使用平滑的正弦函数计算脉冲强度 (0.4 - 1.0)
// 使用更大的周期让变化更平缓,并添加相位偏移以避免突然变化
const pulseIntensity = 0.7 + 0.3 * Math.sin(time / 2500);
// 外层发光效果
const glowGradient = ctx.createRadialGradient(
x + w/2, y + h/2, 0,
x + w/2, y + h/2, base * 0.8
);
glowGradient.addColorStop(0, `rgba(255, 149, 0, ${pulseIntensity * 0.3})`);
glowGradient.addColorStop(0.5, `rgba(255, 149, 0, ${pulseIntensity * 0.15})`);
glowGradient.addColorStop(1, 'rgba(255, 149, 0, 0)');
ctx.fillStyle = glowGradient;
ctx.fillRect(x - padding * 2, y - padding * 2, w + padding * 4, h + padding * 4);
// 主体渐变光圈
const mainGradient = ctx.createLinearGradient(x, y, x + w, y + h);
mainGradient.addColorStop(0, `rgba(255, 149, 0, ${pulseIntensity * 0.8})`);
mainGradient.addColorStop(0.5, `rgba(244, 81, 30, ${pulseIntensity * 0.85})`);
mainGradient.addColorStop(1, `rgba(220, 38, 38, ${pulseIntensity * 0.9})`);
ctx.beginPath();
ctx.roundRect(x - padding, y - padding, w + padding * 2, h + padding * 2, 4);
ctx.fillStyle = mainGradient;
ctx.fill();
// 内边框高光
ctx.strokeStyle = `rgba(255, 255, 255, ${pulseIntensity * 0.3})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x - padding + 1, y - padding + 1, w + padding * 2 - 2, h + padding * 2 - 2, 3);
ctx.stroke();
// 断连图标遮罩
drawDisconnectedIcon(ctx, x + w/2, y + h/2, base * 0.3, pulseIntensity);
}
/**
*
* @param ctx Canvas上下文
* @param pen
* @param time
*/
function drawDoorAreaDisconnectedPulse(ctx: CanvasRenderingContext2D, pen: MapPen, time: number): void {
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
// 使用平滑的正弦函数计算脉冲强度 (0.4 - 1.0)
// 与自动门点保持一致的节奏,但略微错开相位
const pulseIntensity = 0.7 + 0.3 * Math.sin(time / 2500 + Math.PI / 4);
// 外层脉冲光圈
const glowPadding = Math.max(4, Math.min(w, h) * 0.05);
const glowGradient = ctx.createRadialGradient(
x + w/2, y + h/2, 0,
x + w/2, y + h/2, Math.max(w, h) * 0.6
);
glowGradient.addColorStop(0, `rgba(239, 68, 68, ${pulseIntensity * 0.4})`);
glowGradient.addColorStop(0.6, `rgba(239, 68, 68, ${pulseIntensity * 0.2})`);
glowGradient.addColorStop(1, 'rgba(239, 68, 68, 0)');
ctx.fillStyle = glowGradient;
ctx.fillRect(x - glowPadding, y - glowPadding, w + glowPadding * 2, h + glowPadding * 2);
// 主体边框(加粗且带脉冲效果)
ctx.strokeStyle = `rgba(239, 68, 68, ${0.6 + pulseIntensity * 0.4})`;
ctx.lineWidth = Math.max(3, Math.min(w, h) * 0.02);
ctx.setLineDash([]);
ctx.beginPath();
ctx.roundRect(x, y, w, h, 4);
ctx.stroke();
// 内边框高光
ctx.strokeStyle = `rgba(255, 255, 255, ${pulseIntensity * 0.2})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(x + 2, y + 2, w - 4, h - 4, 3);
ctx.stroke();
// 显示断连状态的图标(关门状态)
if (__doorImgClosed && __doorImgClosed.complete) {
const iconPadding = Math.max(2, Math.min(w, h) * 0.02);
const availW = Math.max(0, w - iconPadding * 2);
const availH = Math.max(0, h - iconPadding * 2);
const ratio = __doorImgClosed.naturalWidth > 0 && __doorImgClosed.naturalHeight > 0 ?
__doorImgClosed.naturalWidth / __doorImgClosed.naturalHeight : 1;
let drawW = availW;
let drawH = drawW / ratio;
if (drawH > availH) {
drawH = availH;
drawW = drawH * ratio;
}
const dx = x + (w - drawW) / 2;
const dy = y + (h - drawH) / 2;
// 使用平滑的脉冲透明度效果 (0.8 - 1.0)
ctx.globalAlpha = 0.85 + 0.15 * Math.sin(time / 2500 + Math.PI / 6);
ctx.drawImage(__doorImgClosed, dx, dy, drawW, drawH);
ctx.globalAlpha = 1.0;
}
}
export function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { export function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const theme = sTheme.editor; const theme = sTheme.editor;
const { active, iconSize: r = 0, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {}; const { active, iconSize: r = 0, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
@ -21,34 +174,51 @@ export function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
ctx.save(); ctx.save();
// 自动门点:根据连接与开关状态绘制矩形光圈(无边框) // 自动门点:根据连接与开关状态绘制光圈效果
if (type === MapPointType. && pointActive) { if (type === MapPointType. && pointActive) {
// 让光圈随点位尺寸等比缩放,避免缩放画布时视觉上变大
const base = Math.min(w, h);
const padding = Math.max(2, Math.min(10, base * 0.2));
const rx = x - padding;
const ry = y - padding;
const rw = w + padding * 2;
const rh = h + padding * 2;
// 使用与点位相同的圆角半径,使观感统一
ctx.beginPath();
ctx.roundRect(rx, ry, rw, rh, r);
if (isConnected === false) { if (isConnected === false) {
// 未连接:深红色实心,不描边 // 未连接:使用新的脉冲效果
ctx.fillStyle = colorConfig.getColor('autoDoor.strokeClosed') || '#fe5a5ae0'; const currentTime = performance.now();
drawAutoDoorDisconnectedPulse(ctx, pen, currentTime);
ctx.restore(); // 提前返回,不执行后续渲染
return;
} else { } else {
// 已连接根据门状态显示颜色0=关门-浅红1=开门-蓝色) // 已连接:使用原有的光圈效果,但优化颜色和样式
const base = Math.min(w, h);
const padding = Math.max(2, Math.min(10, base * 0.2));
const rx = x - padding;
const ry = y - padding;
const rw = w + padding * 2;
const rh = h + padding * 2;
// 使用与点位相同的圆角半径,使观感统一
ctx.beginPath();
ctx.roundRect(rx, ry, rw, rh, r);
// 优化后的颜色方案
if (doorStatus === 0) { if (doorStatus === 0) {
ctx.fillStyle = colorConfig.getColor('autoDoor.fillClosed') || '#cddc39'; // 关门:柔和的橙色渐变
const closedGradient = ctx.createLinearGradient(x, y, x + w, y + h);
closedGradient.addColorStop(0, 'rgba(255, 193, 7, 0.6)');
closedGradient.addColorStop(1, 'rgba(255, 152, 0, 0.8)');
ctx.fillStyle = closedGradient;
} else { } else {
ctx.fillStyle = colorConfig.getColor('autoDoor.fillOpen') || '#1890FF'; // 开门:明亮的蓝色渐变
const openGradient = ctx.createLinearGradient(x, y, x + w, y + h);
openGradient.addColorStop(0, 'rgba(24, 144, 255, 0.6)');
openGradient.addColorStop(1, 'rgba(19, 106, 207, 0.8)');
ctx.fillStyle = openGradient;
} }
ctx.fill();
// 添加细微的边框高光
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
ctx.lineWidth = 1;
ctx.stroke();
// 重置路径,避免后续对点位边框的 stroke 影响到光圈路径
ctx.beginPath();
} }
ctx.fill();
// 重置路径,避免后续对点位边框的 stroke 影响到光圈路径
ctx.beginPath();
} }
switch (type) { switch (type) {
@ -297,38 +467,53 @@ export function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
if (!finalStrokeColor) { if (!finalStrokeColor) {
finalStrokeColor = colorConfig.getColor('area.strokeActive') || '#8C8C8C'; finalStrokeColor = colorConfig.getColor('area.strokeActive') || '#8C8C8C';
} }
// 门区域断连:加粗、红色(不闪烁) // 门区域断连处理
if ((type as any) === DOOR_AREA_TYPE) { if ((type as any) === DOOR_AREA_TYPE) {
const isConnected = (pen.area as any)?.isConnected; const isConnected = (pen.area as any)?.isConnected;
if (isConnected === false) { if (isConnected === false) {
finalStrokeColor = '#ff4d4f'; // 使用新的脉冲效果
ctx.lineWidth = Math.max(borderWidth * 2, 4); const currentTime = performance.now();
ctx.setLineDash([]); drawDoorAreaDisconnectedPulse(ctx, pen, currentTime);
// 绘制基本边框(作为备用,确保兼容性)
ctx.strokeStyle = 'rgba(239, 68, 68, 0.3)';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, w, h);
} else {
// 正常连接状态:使用标准边框
ctx.strokeStyle = finalStrokeColor;
ctx.stroke();
} }
} else {
// 非门区域:使用标准边框
ctx.strokeStyle = finalStrokeColor;
ctx.stroke();
} }
ctx.strokeStyle = finalStrokeColor;
ctx.stroke();
// 门区域图标背景:根据设备连接与开关状态绘制淡化图标 // 门区域图标背景:根据设备连接与开关状态绘制淡化图标
if ((type as any) === DOOR_AREA_TYPE) { if ((type as any) === DOOR_AREA_TYPE) {
const isConnected = (pen.area as any)?.isConnected; const isConnected = (pen.area as any)?.isConnected;
const doorStatus = (pen.area as any)?.doorStatus; const doorStatus = (pen.area as any)?.doorStatus;
const img = isConnected === false ? __doorImgClosed : doorStatus === 1 ? __doorImgOpen : __doorImgClosed;
if (img && img.complete) { // 只有连接状态才绘制常规图标(断连状态已在脉冲效果中处理)
const padding = Math.max(2, Math.min(10, Math.min(w, h) * 0.02)); if (isConnected !== false) {
const availW = Math.max(0, w - padding * 2); const img = doorStatus === 1 ? __doorImgOpen : __doorImgClosed;
const availH = Math.max(0, h - padding * 2); if (img && img.complete) {
const ratio = img.naturalWidth > 0 && img.naturalHeight > 0 ? img.naturalWidth / img.naturalHeight : 1; const padding = Math.max(2, Math.min(10, Math.min(w, h) * 0.02));
let drawW = availW; const availW = Math.max(0, w - padding * 2);
let drawH = drawW / ratio; const availH = Math.max(0, h - padding * 2);
if (drawH > availH) { const ratio = img.naturalWidth > 0 && img.naturalHeight > 0 ? img.naturalWidth / img.naturalHeight : 1;
drawH = availH; let drawW = availW;
drawW = drawH * ratio; let drawH = drawW / ratio;
if (drawH > availH) {
drawH = availH;
drawW = drawH * ratio;
}
const dx = x + (w - drawW) / 2;
const dy = y + (h - drawH) / 2;
// 按原图不透明绘制
ctx.drawImage(img, dx, dy, drawW, drawH);
} }
const dx = x + (w - drawW) / 2;
const dy = y + (h - drawH) / 2;
// 按原图不透明绘制
ctx.drawImage(img, dx, dy, drawW, drawH);
} }
} }