我用Echarts画出了冰墩墩和雪融融:Echarts Heatmap画图实践

342 阅读4分钟

一、前言

整体的思路:通过读取图片像素,将像素点的位置映射到ecahrtsHeatmap组件上,将颜色映射到Heatmap组件的itemStyle属性上。

所以可以分三步走:

第一步:获取图片每个像素位置和颜色

第二步:将数据格式整理成echrts需要的格式

第三步:绘制图形

源码戳这里

二、获取图片每个像素位置和颜色

比较简单的方式是:将图片传入canvas绘图,获取到图片2D对象然后使用getImangeData()函数获取到图片信息,getImangeData()函数返回ImangeData实例信息:data: Uint8ClampedArray (介里存的就是像素信息)width: number(图片的宽)height: number(图片的高)

代码如下

// 获取画布
const canvas = document.getElementById('canvas')
// 获取当前图形对象
const ctx = canvas.getContext('2d')
// 获取图片对象
const image = document.getElementById('img')

// 图片加载完成后
image.onload = () => {
    // 绘图
    ctx.drawImage(image, 0, 0)
    // 获取宽高
    const { clientWidth, clientHeight } = canvas
    // 像素信息
    const imgData = ctx.getImageData(0, 0, clientWidth, clientHeight).data
}

注意:在浏览器中,canvas绘图和img标签的src必须同源才能这样操作,如果不同源就会报错:cros-origin data。为了避免上面的错误,我将图形和页面都放在服务端,代码已经准备好了,npm run start,就可以在http://localhost:3000/index端口访问了

Uint8ClampedArray是8位无符号整型固定数组,是将所有像素点的颜色r``g``b``a扁平化保存的数组。

rred(1-255)

ggreen(1-255)

bblue(1-255)

aalpha通道表示像素点的亮暗程度(1-255),这里需要注意下,在使用rgba格式颜色时,a一般时0-1的小数,表示透明度

所以假如有三个像素点,颜色分别是:白rgba(255, 255, 255, 255)、黑rgba(255, 255, 255, 0)、白rgba(255, 255, 255, 255),此时的Uint8ClampedArray[255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255],依次类推。

另外,Uint8ClampedArray长度始终等于width*height*4,比如,图片的像素为200x300,那么Uint8ClampedArray的长度为200x300x4=240000

知道这些,一切就都好办了,我们可以通过遍历Uint8ClampedArray拿到像素数组了。

const rgba = []
// 获取像素值
for (let i = 0; i < imgData.length; i = i + 4) {
    // r, g, b, a
    rgba.push([imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]])

}

紧接着,可以按行分割像素.

// 按行排列
const rowPx = new Array(clientHeight)
    .fill(null)
    .map((_, idx) => rgba.slice(clientWidth * idx, clientWidth * (idx + 1)))

到这里已经perfect了! 接下来只需要将数据格式整理成echrts需要的格式然后画图。

三、整理数据格式

按照图片的宽高生成坐标轴就不用多说了。

需要额外注意的是,绘图的坐标和获取像素的坐标是存在90°差别的。为了显示成我们习惯的方向,需要将行列互换,并且,将行首和行尾倒置。我将像素的四个信息值追加到了data最后面。

// 初始化坐标轴数据
const xdata = new Array(clientWidth).fill(null).map((_, idx) => idx)
const ydata = new Array(clientHeight).fill(null).map((_, idx) => idx)

const data = rowPx
    .map((row, idx) => row.map((px, idy) => [
      idy,
      clientHeight - idx,
      ...px
    ]))
    .reduce((pre, curr) => pre.concat(curr), [])

四、绘制图形

这里我已经提供了一个Heatmap基本的option。将上面整理好的数据分别放在xAxis.datayAxis.dataseries[0].data

 // echarts实例
  const myChart = echarts.init(document.getElementById('echarts'))

  // 各加补充50坐标轴空间
  myChart.resize({width: clientWidth + 50, height: clientHeight + 50})

  const option = {
    tooltip: {},
    toolbox: {
      feature: {
        saveAsImage: {}
      }
    },
    grid: {
      show: false,
      top: 0
    },
    xAxis: {
      type: "category",
      data: xdata
    },
    yAxis: {
      type: "category",
      data: ydata
    },
    series: [{
      type: "heatmap",
      data: data.map(value => ({
        value,
        itemStyle: {
          color: `rgba(${value[2]}, ${value[3]}, ${value[4]}, ${1})`
        }
      })),
      progressive: 1000,
      animation: false
    }]
  }

  myChart.setOption(option)

启动服务,在浏览器打开服务http://localhost:3000/index.html

GIF 2022-3-21 9-57-10.gif

五、缺点

如果图形的像素密集,echarts绘图变得十分缓慢。

六、思路扩展

我只是简单的将像素信息映射到echarts里,就可以画出可爱的冰墩墩和雪融融了,可以看到全程只是搬运像素,没做任何处理,所以理论上

  1. 可以对图片、canvas画布和echarts画布大小进行联动,实现各种大小图形的绘制源码点这里
  2. 对像素进行模糊化处理可以画出MC像素风格的图形源码点这里
  3. 通过减少相邻的像素降低图片大小,达到压缩图片的效果源码点这里
  4. 通过使用算法库实现加滤镜效果

感兴趣的小伙伴可以关注我以及我的微信公众号:萌萌哒草头将军

不定期更新有意思的、硬核前端知识