Canvas 实现类似 PS 的滤镜效果 Canvas 像素级操作

发布于 2021-01-09 21:48:11 字数 5834 浏览 1807 评论 0

Canvas 是 HTML5 新增的元素,可用于通过使用 JavaScript 中的脚本来绘制图形。例如,它可以用于绘制图形,制作照片,创建动画,甚至可以进行实时视频处理或渲染。

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}

知识补充——putImageData

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 图:

完整的项目地址在这里

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84960 人气
更多

推荐作者

linfzu01

文章 0 评论 0

可遇━不可求

文章 0 评论 0

枕梦

文章 0 评论 0

qq_3LFa8Q

文章 0 评论 0

JP

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文