canvas 2d 貼圖技術實踐


  最近在公司內部的技術協會論壇里閑逛的時候,無意中發現了一篇手淘前端大牛岑安兩年前寫的博文,講述了canvas的2d貼圖技術。看到后覺得相當神奇。於是就自己實現了一下。不過岑安前輩的那篇博文也只是大概講述了一下實現思路,整個邏輯還是自己慢慢摸索出來的,過程還是挺心酸的,所以在此記錄一下並且分享一下,讓跟我一樣喜歡canvas的人有所收獲吧。

  廢話不說,先把demo貼出來,好歹讓大伙看看我們要實現怎樣的效果:

  第一個demo: 圖像拉扯變形demo_1

  第二個demo: 圖像3d變形demo_2

  看完demo,是否覺得挺好玩的?

  如果覺得好玩,那就繼續看下去吧,接下來我將逐步分析整個實現邏輯。主要講的就是第一個demo的實現邏輯,因為第二個就是在第一個的基礎上實現的,只要理解了第一個的原理,第二個就變得很簡單了。

  第一個demo中,實現了對圖像的拉扯,涉及到這種變形的,首先想到的就是transform,沒錯,就是canvas的2d繪圖API中的transform啦。transform方法中傳入的abcdef六個值就是變換矩陣的參數。也就是說,我們可以通過修改這六個值來實現對圖片的變形操作。

  transform() 允許您縮放、旋轉、移動並傾斜當前的環境。如果對transform不是很了解的,可以看這篇博文:http://yehao.diandian.com/post/2012-12-30/40046242001    里面講的還是很詳細的。

  

 

  了解了transform之后,你會發現,transform能做的,好像就只有縮放、旋轉、移動、傾斜這幾個功能。但是demo1中可以拉扯成各種形狀,感覺不像是用這幾個就能實現的。但是其實,還真就是用這幾個變換實現的。

  demo1貼圖右側有個數值選擇,當選擇1,並且選擇顯示方框的時候,我們看到是這樣一個畫面:

  

  沒錯,這個是什么意思呢,說明這張圖片其實分成了兩塊,左上角的三角形以及右下角的三角形,我們拖動一下圖片,再看一下效果:

  

 

  為了方便理解,我加了輔助線,畫了輔助線后,就變得很簡單了,相當於分成了兩塊,上面正常的圖片,一塊是變成了由紅色圈起來的,另一塊則是變成了由黑色圈起來的,當用畫筆補全后,兩個三角形都其實是一個平行四邊形,而從矩形變成平行四邊形,transform就能做了,當變成我們需要的形狀的時候,再通過canvas的clip方法,只截取一半的三角形,把兩塊三角形合並起來。就有了拉扯效果了。

  而為了讓拉扯效果更真實,就自然就需要使用更多的三角區域,當我把矩形分成20*20個小矩形,也就是20*20*2個三角形的時候,當鼠標拉扯時就出現了以下效果:

  

  以上,就是demo1的整個理論邏輯。

  接下來就講代碼該如何實現:

  首先是圖片的變形效果,也就是用transform,要傳入矩陣參數,起初我是用向量來做的,但是做到后面發現向量做起來會有好多其他問題。比如:圖片拉扯過度的時候,圖片翻轉就出問題了,等等。。。

  所以最后,還是選擇了用代數法來實現,也就是要解三元一次方程!!!!

  為啥說是三元一次方程呢?因為按照transform的矩陣運算的規則

                |a , b , 0|

  [X,Y,1] = [x , y , 1] * |c , d , 0|

               |e , f , 1|

  解出來就是這樣:X = ax + cy +e  和  Y = bx + dy + f  , 也就是,新的坐標的XY值就等於舊的坐標的xy值進行一些運算后可以得到。

  相對的,也就是說,只要我們知道了平行四邊形三個頂點變換前后的坐標值,我們就可以算出abcdef六個矩陣參數,然后我們先用transform改變繪制環境,再把圖片繪制到平行四邊形變換前的位置,就可以繪制出相應的傾斜效果了。

  所以,首先我們要封裝出一個解三元一次方程以及獲取矩陣參數的方法:

  先是解三元一次方程的方法,具體原理我就不講了,百度一下就知道了,或者有琢磨精神的可以自己親自拿筆算一下:

/**
     * 解三元一次方程,需要傳入三組方程參數
     * @param arr1        第一組參數
     * @param arr2        第二組參數
     * @param arr3        第三組參數
     * @returns {{x: number, y: number, z: number}}
     */
    function equation(arr1 , arr2 , arr3){
        var a1 = +arr1[0];
        var b1 = +arr1[1];
        var c1 = +arr1[2];
        var d1 = +arr1[3];

        var a2 = +arr2[0];
        var b2 = +arr2[1];
        var c2 = +arr2[2];
        var d2 = +arr2[3];

        var a3 = +arr3[0];
        var b3 = +arr3[1];
        var c3 = +arr3[2];
        var d3 = +arr3[3];

        //分離計算單元
        var m1 = c1 - (b1 * c2 / b2);
        var m2 = c2 - (b2 * c3 / b3);
        var m3 = d2 - (b2 * d3 / b3);
        var m4 = a2 - (b2 * a3 / b3);
        var m5 = d1 - (b1 * d2 / b2);
        var m6 = a1 - (b1 * a2 / b2);

        //計算xyz
        var x = ((m1 / m2) * m3 - m5)/((m1 / m2) * m4 - m6);
        var z = (m3 - m4 * x) / m2;
        var y = (d1 - a1 * x - c1 * z) / b1;

        return {
            x : x,
            y : y,
            z : z
        }
    }

  然后就是獲取矩陣,其實就是將各個參數整理一下,傳入解方程的方法中,進行處理: 

/**
     * 根據變化前后的點坐標,計算矩陣
     * @param arg_1     變化前坐標1
     * @param _arg_1    變化后坐標1
     * @param arg_2     變化前坐標2
     * @param _arg_2    變化后坐標2
     * @param arg_3     變化前坐標3
     * @param _arg_3    變化后坐標3
     * @returns {{a: number, b: number, c: number, d: number, e: number, f: number}}
     */
    function getMatrix(arg_1 , _arg_1 , arg_2 , _arg_2 , arg_3 , _arg_3){
        //傳入x值解第一個方程 即  X = ax + cy + e 求ace
        //傳入的四個參數,對應三元一次方程:ax+by+cz=d的四個參數:a、b、c、d,跟矩陣方程對比c為1
        var arr1 = [arg_1.x , arg_1.y , 1 , _arg_1.x];
        var arr2 = [arg_2.x , arg_2.y , 1 , _arg_2.x];
        var arr3 = [arg_3.x , arg_3.y , 1 , _arg_3.x];

        var result = equation(arr1 , arr2 , arr3);

        //傳入y值解第二個方程 即  Y = bx + dy + f 求 bdf
        arr1[3] = _arg_1.y;
        arr2[3] = _arg_2.y;
        arr3[3] = _arg_3.y;

        var result2 = equation(arr1 , arr2 , arr3);

        //獲得a、c、e
        var a = result.x;
        var c = result.y;
        var e = result.z;

        //獲得b、d、f
        var b = result2.x;
        var d = result2.y;
        var f = result2.z;

        return {
            a : a,
            b : b,
            c : c,
            d : d,
            e : e,
            f : f
        };
    }

  計算完畢,就可以獲取到六個矩陣參數了。

  這兩個計算看似簡單,但是一不小心就容易出錯,樓主之前做的時候就一直出錯,一直不知道原因在哪,最后手動把三元一次方程解了一遍,才發現是某個參數錯了。所以樓主把這兩個計算封裝了一下,以便以后再利用:

  github地址:https://github.com/whxaxes/wheels/tree/master/matrix   有興趣或者有需要的可以一用

 

  回歸正題,到了現在,我們就可以獲取到所有的矩陣參數了,接下來要解決的問題,就是如何把任意四個點連線形成的四邊形分成N份的邏輯了(注意:是任意四個點,因為拉升之后,四個點形成的坐標就不是矩形了)。這個就可以用向量來做了,用向量的話,計算量會小很多,對性能的提升也是很有幫助。

  怎么實現呢,畫個圖就清晰了:

  

  我們只需要獲取到AD向量,以及BC向量,把兩個向量N等分,然后用個循環,在每一等分上獲取AB方向的向量,然后再進行N等分,再計算,就可以獲取到所有的點了。因為用的是向量,所以我們完全不用考慮角度的問題,無論四邊形的形狀如何,只要我們有四個點的坐標,就可以計算出里面的所有點坐標。代碼如下:

