前阵子因业务需求,须要对图片进行一些特殊处理,例如反相,高亮,黑白等,都是使用Canvas
来实现javascript
要实现上述所说的各类效果,最核心的事情即是对图片的ImageData
对象进行改动。css
ImageData
对象是一个用来描述图片属性的一种数据对象,它有三个属性,分别是data
、width
、height
。后两个表明的是图片的宽高,不用多说。最重要的就是data
属性,它是一个Uint8ClampedArray
(8位无符号整形固定数组)类型化数组。按照从上到下,从左到右的顺序,它里面储存了一张图片的全部像素的rgba信息。html
例如,一张图片有4个像素,那data
里面就有16个值,data[0]~data[3]
的值就是第一个像素中的r、g、b、a值(不了解rgba的看这里)。html5
如何得到一张图片的ImageData
对象?经过canvas的getImageData
即可以很简单地得到:java
const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') ctx.drawImage(img, 0, 0, canvas.width, canvas.height) const oriPeixel = ctx.getImageData(0, 0, canvas.width, canvas.height)
值得注意的是,ImageData
里面的属性都是只读的,不能直接更改和赋值。css3
例如咱们把上面的oriPeixel
的属性赋值,就会报如下的错:git
oriPeixel.data = [] > Uncaught TypeError: Cannot assign to read only property 'data' of object '#<ImageData>'
了解了ImageData后,咱们来看看效果demogithub
先看demo:demo-1canvas
能够见到,图片先是渐变成反相的样子,再渐变为下一张图片,是否是很酷炫。要现实这个,主要是用到getImageData
及putImageData
这两个API数组
刚才咱们说过,图片的ImageData
对象储存着该图片的每一个像素的信息,想要获得图片的反相效果,要做以下处理:
threshold (ctx, idx) { let pixels = ctx.getImageData(0, this.height * idx, this.width, this.height) let d = pixels.data for (let i = 0; i < d.length; i += 4) { let r = d[i] let g = d[i + 1] let b = d[i + 2] // 根据rgb求灰度值公式0.2126 * r + 0.7152 * g + 0.0722 * b let v = (0.2126 * r + 0.7152 * g + 0.0722 * b >= 100) ? 255 : 128 d[i] = d[i + 1] = d[i + 2] = v } return pixels }
返回的pixels
即是图片通过反相处理后的ImageData
这里主要是对每一个像素的灰度值做过滤,大于等于100的,直接为白色,不然置于128
除此以外,还有黑白,高亮等其余像素处理,具体的能够看这篇文章
有了通过反相处理后的图片的ImageData
数据,下一步要作的天然就是渐变赋值了。原生是没有提供相关的API自动达成这种的渐变效果的,因此就须要咱们自行实现一遍了,这个会比较麻烦。
用js写过动画的同窗都知道,基本上都会使用requestAnimationFrame
函数来进行帧处理,这里也不意外。
主要思路是这样,图片通过以下的顺序进行渐变:
图片1----->图片1反相----->图片2----->图片2反相----->图片3......
直接贴上主要代码:
gradualChange () { // 图片原始的ImageData数据 let oriPixels = this.ctx.getImageData(0, 0, this.width, this.height) let oriData = oriPixels.data // 图片反相后的ImageData数据 let nextData = this.nextPixel[0].data let length = oriData.length let totalgap = 0 let gap = 0 let gapTemp for (let i = 0; i < length; i++) { // 计算每一个rgba的差值,同时缩小处理。除的数值表明着渐变速度,越大越慢 gapTemp = (nextData[i] - oriData[i]) / 13 if (oriData[i] !== nextData[i]) { // 每一个rgba值增量处理,简单来讲就是各类取整,[-1,1]区间直接取-1或1 gap = gapTemp > 1 ? Math.floor(gapTemp) : gapTemp < -1 ? Math.ceil(gapTemp) : oriData[i] < nextData[i] ? 1 : oriData[i] > nextData[i] ? -1 : 0 totalgap += Math.abs(gap) oriData[i] = oriData[i] + gap } } // 经过putImageData更新图片 this.ctx.putImageData(oriPixels, 0, 0) // 总值为0,证实已经渐变完成 if (!totalgap) { this.nextPixel.shift() if (!this.nextPixel[0]) { this.isChange = false } } }
上面是渐变过程的主要代码,完整的代码能够查看:我是代码
一样是先看demo
能够见到,移动端的demo中,光条上有几个亮斑在同时移动;而PC端,则是在当鼠标hover上去以后,在光条中有一个圆形光斑的高亮效果,由于图片自己是透明的,因此背景色作了深色处理。
须要说明的是,要实现这种效果,最好是找一些背景一部分透明,一部分带有带状色条的图片,例如我demo中的图片。这类图片有至关区域像素的rgba值为4个0,咱们很容易对其作边界处理
一样的,实现这种效果也是须要对图片像素的rgba值进行处理,可是会比图片反相渐变复杂一些,由于这里须要先实现一个圆形的光斑。
光斑实现
既然是圆形光斑,确定是先有圆心和半径。在这里,我是在横向的方向上,取光条的中心为圆心,半径取50
实现的代码在demo2的brightener
函数里面,理解起来也不困难,给定一个y
坐标,而后再遍历一遍在这个y
坐标下的像素,找出每条光条初始点和结束点的x
坐标。rgba值连续两点不为0的,就认为是仍处在光条中,尚未达到边界值
brightener (y) { // ....完整请看源代码 for (let x = 0; x < cW; x++) { sPx = (cY * cW + x) * 4 if (oriData[sPx] || oriData[sPx + 1] || oriData[sPx + 2]) { startX || (startX = x) tempX = sPx + 4 if (oriData[tempX] || oriData[tempX + 1] || oriData[tempX + 2]) { continue } else { endX = tempX / 4 - cY * cW cX = Math.ceil((endX - startX) / 2) + startX startX = 0 res.push({ x: cX, y: cY }) } } } return res }
肯定了圆心以后,就能够根据半径肯定一个圆,并用一个数组存储这个圆内各个点,以便后续处理。过程也很简单,就是初中学的那一套,两点距离小于半径就能够了
createArea (x, y, radius) { let result = [] for (let i = x - radius; i <= x + radius; i++) { for (let j = y - radius; j <= y + radius; j++) { let dx = i - x let dy = j - y if ((dx * dx + dy * dy) <= (radius * radius)) { let obj = {} if (i > 0 && j > 0) { obj.x = i obj.y = j result.push(obj) } } } } return result }
以后,就是实现一个光斑效果。在这里,我是从圆心向边缘进行一个透明度的衰减渐变
// ... const validArr = this.createArea(x, y, radius) validArr.forEach((px, i) => { sPx = (px.y * cW + px.x) * 4 // 像素点的rgb值不全为0 if (oriData[sPx] || oriData[sPx + 1] || oriData[sPx + 2]) { distance = Math.sqrt((px.x - x) * (px.x - x) + (px.y - y) * (px.y - y)) // 根据距离和半径的比率进行正比衰减 gap = Math.floor(opacity * (1 - distance / radius)) oriData[sPx + 3] += gap } }) // 更新ImageData this.ctx.putImageData(oriPixels, 0, 0)
到这里,一个光斑就这样实现了
光斑有了,天然就是让它动起来。这个就简单啦,光斑生成的咱们已经完成,那么咱们只要把圆心动起来就能够了
在这里,一样是使用requestAnimationFrame
函数来进行帧处理。而光斑是从下向上移动的,能够看到startY
在不断递减
autoPlay (timestamp) { if (this.startY <= -25) { let timeGap if (!this.progress) { this.progress = timestamp } timeGap = timestamp - this.progress // 判断间隔时间是否知足 if (timeGap > this.autoPlayInterval) { this.startY = this.height - 1 this.progress = 0 } } else { // 根据Y坐标生成圆心及光斑 const res = this.getBrightCenter(this.startY) this.brightnessCtx(res, 50, 60) this.startY -= 10 } window.requestAnimationFrame(this.autoPlay.bind(this), false) }
能够看到,无非就是循环startY
坐标,生成新光斑的过程。而PC上的效果是当鼠标hover上去时有光斑效果,同理去掉这个自动移动的过程,对图片的mousemove
事件进行监听,得出x
,y
坐标做为圆心便可
值得注意的是,由于在不断地更新ImageData
,因此咱们须要一个临时的canvas
来存放原始图片的ImageData
数据。demo1也是做了一样的处理
以上即是使用Canvas
实现一些图片效果的介绍,权当抛砖引玉,各类看官也能够发挥想象力,实现本身的酷炫效果