CodeLittlePrince/blog

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图:
交互效果gif图

完整的项目地址在这里>>>

透明度outputData[index + 3] = 255; 是index+1吗

滤镜这种 ,类似数学公式,往里面套就行 (有的公式需要套好几层,一层一层套就可以实现)
大佬 可以参考一下
https://github.com/GrammyLi/fliter

canvas 滤镜实践:
https://grammyli.com/avatar/