一、前言
整体的思路:通过读取图片像素,将像素点的位置映射到ecahrts
的Heatmap
组件上,将颜色映射到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
扁平化保存的数组。
r
:red
(1-255)
g
:green
(1-255)
b
:blue
(1-255)
a
: alpha通道
表示像素点的亮暗程度(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.data
、yAxis.data
,series[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
五、缺点
如果图形的像素密集,echarts绘图变得十分缓慢。
六、思路扩展
我只是简单的将像素信息映射到echarts
里,就可以画出可爱的冰墩墩和雪融融了,可以看到全程只是搬运像素,没做任何处理,所以理论上
- 可以对图片、canvas画布和echarts画布大小进行联动,实现各种大小图形的绘制
源码点这里
- 对像素进行模糊化处理可以画出MC像素风格的图形
源码点这里
- 通过减少相邻的像素降低图片大小,达到压缩图片的效果
源码点这里
- 通过使用算法库实现加滤镜效果
感兴趣的小伙伴可以关注我以及我的微信公众号:萌萌哒草头将军
不定期更新有意思的、硬核前端知识