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

相关推荐
倚肆1 小时前
CSS 选择器空格使用区别详解
前端·css
盼哥PyAI实验室1 小时前
学会给网页穿衣服——学习 CSS 语言
前端·css·学习
我的xiaodoujiao1 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 25--数据驱动--参数化处理 Excel 文件 2
前端·python·学习·测试工具·ui·pytest
岁月宁静2 小时前
从0到1:智能汇 AI 全栈实战,拆解多模态 AI 应用开发全流程
前端·vue.js·node.js
廾匸6402 小时前
语义化标签
前端·javascript·html
烛阴2 小时前
隐式vs显式:解密C#类型转换的底层逻辑
前端·c#
Fantasydg2 小时前
AJAX JSON学习
前端·学习·ajax
瓢儿菜20182 小时前
Web开发:什么是 HTTP 状态码?
前端·网络协议·http
1024小神3 小时前
swiftui使用WKWebView加载自签的https服务,允许不安全访问
前端
anyup3 小时前
支持鸿蒙!开源三个月,uView Pro 开源库近期更新全面大盘点,及未来计划
前端·vue.js·uni-app