最近在公司內部的技術協會論壇裏閒逛的時候,無心中發現了一篇手淘前端大牛岑安兩年前寫的博文,講述了canvas的2d貼圖技術。看到後以爲至關神奇。因而就本身實現了一下。不過岑安前輩的那篇博文也只是大概講述了一下實現思路,整個邏輯仍是本身慢慢摸索出來的,過程仍是挺心酸的,因此在此記錄一下而且分享一下,讓跟我同樣喜歡canvas的人有所收穫吧。html
廢話不說,先把demo貼出來,好歹讓大夥看看咱們要實現怎樣的效果:前端
第一個demo: 圖像拉扯變形demo_1git
第二個demo: 圖像3d變形demo_2github
看完demo,是否以爲挺好玩的?canvas
若是以爲好玩,那就繼續看下去吧,接下來我將逐步分析整個實現邏輯。主要講的就是第一個demo的實現邏輯,由於第二個就是在第一個的基礎上實現的,只要理解了第一個的原理,第二個就變得很簡單了。app
第一個demo中,實現了對圖像的拉扯,涉及到這種變形的,首先想到的就是transform,沒錯,就是canvas的2d繪圖API中的transform啦。transform方法中傳入的abcdef六個值就是變換矩陣的參數。也就是說,咱們能夠經過修改這六個值來實現對圖片的變形操做。post
transform() 容許您縮放、旋轉、移動並傾斜當前的環境。若是對transform不是很瞭解的,能夠看這篇博文:http://yehao.diandian.com/post/2012-12-30/40046242001 裏面講的仍是很詳細的。性能
瞭解了transform以後,你會發現,transform能作的,好像就只有縮放、旋轉、移動、傾斜這幾個功能。可是demo1中能夠拉扯成各類形狀,感受不像是用這幾個就能實現的。可是其實,還真就是用這幾個變換實現的。this
demo1貼圖右側有個數值選擇,當選擇1,而且選擇顯示方框的時候,咱們看到是這樣一個畫面:spa
沒錯,這個是什麼意思呢,說明這張圖片其實分紅了兩塊,左上角的三角形以及右下角的三角形,咱們拖動一下圖片,再看一下效果:
爲了方便理解,我加了輔助線,畫了輔助線後,就變得很簡單了,至關於分紅了兩塊,上面正常的圖片,一塊是變成了由紅色圈起來的,另外一塊則是變成了由黑色圈起來的,當用畫筆補全後,兩個三角形都實際上是一個平行四邊形,而從矩形變成平行四邊形,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的邏輯。
至此,整個過程都講述完了。感謝一閱。