/**
     * 將abcd四邊形分割成n的n次方份,獲取n等分后的所有點坐標
     * @param n     多少等分
     * @param a     a點坐標
     * @param b     b點坐標
     * @param c     c點坐標
     * @param d     d點坐標
     * @returns {Array}
     */
    function rectsplit(n , a , b , c , d){
        //ad向量方向n等分
        var ad_x = (d.x - a.x)/n;
        var ad_y = (d.y - a.y)/n;
        //bc向量方向n等分
        var bc_x = (c.x - b.x)/n;
        var bc_y = (c.y - b.y)/n;

        var ndots = [];
        var x1, y1, x2, y2, ab_x, ab_y;

        //左邊點遞增,右邊點遞增,獲取每一次遞增后的新的向量,繼續n等分,從而獲取所有點坐標
        for(var i=0;i<=n;i++){
            //獲得ad向量n等分后的坐標
            x1 = a.x + ad_x * i;
            y1 = a.y + ad_y * i;
            //獲得bc向量n等分后的坐標
            x2 = b.x + bc_x * i;
            y2 = b.y + bc_y * i;

            for(var j=0;j<=n;j++){
                //ab向量為:[x2 - x1 , y2 - y1],所以n等分后的增量為除於n
                ab_x = (x2 - x1)/n;
                ab_y = (y2 - y1)/n;

                ndots.push({
                    x: x1 + ab_x * j,
                    y: y1 + ab_y * j
                })
            }
        }

        return ndots;
    }

  計算完畢,並且把點繪制到各個坐標上的時候,拖動四個頂點,就出現了以下效果,無論我的四個頂點位置如何變幻,都能保證所有點的位置不會錯。

  

  當這個也計算完畢,整個demo的制作就基本上完成了,然后就是進行圖片的渲染了,接下來的邏輯就相當簡單了,先是用上面的rectsplit方法把當前的四邊形分成N份,並且獲取所有坐標點,當然還需要直接獲取初始四邊形分成N份后的所有坐標點,不過這個是可以在剛開始的時候就初始化好,因為這個數值是不會變的,沒必要重復計算。

  兩組點坐標獲取到,然后傳入方法里計算矩陣,以及進行clip處理,再把圖片繪制上去,整個渲染過程就完成了。

/**
     * 畫布渲染
     */
    function render(){
        ctx.clearRect(0,0,canvas.width,canvas.height);

        var ndots = rectsplit(count, dots[0], dots[1], dots[2], dots[3]);

        ndots.forEach(function(d , i){
            //獲取四邊形的四個點
            var dot1 = ndots[i];
            var dot2 = ndots[i + 1];
            var dot3 = ndots[i + count + 2];
            var dot4 = ndots[i + count + 1];

            //獲取初始四邊形的四個點
            var idot1 = idots[i];
            var idot2 = idots[i + 1];
            var idot3 = idots[i + count + 2];
            var idot4 = idots[i + count + 1];

            if (dot2 && dot3 && i%(count+1)<count){
                //繪制三角形的下半部分
                renderImage(idot3, dot3, idot2, dot2, idot4, dot4);

                //繪制三角形的上半部分
                renderImage(idot1, dot1, idot2, dot2, idot4, dot4);
            }

            if(hasDot){
                ctx.save();
                ctx.fillStyle = "red";
                ctx.fillRect(d.x-1 , d.y-1 , 2 , 2);
                ctx.save();
            }
        });
    }

    /**
     * 計算矩陣,同時渲染圖片
     * @param arg_1
     * @param _arg_1
     * @param arg_2
     * @param _arg_2
     * @param arg_3
     * @param _arg_3
     */
    function renderImage(arg_1 , _arg_1 , arg_2 , _arg_2 , arg_3 , _arg_3){
        ctx.save();
        //根據變換后的坐標創建剪切區域
        ctx.beginPath();
        ctx.moveTo(_arg_1.x, _arg_1.y);
        ctx.lineTo(_arg_2.x, _arg_2.y);
        ctx.lineTo(_arg_3.x, _arg_3.y);
        ctx.closePath();
        if(hasRect){
            ctx.lineWidth = 2;
            ctx.strokeStyle = "red";
            ctx.stroke();
        }
        ctx.clip();

        if(hasPic){
            //傳入變換前后的點坐標,計算變換矩陣
            var result = matrix.getMatrix.apply(this , arguments);

            //變形
            ctx.transform(result.a , result.b , result.c , result.d , result.e , result.f);

            //繪制圖片
            ctx.drawImage(img , idots[0].x , idots[0].y , img.width , img.height);
        }

        ctx.restore();
    }

  

  至此,demo1的整個理論原理以及代碼邏輯都分析完畢,下面貼出該項目的github地址:

  https://github.com/whxaxes/canvas-test/tree/gh-pages/src/Funny-demo/transform  

  

  當demo1做出來的時候,demo2也就很簡單了,因為,只要我們知道四邊形的各個點變換前后的坐標值,我們就可以讓圖片變形成任何我們想要的樣子。

  而上面的demo2就是在demo1的基礎上,加入了z軸的影響,x,y軸都僅僅是平面上的,當加入了z軸以后,再將z軸的值映射到x,y軸上來,然后再進行圖片變換,就有了demo2的效果。demo2的源碼也在上面那個github地址上,里面的demo1.js就是demo1的,demo2.js就是demo2的邏輯。

  

 

  至此,整個過程都講述完了。感謝一閱。

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM