Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

canvas-玩转每一个像素-滤镜 #4

Open
CodeLittlePrince opened this issue Nov 6, 2017 · 3 comments
Open

canvas-玩转每一个像素-滤镜 #4

CodeLittlePrince opened this issue Nov 6, 2017 · 3 comments
Labels

Comments

@CodeLittlePrince
Copy link
Owner

CodeLittlePrince commented Nov 6, 2017

前言

接触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图

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

@CodeLittlePrince CodeLittlePrince changed the title canvas-滤镜 canvas-玩转每一个像素-滤镜 Nov 6, 2017
@Birdy-C
Copy link

Birdy-C commented Jun 15, 2018

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

@CodeLittlePrince
Copy link
Owner Author

@Birdy-C 蛤?

@GrammyLi
Copy link

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants