1、需求背景:
最近接到一个智慧园区项目需求:客户提供了一张厂区静态底图,需要在图中实现10个不规则区域的智能交互:
-
鼠标交互:滑过区域时高亮显示
-
状态展示:开灯时显示灯光效果
-
告警提示:故障时红色闪烁提醒
-
响应式:全屏适配不偏移
核心挑战:这些区域都是不规则多边形,如何让交互效果精准贴合底图区域?
2. 技术方案选型:为什么放弃传统DOM方案?
方案一:传统 DOM + CSS (放弃)
css
<div class="area area-1" style="left: 100px; top: 150px; width: 80px; height: 60px;"></div>
<div class="area area-2" style="left: 200px; top: 180px; width: 120px; height: 90px;"></div>
放弃原因:
精度灾难:不规则区域用矩形div近似
响应式噩梦:全屏时位置错乱,调试到怀疑人生
交互失真:鼠标滑过矩形框,实际却点在空白处
方案二:Canvas + SVG 路径(最终选择)
核心思路:把不规则区域变成数学路径,让计算机精确计算命中关系
核心技术实现:精准命中的秘密
路径数据采集:把图形变成数据
首先,我们在设计稿分辨率下采集每个区域的路径点:
//
export const fullShapeList = [
{
remark: 'remark0-1',
path: new Path2D('M 100,150 L 200,180 L 150,250 Z'), // 三角形区域
name: '1号厂房'
},
{
remark: 'remark0-2',
path: new Path2D('M 300,200 L 400,220 L 380,300 L 320,280 Z'), // 四边形区域
name: '2号仓库'
}
// ...更多区域
]
坐标映射:响应式的数学魔法
const
setRatio() {
const currentWidth = window.innerWidth
const currentHeight = window.innerHeight
this.ratioX = DEFAULT_CONFIG.WIDTH / currentWidth
this.ratioY = DEFAULT_CONFIG.HEIGHT / currentHeight
}
handleCanvasClick(event) {
// 关键步骤:坐标转换
const mouseX = event.offsetX * this.ratioX
const mouseY = event.offsetY * this.ratioY
// 精准命中检测
const hitArea = this.shapeList.find(shape =>
this.ctx.isPointInPath(shape.path, mouseX, mouseY)
)
if (hitArea) {
this.handleAreaClick(hitArea)
}
状态渲染:视觉效果的层级管理
getFillStyle(item)
// 1. 鼠标悬停最高优先级(蓝色高亮)
if (item.remark === this.hoveredRemark) {
return this.selectPattern
}
// 2. 告警状态(红色闪烁)
else if (item.alarmStatus === 1) {
return this.shouldFlash ? this.faultPattern : null
}
// 3. 开灯状态(全亮效果)
else if (item.state === 100) {
return this.allPattern
}
// 4. 调光状态(半透明效果)
else if (item.state > 0) {
return this.dimmingPattern
}
return null // 默认不填充
}
4.1 调光效果的真实模拟
调光不是简单的透明度变化,而是两层图案的智能混合:
createDimmingPattern(level
if (level <= 0) return this.closePattern // 全关
if (level >= 1) return this.allPattern // 全开
const patternCanvas = document.createElement('canvas')
const ctx = patternCanvas.getContext('2d')
ctx.fillStyle = this.closePattern
ctx.fillRect(0, 0, patternCanvas.width, patternCanvas.height)
ctx.globalAlpha = level
ctx.fillStyle = this.allPattern
ctx.fillRect(0, 0, patternCanvas.width, patternCanvas.height)
return this.ctx.createPattern(patternCanvas, 'no-repeat')
}
告警闪烁的实现
实现原理: 使用 Set 存储需要闪烁的区域标识 定时器切换显示/隐藏状态 只重绘告警区域,优化性能
//
this.flickeringRemarks = new Set()
startFlickering() {
this.flickerIntervalId = setInterval(() => {
this.flickerToggle = !this.flickerToggle
// 只重绘告警区域,性能提升 80%
this.updateAlarmAreasOnly()
}, 500)
}
图片资源的智能管理
async
const loadPromises = Object.entries(this.mapConfig.images).map(([key, src]) => {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve({ key, img })
img.onerror = () => resolve({ key, img: this.createFallbackImage(key) })
img.src = src
})
})
const results = await Promise.all(loadPromises)
results.forEach(({ key, img }) => {
this.imageMap[key] = this.createPatternFromImage(img)
})
}
整体实现
步骤1:初始化画布
async
const loadPromises = Object.entries(this.mapConfig.images).map(([key, src]) => {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve({ key, img })
img.onerror = () => resolve({ key, img: this.createFallbackImage(key) })
img.src = src
})
})
const results = await Promise.all(loadPromises)
results.forEach(({ key, img }) => {
this.imageMap[key] = this.createPatternFromImage(img)
})
}
步骤2 数据获取
这里就根据自己的业务逻辑来,就不贴具体的代码了
步骤三 渲染
setPattern(data)
this.drawBackground(() => {
data.forEach(item => {
const shape = this.getShapeByRemark(item.remark)
if (shape) {
const fillStyle = this.getFillStyle(item)
if (fillStyle) {
this.ctx.fillStyle = fillStyle
this.ctx.fill(shape.path)
}
}
})
})
}
总结:
至此,整个方案基本上就是这样了,Canvas + SVG路径的方案
- 精准贴合:路径与底图区域完全匹配
- 响应式友好:坐标映射算法保证全屏无偏差
- 性能优异:Canvas渲染大量区域依然流畅
- 交互准确:鼠标事件精准响应不规则形状
但是也有不好的点: 需要手动去获取/绘制不规则区域,如果说区域一多,这个必定是造成庞大的工作量,这点目前还不知道怎么解决,其实,类似与这种需求,要mars3D实现应该也是可以的,但是就是调光效果可能会有点难满足客户的需求。只能说每种方案,都有利有弊吧。如果大家有其他更好的方案,欢迎评论区留言。