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

前文

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

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

1. 引用图片

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

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

<body>
  <canvas id="canvas" width="500" height="500"></canvas>
</body>

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

<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

<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的图片

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

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

        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)的位置

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

3.canvas中的动画

3.1 requestAnimationFrame的简介

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

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实现动画

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:

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

<!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中的视频

在页面中,插入一段视频,只需要使用<video src="video1.mp4"></video>标签

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

例1:

<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(),这个方法能获取整张图片,或者一片图片区域的所有信息.
用法为:

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

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

image.png

点击生成:

image.png
<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的基础上加以改进:

<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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容