canvas的重頭戲--圖片的處理

前文

相信接觸過一些canvas的小伙們都應(yīng)該會(huì)有這樣的一句感嘆: canvas 強(qiáng) 真的強(qiáng)!
不僅可以靜態(tài)的創(chuàng)建一些我們用普通標(biāo)簽無(wú)法實(shí)現(xiàn)的圖形,而且還能讓這些圖形動(dòng)起來(lái).

其實(shí)在實(shí)際的開發(fā)中,要是你只會(huì)用canvas畫一些矩形啊,三角形啊,五角星等等的東西肯定是不夠的.
因?yàn)檎嬲陂_發(fā)中,canvas大部分都是用來(lái)對(duì)圖片以及視頻做處理,所以博主今天在這里想要介紹的是一些關(guān)于canvas對(duì)圖片的處理

1. 引用圖片

我們知道想在網(wǎng)頁(yè)中顯示一張圖片,我們只需要用<img src="">就可以實(shí)現(xiàn)了,那么在canvas中我們是怎樣插入一張圖片的呢.

1.首先在body中創(chuàng)建好一個(gè)canvas標(biāo)簽

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

2.在js代碼中獲取canvas并創(chuàng)建一個(gè)<img>元素

<script>
        let canvas = document.querySelector('#canvas')  //獲取canvas對(duì)象
        let ctx = canvas.getContext('2d')             //獲取2d上下文
        let img = new Image()                            //創(chuàng)建img
        img.src = 'img/green.jpg'                        //給img添加資源
</script>

3.? 繪制img,考慮到圖片是從網(wǎng)絡(luò)加載贝咙,如果 drawImage 的時(shí)候圖片還沒有完全加載完成,則什么都不做抱怔,個(gè)別瀏覽器會(huì)拋異常硝岗。所以我們應(yīng)該保證在 img 繪制完成之后再 drawImage

<script>
        let canvas = document.querySelector('#canvas')
        let ctx = canvas.getContext('2d')
        let img = new Image()
        img.src = 'img/green.jpg'
        //圖片是否已經(jīng)加載完成
        img.onload = function () {
            ctx.drawImage(this, 100, 100, this.width / 2, this.height / 2)
        }
    </script>

通過上面的三個(gè)步驟,這時(shí)候打開你們的瀏覽器就可以在頁(yè)面中看到對(duì)應(yīng)的圖片了.
這里主要用到的是drawImage這個(gè)方法,下面是對(duì)其的一些詳細(xì)講解.

2.解析drawImage( )

對(duì)于drawImage()這個(gè)方法,有三種使用的方式:

第一種:

只傳入3個(gè)參數(shù)

drawImage(image,x,y)

參數(shù)1: image:
指的就是你的圖片對(duì)象,
也就是你let img = new Image()中的img
參數(shù)2 : x
圖片相對(duì)于畫布原點(diǎn)(0,0)也就是畫布的最左上角 的x軸方向的坐標(biāo)
參數(shù)3: y
圖片相對(duì)于畫布原點(diǎn)(0,0)也就是畫布的最左上角 的y軸方向的坐標(biāo)

第二種:

傳入5個(gè)參數(shù)

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

前面三個(gè)參數(shù)和第一種的使用方式一樣.

參數(shù)4,5: width 和 height
可以規(guī)定圖片的寬度和高度.
如:在畫布(100,100)的位置插入一張300*300的圖片

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

那么利用width和height我們可以發(fā)現(xiàn),想要將圖片縮減為其原始大小的一半,就可以這樣寫:

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

第三種:

傳入9個(gè)參數(shù)
當(dāng)在drawImage()中傳入9個(gè)參數(shù)后,這個(gè)方法的用法將和前面?zhèn)z種不一樣了.

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

第三種的使用方式傳遞的是9個(gè)參數(shù),

參數(shù)1 : image
還是圖片的對(duì)象

