Canvas的重头戏--图片的处理

##前文

相信接触过一些canvas的小伙们都应该会有这样的一句感叹: canvas 强 真的强!
不仅可以静态的创建一些我们用普通标签无法实现的图形,而且还能让这些图形动起来.

其实在实际的开发中,要是你只会用canvas画一些矩形啊,三角形啊,五角星等等的东西肯定是不够的.
因为真正在开发中,canvas大部分都是用来对图片以及视频做处理,所以博主今天在这里想要介绍的是一些关于canvas对图片的处理

##1. 引用图片

我们知道想在网页中显示一张图片,我们只需要用<img src="">就可以实现了,那么在canvas中我们是怎样插入一张图片的呢.

1.首先在body中创建好一个canvas标签

1
2
3
<body>
<canvas id="canvas" width="500" height="500"></canvas>
</body>

2.在js代码中获取canvas并创建一个元素

1
2
3
4
5
6
<script>
let canvas = document.querySelector('#canvas') //获取canvas对象
let ctx = canvas.getContext('2d') //获取2d上下文
let img = new Image() //创建img
img.src = 'img/green.jpg' //给img添加资源
</script>

3.​ 绘制img,考虑到图片是从网络加载,如果 drawImage 的时候图片还没有完全加载完成,则什么都不做,个别浏览器会抛异常。所以我们应该保证在 img 绘制完成之后再 drawImage

1
2
3
4
5
6
7
8
9
10
<script>
let canvas = document.querySelector('#canvas')
let ctx = canvas.getContext('2d')
let img = new Image()
img.src = 'img/green.jpg'
//图片是否已经加载完成
img.onload = function () {
ctx.drawImage(this, 100, 100, this.width / 2, this.height / 2)
}
</script>

通过上面的三个步骤,这时候打开你们的浏览器就可以在页面中看到对应的图片了.
这里主要用到的是drawImage这个方法,下面是对其的一些详细讲解.

2.解析drawImage( )

对于drawImage()这个方法,有三种使用的方式:

第一种:

只传入3个参数

drawImage(image,x,y)

参数1: image:
指的就是你的图片对象,
也就是你let img = new Image()中的img
参数2 : x
图片相对于画布原点(0,0)也就是画布的最左上角 的x轴方向的坐标
参数3: y
图片相对于画布原点(0,0)也就是画布的最左上角 的y轴方向的坐标

第二种:

传入5个参数

drawImage(image,x,y,width,height)

前面三个参数和第一种的使用方式一样.

参数4,5: width 和 height
可以规定图片的宽度和高度.
如:在画布(100,100)的位置插入一张300*300的图片

1
2
3
img.onload = function () {
ctx.drawImage(this, 100, 100, 300, 300)
}

那么利用width和height我们可以发现,想要将图片缩减为其原始大小的一半,就可以这样写:

1
2
3
img.onload = function () {
ctx.drawImage(this,100,100,this.width / 2, this.height / 2)
}

第三种:

传入9个参数
当在drawImage()中传入9个参数后,这个方法的用法将和前面俩种不一样了.

它的用法是从图片中截取一定尺寸的图片,并
drawImage(image,sourceX,sourceY,sourceWidth,sourceHeight,x,y,width,height)

第三种的使用方式传递的是9个参数,

参数1 : image
还是图片的对象

参数2,3 : 从一张大图上指定要截取小图的位置(x,y)坐标
参数4,5: 从一张大图上指定要截取小图的大小
参数6,7: 从一张大图上截取下来的小图要放在canvas(画布)中的位置(x,y)
参数8,9: 截取下来小图规定的宽高

如下图中,有5架小飞机,我只想截取最后一架并显示在画布中.

![herofly.png
9G0A~6SHJ`)CJ8AUM8__[LO.png](http://upload-images.jianshu.io/upload_images/7190596-aa463578d541b493.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

整张图的宽度是330px,一架飞机就是66px,所以最后一张图就是从66 * 3 = 198px的位置开始截取,截取完后放在画布(0, 0)的位置

1
2
3
var img1 = new Image()
img1.src = 'img/herofly.png'
drawImage(img1, 198, 0, 66, 82, 0, 0, 66, 82)

3.canvas中的动画

3.1 requestAnimationFrame的简介

我们利用普通的定时器来实现动画的写法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var x = 0;

function animate(){
//清除画布内容
ctx.clearRect(0, 0, canvas.width, canvas.height);
x += 2;
ctx.fillStyle = "red";
ctx.fillRect(x, 0, 50, 50);
if (x > 200){
return;
}
setTimeout(animate,30);
}
animate();

可以看到上面的动画是靠setTimeout这个定时器每隔30毫秒调用一次animate() 来实现的.

这种利用定时器来实现动画效果在移动端实际来说是很不可取的,在移动端上看到的动画会很卡顿,造成用户体验很不流程.
所以ES6新增了一个类似于定时器的API:
requestAnimationFrame()
它只有一个参数,就是要执行的函数.

使用requestAnimationFrame实现动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var x = 0;

function animate(){
//清除画布内容
ctx.clearRect(0, 0, canvas.width, canvas.height);
x += 2;
ctx.fillStyle = "red";
ctx.fillRect(x, 0, 50, 50);
if (x > 200){
return;
}
requestAnimationFrame(animate); //唯一不同
}
animate();

可以看到俩段代码的区别,仅仅是一个用的是setTimout,一个是requestAnimationFrame

setTimout表示的是: 每隔30毫秒,执行一次animate()函数.
而requestAnimationFrame 在一秒中执行多少次是由它的应用场景决定的,一般都能达到58~60次.也就是1000/60(相当于定时器16毫秒执行一次)

那么这里得到的1000/60就是一帧.不同的场景帧数可能会不一样.

3.2 canvas中切换图片的动画

还是利用上面的那种飞机图.我现在想实现一个从第一架完整飞机变化到最后一架爆炸飞机的效果.
herofly.png

那么有心的小伙就会发现了,在js中我们想实现图片的切换,只要改变背景图的background-position就可以了,那么在canvas中利用的就是requestAnimationFrame配合drawImage了.

只要不停的改变截取图片的位置就可以了.
我们来看下面的demo1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>爆炸飞机切换</title>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
let windowW = document.body.clientWidth
let windowH = document.body.clientHeight
let canvas = document.querySelector('#canvas')
canvas.width = windowW
canvas.height = windowH
let ctx = canvas.getContext('2d')
let frame = 0 //帧数
let img1 = new Image()
img1.src = 'img/herofly.png'

//定义变量:图片截取的位置(x,y) 图片截取的宽高(w,h) 整张大图的宽度, 截取的飞机在canvas中的位置(iX, iY)
let x = 0, y = 0, w = 66, h = 82, img1W = 330, iX = 0, iY = 0;

animate()
function animate() {
//定义一个帧数的变量,函数每一帧执行一次,则frame就加一次,以此记录帧数
frame++
ctx.clearRect(0, 0, canvas.width, canvas.height)

//每过20帧执行一次 x += w 以此达到切换图片的效果
if(frame % 20 === 0 ) {
x += w
if (x >= img1W - w) { //判定当走到最后一张爆炸图的时候,让x又等于0, 达到无限动画的效果
x = 0
}
}
//每隔一帧就执行绘画飞机的操作
ctx.drawImage(img1, x, y, w, h, iX, iY, w, h)

//为避免frame加到太大,在这里做一个当frame加到10000时,又让它为0的操作
if(frame > 10000 ) {
frame = 0
}

//利用requestAnimationFrame达到动画效果
requestAnimationFrame(animate)
}
</script>
</body>
</html>

3.3 canvas中图片运动的动画

上面我们介绍的是在一张大图中,持续改变它截取图片的位置(也就是x, y ),来达到切换图片的效果.这种转化有些类似于”静态的转化”.

那么怎样让图片在canvas中移动呢,改变的就是我们drawImage()中的第6,7个参数(也就是截取下来的图片在canvas中的位置)

还是利用demo1中的那张飞机图,只不过这次我不让它”爆炸”了(不进行图片切换),而是让它从canvas的最下边飞到最上边

来看demo2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>爆炸飞机切换</title>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
let windowW = document.body.clientWidth
let windowH = document.body.clientHeight
let canvas = document.querySelector('#canvas')
canvas.width = windowW
canvas.height = windowH
let ctx = canvas.getContext('2d')
let frame = 0 //帧数
let img1 = new Image()
img1.src = 'img/herofly.png'

//定义变量:图片截取的位置(x,y) 图片截取的宽高(w,h) 整张大图的宽度, 截取的飞机在canvas中的位置(iX, iY)
let x = 0, y = 0, w = 66, h = 82, img1W = 330, iX = 0, iY = canvas.height - h;

animate()
function animate() {
//定义一个帧数的变量,函数每一帧执行一次,则frame就加一次,以此记录帧数
frame++
ctx.clearRect(0, 0, canvas.width, canvas.height)

//每过20帧执行一次 iY -= 4 以此达到图片运动的效果
if(frame % 20 === 0 ) {
iY -= 4
if (iY <= 0) { //判定当飞机运动到最上边的时候,让iY又等于画布的高 - 飞机的高, 达到无限动画的效果
iY = canvas.height - h
}
}
//每隔一帧就执行绘画飞机的操作
ctx.drawImage(img1, x, y, w, h, iX, iY, w, h)

//为避免frame加到太大,在这里做一个当frame加到10000时,又让它为0的操作
if(frame > 10000 ) {
frame = 0
}

//利用requestAnimationFrame达到动画效果
requestAnimationFrame(animate)
}
</script>
</body>
</html>

可以看到上面的demo2 和 demo1 大致相同,只不过此时改变的是iY而已.

3.4 canvas中的视频

在页面中,插入一段视频,只需要使用标签

而在canvas中我们只需要将视频当图片一样插入,在利用canvas中的动画让它达到播放的效果.

例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
<div class="out">
<video id="video1" src="img/xiaoyin.mp4" style="width:300px;" autoplay></video>
<canvas id="myCanvas" width="1000" height="300"></canvas>
</div>

<script>
let canvas = document.querySelector("#myCanvas")
let ctx = canvas.getContext("2d")
let imgObj = document.querySelector('#video1')

function play(){
ctx.drawImage(imgObj, 0, 0,canvas.width,canvas.height)
window.requestAnimationFrame(play);
}
play()
</script>
</body>

此时页面中出现的应该是俩个视频,并且用canvas绘制出来的视频并不会卡顿,效果和直接用video的一样,要是你想只显示canvas的视频的话,可以将video1给display:none掉.
效果图:

image.png

3.5 灰色视频

在介绍讲解灰色视频之前,我想先介绍一个很牛x的方法getImageData(),这个方法能获取整张图片,或者一片图片区域的所有信息.
用法为:

1
2
ctx.drawImage(imgObj, 0, 0,canvas.width,canvas.height)
var imageData = ctx.getImageData(0,0, canvas.width, canvas.height);

来看下面这个小例子,点击按钮生成,将左侧彩色的图片变为灰色:

image.png

点击生成:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<body>
<div class="out">
<canvas id="canvas" width="300" height="400"></canvas>
<img id="PutImg" src="" alt="">
<input id="btn" type="button" value="生成">
</div>
<script>
let out = document.querySelector('.out')
let btn = document.querySelector('#btn')
let PutImg = document.querySelector('#PutImg')
let canvas = document.querySelector('#canvas')
let ctx = canvas.getContext('2d')

let img = new Image()
img.src = 'img/01.jpg'

img.onload=function () {
ctx.drawImage(img,0,0,canvas.width,canvas.height)

btn.onclick = function () {

var imageData = ctx.getImageData(0,0, canvas.width, canvas.height);
console.log(imageData);
var pixels = imageData.data;

//遍历像素点
for (var i=0; i<pixels.length; i+=4){

var r = pixels[i];
var g = pixels[i+1];
var b = pixels[i+2];

//获取灰色
var gray = parseInt((r+g+b)/3);

pixels[i] = gray;
pixels[i+1] = gray;
pixels[i+2] = gray;
}
ctx.putImageData(imageData, 0,0);
let url = canvas.toDataURL()
PutImg.src = url

ctx.clearRect(0,0,canvas.width,canvas.height)
}

}
</script>
</body>

我们可以将上面获取到的imageData对象打印出来看下:

image.png

这个imageData对象中有3个属性,分别是data,高度,宽度
那么这个data可以看出是一个数组,而且是一个长度为480000的数组
那么这个数组是怎么来的呢.
其实这个数组存储的是所有像素点的颜色信息
你可以理解为,我的这张图片是300x400像素的,也就是有120000个像素点,而一个像素点的颜色(也就是rgba) 是由个值组成的,分别是r,g,b,a的值
也就是说数组中每4个值代表的就是一个像素点的信息.
如前4个值[134,134,134,225] 表示的就是第一个像素点(最左上角的)的信息.
所以在做灰色处理时,我们只需要将每个像素点的前三个值全部一样的就可以了,然后在利用putImageData()方法来输出一下处理好的图片.

而视频的处理也是一样的
在例1的基础上加以改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<body>
<div class="out">
<video id="video1" src="img/xiaoyin.mp4" style="width:300px;" autoplay></video>
<canvas id="myCanvas" width="1000" height="300"></canvas>
</div>

<script>
let canvas = document.querySelector("#myCanvas")
let ctx = canvas.getContext("2d")
let imgObj = document.querySelector('#video1')

function play(){
ctx.drawImage(imgObj, 0, 0,canvas.width,canvas.height)

var imageData = ctx.getImageData(0,0, canvas.width, canvas.height);

var pixels = imageData.data;

//遍历像素点
for (var i=0; i<pixels.length; i+=4){

var r = pixels[i];
var g = pixels[i+1];
var b = pixels[i+2];

//获取灰色
var gray = parseInt((r+g+b)/3);

pixels[i] = gray;
pixels[i+1] = gray;
pixels[i+2] = gray;
}

ctx.putImageData(imageData, 0,0);

window.requestAnimationFrame(play);
}
play()
</script>
</body>

此时我们的视频就变成灰色的了
效果图:

image.png

评论