diff --git a/src/services/editor/editor-drawers.ts b/src/services/editor/editor-drawers.ts index 288a4d3..711e4f5 100644 --- a/src/services/editor/editor-drawers.ts +++ b/src/services/editor/editor-drawers.ts @@ -12,6 +12,159 @@ __doorImgOpen.src = (doorOpenUrl as unknown as string) || ''; const __doorImgClosed = new Image(); __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 { const theme = sTheme.editor; 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(); - // 自动门点:根据连接与开关状态绘制矩形光圈(无边框) + // 自动门点:根据连接与开关状态绘制光圈效果 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) { - // 未连接:深红色实心,不描边 - ctx.fillStyle = colorConfig.getColor('autoDoor.strokeClosed') || '#fe5a5ae0'; + // 未连接:使用新的脉冲效果 + const currentTime = performance.now(); + drawAutoDoorDisconnectedPulse(ctx, pen, currentTime); + ctx.restore(); // 提前返回,不执行后续渲染 + return; } 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) { - 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 { - 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) { @@ -297,38 +467,53 @@ export function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void { if (!finalStrokeColor) { finalStrokeColor = colorConfig.getColor('area.strokeActive') || '#8C8C8C'; } - // 门区域断连:加粗、红色(不闪烁) + // 门区域断连处理 if ((type as any) === DOOR_AREA_TYPE) { const isConnected = (pen.area as any)?.isConnected; if (isConnected === false) { - finalStrokeColor = '#ff4d4f'; - ctx.lineWidth = Math.max(borderWidth * 2, 4); - ctx.setLineDash([]); + // 使用新的脉冲效果 + const currentTime = performance.now(); + 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) { const isConnected = (pen.area as any)?.isConnected; 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)); - const availW = Math.max(0, w - padding * 2); - const availH = Math.max(0, h - padding * 2); - const ratio = img.naturalWidth > 0 && img.naturalHeight > 0 ? img.naturalWidth / img.naturalHeight : 1; - let drawW = availW; - let drawH = drawW / ratio; - if (drawH > availH) { - drawH = availH; - drawW = drawH * ratio; + + // 只有连接状态才绘制常规图标(断连状态已在脉冲效果中处理) + if (isConnected !== false) { + const img = doorStatus === 1 ? __doorImgOpen : __doorImgClosed; + if (img && img.complete) { + const padding = Math.max(2, Math.min(10, Math.min(w, h) * 0.02)); + const availW = Math.max(0, w - padding * 2); + const availH = Math.max(0, h - padding * 2); + const ratio = img.naturalWidth > 0 && img.naturalHeight > 0 ? img.naturalWidth / img.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; + // 按原图不透明绘制 + 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); } }