參數(shù)2,3 : 從一張大圖上指定要截取小圖的位置(x,y)坐標(biāo)
參數(shù)4,5: 從一張大圖上指定要截取小圖的大小
參數(shù)6,7: 從一張大圖上截取下來(lái)的小圖要放在canvas(畫布)中的位置(x,y)
參數(shù)8,9: 截取下來(lái)小圖規(guī)定的寬高

如下圖中,有5架小飛機(jī),我只想截取最后一架并顯示在畫布中.

![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,一架飛機(jī)就是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中的動(dòng)畫

3.1 requestAnimationFrame的簡(jiǎn)介

我們利用普通的定時(shí)器來(lái)實(shí)現(xiàn)動(dòng)畫的寫法為:

var x = 0;

function animate(){
  //清除畫布內(nèi)容
  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();

可以看到上面的動(dòng)畫是靠setTimeout這個(gè)定時(shí)器每隔30毫秒調(diào)用一次animate() 來(lái)實(shí)現(xiàn)的.

這種利用定時(shí)器來(lái)實(shí)現(xiàn)動(dòng)畫效果在移動(dòng)端實(shí)際來(lái)說是很不可取的,在移動(dòng)端上看到的動(dòng)畫會(huì)很卡頓,造成用戶體驗(yàn)很不流程.
所以ES6新增了一個(gè)類似于定時(shí)器的API:
requestAnimationFrame()
它只有一個(gè)參數(shù),就是要執(zhí)行的函數(shù).

使用requestAnimationFrame實(shí)現(xiàn)動(dòng)畫

var x = 0;

function animate(){
  //清除畫布內(nèi)容
  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();

可以看到倆段代碼的區(qū)別,僅僅是一個(gè)用的是setTimout,一個(gè)是requestAnimationFrame

setTimout表示的是: 每隔30毫秒,執(zhí)行一次animate()函數(shù).
而requestAnimationFrame 在一秒中執(zhí)行多少次是由它的應(yīng)用場(chǎng)景決定的,一般都能達(dá)到58~60次.也就是1000/60(相當(dāng)于定時(shí)器16毫秒執(zhí)行一次)

那么這里得到的1000/60就是一幀.不同的場(chǎng)景幀數(shù)可能會(huì)不一樣.

3.2 canvas中切換圖片的動(dòng)畫

還是利用上面的那種飛機(jī)圖.我現(xiàn)在想實(shí)現(xiàn)一個(gè)從第一架完整飛機(jī)變化到最后一架爆炸飛機(jī)的效果.


herofly.png

那么有心的小伙就會(huì)發(fā)現(xiàn)了,在js中我們想實(shí)現(xiàn)圖片的切換,只要改變背景圖的background-position就可以了,那么在canvas中利用的就是requestAnimationFrame配合drawImage了.

只要不停的改變截取圖片的位置就可以了.
我們來(lái)看下面的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>爆炸飛機(jī)切換</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       //幀數(shù)
    let img1 = new Image()
    img1.src = 'img/herofly.png'

    //定義變量:圖片截取的位置(x,y) 圖片截取的寬高(w,h) 整張大圖的寬度, 截取的飛機(jī)在canvas中的位置(iX, iY)
    let x = 0, y = 0, w = 66, h = 82, img1W = 330, iX = 0, iY = 0;

    animate()
    function animate() {
        //定義一個(gè)幀數(shù)的變量,函數(shù)每一幀執(zhí)行一次,則frame就加一次,以此記錄幀數(shù)
        frame++
        ctx.clearRect(0, 0, canvas.width, canvas.height)

        //每過20幀執(zhí)行一次 x += w 以此達(dá)到切換圖片的效果
        if(frame % 20 === 0 ) {
            x += w
            if (x >= img1W - w) {   //判定當(dāng)走到最后一張爆炸圖的時(shí)候,讓x又等于0, 達(dá)到無(wú)限動(dòng)畫的效果
                x = 0
            }
        }
        //每隔一幀就執(zhí)行繪畫飛機(jī)的操作
        ctx.drawImage(img1, x, y, w, h, iX, iY, w, h)

        //為避免frame加到太大,在這里做一個(gè)當(dāng)frame加到10000時(shí),又讓它為0的操作
        if(frame > 10000 ) {
            frame = 0
        }

        //利用requestAnimationFrame達(dá)到動(dòng)畫效果
        requestAnimationFrame(animate)
    }
</script>
</body>
</html>

3.3 canvas中圖片運(yùn)動(dòng)的動(dòng)畫

上面我們介紹的是在一張大圖中,持續(xù)改變它截取圖片的位置(也就是x, y ),來(lái)達(dá)到切換圖片的效果.這種轉(zhuǎn)化有些類似于"靜態(tài)的轉(zhuǎn)化".

那么怎樣讓圖片在canvas中移動(dòng)呢,改變的就是我們drawImage()中的第6,7個(gè)參數(shù)(也就是截取下來(lái)的圖片在canvas中的位置)

還是利用demo1中的那張飛機(jī)圖,只不過這次我不讓它"爆炸"了(不進(jìn)行圖片切換),而是讓它從canvas的最下邊飛到最上邊

來(lái)看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>爆炸飛機(jī)切換</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       //幀數(shù)
    let img1 = new Image()
    img1.src = 'img/herofly.png'

    //定義變量:圖片截取的位置(x,y) 圖片截取的寬高(w,h) 整張大圖的寬度, 截取的飛機(jī)在canvas中的位置(iX, iY)
    let x = 0, y = 0, w = 66, h = 82, img1W = 330, iX = 0, iY = canvas.height - h;

    animate()
    function animate() {
        //定義一個(gè)幀數(shù)的變量,函數(shù)每一幀執(zhí)行一次,則frame就加一次,以此記錄幀數(shù)
        frame++
        ctx.clearRect(0, 0, canvas.width, canvas.height)

        //每過20幀執(zhí)行一次 iY -= 4 以此達(dá)到圖片運(yùn)動(dòng)的效果
        if(frame % 20 === 0 ) {
            iY -= 4
            if (iY <= 0) {   //判定當(dāng)飛機(jī)運(yùn)動(dòng)到最上邊的時(shí)候,讓iY又等于畫布的高 - 飛機(jī)的高, 達(dá)到無(wú)限動(dòng)畫的效果
                iY = canvas.height - h
            }
        }
        //每隔一幀就執(zhí)行繪畫飛機(jī)的操作
        ctx.drawImage(img1, x, y, w, h, iX, iY, w, h)

        //為避免frame加到太大,在這里做一個(gè)當(dāng)frame加到10000時(shí),又讓它為0的操作
        if(frame > 10000 ) {
            frame = 0
        }

        //利用requestAnimationFrame達(dá)到動(dòng)畫效果
        requestAnimationFrame(animate)
    }
</script>
</body>
</html>

可以看到上面的demo2 和 demo1 大致相同,只不過此時(shí)改變的是iY而已.

3.4 canvas中的視頻

在頁(yè)面中,插入一段視頻,只需要使用<video src="video1.mp4"></video>標(biāo)簽

而在canvas中我們只需要將視頻當(dāng)圖片一樣插入,在利用canvas中的動(dòng)畫讓它達(dá)到播放的效果.

例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>

此時(shí)頁(yè)面中出現(xiàn)的應(yīng)該是倆個(gè)視頻,并且用canvas繪制出來(lái)的視頻并不會(huì)卡頓,效果和直接用video的一樣,要是你想只顯示canvas的視頻的話,可以將video1給display:none掉.
效果圖:

image.png

3.5 灰色視頻

在介紹講解灰色視頻之前,我想先介紹一個(gè)很牛x的方法getImageData(),這個(gè)方法能獲取整張圖片,或者一片圖片區(qū)域的所有信息.
用法為:

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

來(lái)看下面這個(gè)小例子,點(diǎn)擊按鈕生成,將左側(cè)彩色的圖片變?yōu)榛疑?

image.png

點(diǎn)擊生成:

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;

            //遍歷像素點(diǎn)
            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對(duì)象打印出來(lái)看下:

image.png

這個(gè)imageData對(duì)象中有3個(gè)屬性,分別是data,高度,寬度
那么這個(gè)data可以看出是一個(gè)數(shù)組,而且是一個(gè)長(zhǎng)度為480000的數(shù)組
那么這個(gè)數(shù)組是怎么來(lái)的呢.
其實(shí)這個(gè)數(shù)組存儲(chǔ)的是所有像素點(diǎn)的顏色信息
你可以理解為,我的這張圖片是300x400像素的,也就是有120000個(gè)像素點(diǎn),而一個(gè)像素點(diǎn)的顏色(也就是rgba) 是由個(gè)值組成的,分別是r,g,b,a的值
也就是說數(shù)組中每4個(gè)值代表的就是一個(gè)像素點(diǎn)的信息.
如前4個(gè)值[134,134,134,225] 表示的就是第一個(gè)像素點(diǎn)(最左上角的)的信息.
所以在做灰色處理時(shí),我們只需要將每個(gè)像素點(diǎn)的前三個(gè)值全部一樣的就可以了,然后在利用putImageData()方法來(lái)輸出一下處理好的圖片.

而視頻的處理也是一樣的
在例1的基礎(chǔ)上加以改進(jìn):

<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;

        //遍歷像素點(diǎn)
        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>

此時(shí)我們的視頻就變成灰色的了
效果圖:

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末颈抚,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌角撞,老刑警劉巖答毫,帶你破解...
    沈念sama閱讀 223,002評(píng)論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件褥民,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡洗搂,警方通過查閱死者的電腦和手機(jī)消返,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門载弄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人撵颊,你說我怎么就攤上這事宇攻。” “怎么了倡勇?”我有些...
    開封第一講書人閱讀 169,787評(píng)論 0 365
  • 文/不壞的土叔 我叫張陵逞刷,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我妻熊,道長(zhǎng)夸浅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,237評(píng)論 1 300
  • 正文 為了忘掉前任扔役,我火速辦了婚禮帆喇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘亿胸。我一直安慰自己坯钦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,237評(píng)論 6 398
  • 文/花漫 我一把揭開白布侈玄。 她就那樣靜靜地躺著婉刀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪序仙。 梳的紋絲不亂的頭發(fā)上突颊,一...
    開封第一講書人閱讀 52,821評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音诱桂,去河邊找鬼洋丐。 笑死,一個(gè)胖子當(dāng)著我的面吹牛挥等,可吹牛的內(nèi)容都是我干的友绝。 我是一名探鬼主播,決...
    沈念sama閱讀 41,236評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼肝劲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼迁客!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起辞槐,我...
    開封第一講書人閱讀 40,196評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤掷漱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后榄檬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卜范,經(jīng)...
    沈念sama閱讀 46,716評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,794評(píng)論 3 343
  • 正文 我和宋清朗相戀三年鹿榜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了海雪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锦爵。...
    茶點(diǎn)故事閱讀 40,928評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖奥裸,靈堂內(nèi)的尸體忽然破棺而出险掀,到底是詐尸還是另有隱情,我是刑警寧澤湾宙,帶...
    沈念sama閱讀 36,583評(píng)論 5 351
  • 正文 年R本政府宣布樟氢,位于F島的核電站,受9級(jí)特大地震影響侠鳄,放射性物質(zhì)發(fā)生泄漏埠啃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,264評(píng)論 3 336
  • 文/蒙蒙 一畦攘、第九天 我趴在偏房一處隱蔽的房頂上張望霸妹。 院中可真熱鬧十电,春花似錦知押、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至畏线,卻和暖如春静盅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背寝殴。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工蒿叠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蚣常。 一個(gè)月前我還...
    沈念sama閱讀 49,378評(píng)論 3 379
  • 正文 我出身青樓市咽,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親抵蚊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子施绎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,937評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容