Canvas、SVG实现不规则区域高亮的方案说明

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实现应该也是可以的,但是就是调光效果可能会有点难满足客户的需求。只能说每种方案,都有利有弊吧。如果大家有其他更好的方案,欢迎评论区留言。

相关推荐
张可爱3 小时前
20251026-从网页 Console 到 Python 爬虫:一次 B 站字幕自动抓取的实践与复盘
前端·python
咖啡の猫4 小时前
Vue中的自定义事件
前端·javascript·vue.js
yangwan4 小时前
Ubunut 22.04 安装 Docker 24.0.x
前端·后端
等风起8814 小时前
Element Plus实现TreeSelect树形选择在不同父节点下子节点有相同id的双向绑定联动
前端·javascript
摸着石头过河的石头4 小时前
跨域资源共享(CORS)完全指南:从基础概念到实际应用
前端·javascript
小胖霞4 小时前
阿里云域名解析 + Nginx 反向代理 + HTTPS 全流程:从 IP 访问到加密域名的完整配置
前端
2301_801252224 小时前
Vue中的指令
前端·javascript·vue.js
烛阴4 小时前
彻底搞懂Lua闭包
前端·lua