canvas-玩转每一个像素-滤镜
CodeLittlePrince opened this issue · 3 comments
前言
接触canvas应该是在去年半次元做制品计划吧,想想也好久了,不过,那会儿每天累得和狗一样,周末还要上课。经验总结也基本都记录在OneNote,思维跳跃性的记录也就不适合作为博客发布。
现在来了新公司,一段时间忙,一段时间闲成狗,所以就会再重新总结,写写博客。
canvas简介
引用自MDN:
是 HTML5 新增的元素,可用于通过使用JavaScript中的脚本来绘制图形。例如,它可以用于绘制图形,制作照片,创建动画,甚至可以进行实时视频处理或渲染。
我认为canvas最好的教程就是MDN的,canvas基础补充请戳这里>>
canvas绘制图片——drawImage
我们可以将已经加载好的图片画到canvas上。
绘制图片的api接口:
drawImage(image, x, y, width, height)
其中 image 是 image 或者 canvas 对象,x 和 y 是其在目标 canvas 里的起始坐标。width 和 height,这两个参数用来控制 当像canvas画入时应该缩放的大小。
drawImage其实还有四个额外参数,一般用来做截图,这里因为文章不涉及就不赘述了。有兴趣的小伙伴可以戳这里>>
获取canvas所有的像素点——getImageData
getImageData是canvas提供的一个非常强大的接口,它可以获取canvas的所有的像素点的值。不过,值的展现形式和一般的rgba或rgb等属性不同,所有的值会被记录在一个Uint8ClampedArray
的一维数组里面。
知识补充——Uint8ClampedArray
The Uint8ClampedArray typed array represents an array of 8-bit unsigned integers clamped to 0-255; if you specified a value that is out of the range of [0,255], 0 or 255 will be set instead; if you specify a non-integer, the nearest integer will be set. The contents are initialized to 0.
翻译一下:
Uint8ClampedArray类型数组表示一个8-bit无符号整数,即0-255区间;如果你设了一个的值超出了[0, 255]的范围,他们会被0或者255代替(小于0代替为0,大于255替代为255);如果你设了一个非整数,会被替代为这个小数最接近的整数。所有的初始值为0;
那么问题来了,数组是怎么存每个像素点的rgba值的呢?
见图:
如果,canvas将每个像素点的值按照rgba这样的顺序一个一个的存入Unit8ClampedArray里面。
因此,数组的长度为length = canvas.width * canvas.height * 4。
知道了这种关系,我们不妨把这个一维数组想象成二维数组,想象它是一个平面图,如图:
一个格子代表一个像素
w = 图像宽度
h = 图像高度
这样,我们可以很容易得到点(x, y)在一维数组中对应的位置。我们想一想,点(1, 1)坐标对应的是数组下标为0,点(2, 1)对应的是数组下标4,假设图像宽度为2*2,那么点(1,2)对应下标就是index=((2 - 1)*w + (1 - 1))*4 = 8
。
推导出公式:index = [(y - 1) * w + (x - 1) ] * 4
知识补充
我们既然已经能够拿到图像的每一个像素点,那么我们就可以为所欲为啦!
不过客官别急,我们还有点小知识要补充,避免代码实现的过程陷入迷茫~
知识补充——createImageData
The CanvasRenderingContext2.createImageData() method of the Canvas 2D API creates a new, blank ImageData object with the specified dimensions. All of the pixels in the new object are transparent black.
翻译(非直译):
createImageData是在canvas在取渲染上下文为2D(即
canvas.getContext('2d')
)的时候提供的接口。作用是创建一个新的、空的、特定尺寸的ImageData对象。其中所有的像素点初始都为黑色透明。
我们会用到ctx.createImageData(width, height)
这个接口,width和height是新ImageData对象的初始长宽。
ImageData又是啥?
ImageData是一个对象,其实我们在canvas.getImageData拿到的对象就是ImageData,它内部由width,height,Uint8ClampedArray组成,
如:{data: Uint8ClampedArray(958400), width: 400, height: 599}
知识补充——createImageData
The CanvasRenderingContext2D.putImageData() method of the Canvas 2D API paints data from the given ImageData object onto the bitmap. If a dirty rectangle is provided, only the pixels from that rectangle are painted. This method is not affected by the canvas transformation matrix.
翻译:
CanvasRenderingContext2D.putImageData() 方法作为canvas 2D API 以给定的ImageData对象绘制数据进位图。如果提供了脏矩形,将只有矩形的像素会被绘制。这个方法不会影响canvas的形变矩阵。
看上去有点迷糊,矩阵都出来了。不过不用担心,我们只关注第一句就好,忽略“如果“之后的文字。
我们将会用到ctx.putImageData(imagedata, dx, dy)
接口,imageData就是用户提供的ImageData对象,dx和dy分别是canvas坐标系的x点和y点,将从这个(dx,dy)开始输入数据。
实现滤镜
终于迎来了最后的阶段!
直接上代码:
html:
<div class="box">
<img id="img" src="index.png">
</div>
<canvas id="canvas"></canvas>
js
draw()
function draw() {
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const img = document.getElementById('img')
// 等图片加载完以后才能获取图片信息
img.onload = function() {
const style = window.getComputedStyle(img)
const w = style.width
const h = style.height
const ws = w.replace(/px/, '')
const hs = h.replace(/px/, '')
canvas.width = ws
canvas.height = hs
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
// 修改颜色准备
const originColor = ctx.getImageData(0, 0, ws, hs)
// 保存ImageData里的Uint8ClampedArray数据
const originColorData = originColor.data
// 创建一个空的图像,这时canvas里其实已经没原来的图像了
const output = ctx.createImageData(ws, hs)
const outputData = output.data
// 诡异画风按钮绑定
const weirdBtn = document.getElementById('weird')
weirdBtn.addEventListener('click', function() {
// 诡异画风数据处理(我们可以用各种处理方法处理图像数据,达到想要的效果)
weird(originColorData, outputData, ws, hs)
ctx.putImageData(output, 0, 0)
})
}
}
// 诡异
function weird(originColorData, outputData, ws, hs) {
let random
let randomData
let index;
let r, g, b;
// 逐行扫描
for (let y = 1; y <= hs; y++) {
// 逐列扫描
for (let x = 1; x <= ws; x++) {
// rgb处理
for (let c = 0; c < 3; c++) {
random = Math.random(0, 255) * 100
randomData = Math.abs(random - originColorData[index])
index = ((y-1) * ws + (x-1)) * 4 + c
outputData[index] = randomData
}
// alpha处理,我们就让透明度一直未1就好了
outputData[index + 3] = 255;
}
}
}
通过对imageData的处理,我们可以控制每个像素点,然后你想处理出不同的效果,只需要改写weird方法就可以了。我写了5种滤镜效果,效果如下gif图:
完整的项目地址在这里>>>
透明度outputData[index + 3] = 255; 是index+1吗
@Birdy-C 蛤?
滤镜这种 ,类似数学公式,往里面套就行 (有的公式需要套好几层,一层一层套就可以实现)
大佬 可以参考一下
https://github.com/GrammyLi/fliter
canvas 滤镜实践:
https://grammyli.com/avatar/