在看D3.js的時候,無心間看到了一個例子,以爲頗有趣,像是會分裂的圓形馬賽克。看了下代碼,使用svg完成的,可是具體實現方式使得在手機端沒法把玩,因而就本身實現了一個canvas版本的。代碼很簡單,canvas初學者能夠本身試試當作練筆,仍是挺有趣的一個效果。html
online demogit
在demo中任意從本地選擇一張圖片,而後經過鼠標移動或者移動端touchmove就能實現圓形分裂的效果。github
若是以爲用得着,你能夠在本身的項目中安裝使用這個效果。npm i circle-split -S
算法
說是難點,其實根本不難。一開始看到的時候會好奇大大小小的圓形的顏色是怎麼計算的,計算該面積下的平均值?其實很簡單,就是從繪製了圖片的canvas上獲取圓心座標在圖片對應位置上的顏色值。這樣的算法在圓形半徑較大的時候,對被遮蓋的圖片區域顏色表明性其實很差,可是從整個分裂過程來看,這個取色方案的效果還不錯。npm
canvas繪圖:CanvasRenderingContext2D.drawImage()編程
canvas繪製圓形:CanvasRenderingContext2D.arc()canvas
canvas上取指定座標上的顏色值:CanvasRenderingContext2D.getImageData()跨域
將圖片繪製在一個offline(即不用掛在DOM樹上)的canvas上,爲了在指定位置獲取顏色用數組
建立另外一個canvas,用來繪製圓。兩個canvas尺寸保持一致(並且都是方形),方便無需座標轉換獲取顏色安全
繪製第一個圓形,以canvas中心爲圓心,使用對應offline canvas座標上的顏色填充
維持一個circles
數組,表明全部的圓,每一個元素有座標(x, y),半徑(r)和是否標記分裂(readyToSplit)
須要一個渲染循環(rendering loop),不斷的找出被標記須要分裂(readyToSplit)的圓,拿去作分裂繪製
事件處理:當mousemove或者touchmove發生在圓上時,該圓被標記readyToSplit = true
,後面的則有渲染循環去處理
在我本身作這樣的編程時,會以測試驅動的方式開始代碼。所以會腦子裏先寫下本身的類將被如何使用,怎麼樣可以簡單易用。
我打算把這個效果封裝成一個類,它將在使用時被實例化。最終的效果確定是要在DOM樹上顯示的,因此這裏在實例化時確定須要指定一個mount節點,全部的事情在其內部進行。並且,按照一般的習慣,開放一些配置,使得使用者能夠作一些簡單的定製化。可是目前尚未想好哪些內部的配置拿出來比較合適,因此第二個參數options
能夠後面再考慮。
var cs = new CircleSplit('#mountNode', options);
我但願可以動態的切換顯示的圖片內容,因此想提供一個setImage
的方法,它應該能接受圖片路徑,或者Image
元素對象。
cs.setImage(image);
OK,這就是目前我但願的實例化方式,和想要提供的接口。後面再具體實現過程當中,能夠再繼續添加或者修改。
結合前面談到的實現思路,考慮CircleSplit
類裏面該若是定義屬性和私有共有方法。
從構造函數入手。我的習慣在構造函數最後加上init方法,init方法裏作一些準備工做,完成setImage
前的一些必要的事情。
function CircleSplit (el, options) { ... this._init(); } CircleSplit.prototype._init = function () { this._createSourceCanvas(); // 建立源canvas,用來繪製圖片,做爲offline canvas,提供座標顏色使用 this._createTargetCanvas(); // 建立目標canvas,用來繪製看到的大大小小的圓 this._render(); // 開啓渲染循環 this.bindEvent(); // 綁定事件,touchmove mousemove這些 }
這樣咱們一會兒多了好幾個函數,並且目的都很明確,所以能夠很容易的判斷須要那些實例屬性和該如何實現各自函數體。這裏可能須要多注意一下_render()
,思路中談到在這裏應該去繪製須要分裂的圓,那麼大體應該像下面這樣:
CircleSplit.prototype._render = function () { // 循環體 this.circles.forEach(function (circle) { if (circle.readyToSplit) { this._splitCircle(circle); circle.readyToSplit = false; } }, this); // 下一個循環 requestAnimationFrame(this._render.bind(this)); }
而何時設置circle.readyToSplit
呢?就是在bindEvent()
的事件處理函數裏面。這裏會經過_tagCircle()
遍歷circles,找到能hit到事件座標的一個圓,將其標記(tag)上readyToSplit。
從共有方法入手。setImage
以後,至關於將整個CircleSplit中的狀態都重置了下,circles
數組得重置,兩個canvas得重置等。
CircleSplit.prototype.setImage = function (image) { this._resetCanvas(this.sourceCanvas); // clear source canvas this._drawSourceImage(image); // draw source canvas this._resetCanvas(this.targetCanvas); // clear target canvas this._drawCircle(x, y, r) // draw target canvas。繪製第一個,也是最大的一個圓形。圓心爲canvas中心,半徑爲canvas的一半 }
_drawSourceImage()
裏面就是調用了CanvasRenderingContext2D.drawImage()
進行圖片繪製。這個API函數有3種傳參形式,我這裏選擇了5參數的形式,使用了本身寫的簡易的居中庫CenterIt,來解決圖片居中繪製問題:不管圖片尺寸,均可以輕易的居中覆蓋填充(cover)或者居中包含(contain)填充。
這裏的_drawCircle(x, y, r)
應該能重用,後面每次圓形分裂的時候都能調用。初步給它3個參數,圓心座標和半徑。在其內部應該可以本身去獲取座標對應的顏色值。因此簡單想象一下它的內部:
CircleSplit.prototype._drawCircle = function (x, y, r) { ... context.fillStyle = this._getColor(x, y); // 獲取座標顏色 context.beginPath(); context.arc(x, y, r, 0, 2 * Math.PI); context.closePath(); context.fill(); ... }
繪製圓時使用CanvasRenderingContext2D.arc()
API,使用起來不算簡單明瞭,每次還須要begin和close Path。相比而下,一些canvas的遊戲庫或者圖形庫,則簡單直觀的多:
// create.js var circle = new createjs.Shape(); circle.graphics.beginFill("DeepSkyBlue").drawCircle(0, 0, 50); // two.js var circle = two.makeCircle(72, 100, 50); circle.fill = '#FF8000'; circle.stroke = 'orangered'; circle.linewidth = 5;
所以,若是要作比較複雜的繪製操做,推薦找一個適合本身的canvas庫,會使得工做變得容易的多。
關於_getColor()
函數,這裏使用了CanvasRenderingContext2D.getImageData()
:
CircleSplit.prototype._getColor = function (x, y) { ... var pixelData = this.sourceCanvas.getContext('2d').getImageData(parseInt(x), parseInt(y), 1, 1).data; return 'rgb(' + pixelData[0] + ',' + pixelData[1] + ',' + pixelData[2] + ')'; }
以下圖:
假設左上角起始點爲(x, y),一個方格爲一個像素,那麼getImageData(x, y, 1, 1).data
就會返回[255,0,0,255]
,表明Red=255,Alpha=255。若是getImageData(x, y, 2, 2).data
就會返回[255,0,0,255, 255,0,0,255, 255,0,0,255, 255,0,0,255]
長度爲16的數組,每4個爲一組表明一個像素上的rgba值。getImageData()
就是一個能幫助咱們對canvas進行像素級別操做的API函數。
一些基於canvas的「刮刮卡」插件,也是getImageData()
的應用:在圖片上絕對定位一個灰色的canvas,表明刮刮卡蒙層;經過對手指觸摸的像素點的alpha值進行修改來實現被「刮「開的效果。固然這裏的修改須要使用到配套的putImageData()
函數;同時對整個canvas像素中alpha值爲0的像素點的百分比 進行統計,能夠完成刮開了80%就展現所有圖片的效果。
上面是大體的實現思路,和編碼的思想過程。爲了表達出我本身在完成一個功能的時候,是如何從無到有,定義屬性,定義API的。只是本身的一點經驗,但願有幫助。
若是你對這些知識不熟悉,卻也感興趣的話,能夠參考該github項目代碼
github上的代碼與上面講的思路一致,可是會有些不同,主要是在功能實現以後,發現了一個須要優化的地方。
渲染速度 在_render()
渲染循環中,咱們對全部的circles進行遍歷。可是當整副圖片分裂次數很完全時,會有上萬個圓,會致使每一個渲染循環裏的計算時間過長,致使下一個渲染循環在理想的時間後才執行,從而致使了卡頓的感受。因而爲了解決這個問題,引入了renderingCircles
數組,將被標記的circle所有插入這個數組中,渲染循環中只關心這裏的值,用額外的存儲空間換更短的計算時間。
顯示模糊 最早的實現中,兩個canvas得尺寸是根據mountNode決定的,canvas.width
canvas.height
被設爲和mountNode同樣的維度值。因而在一些設備上顯示出明顯的邊緣鋸齒。這裏的解決方案就是設置canvas的寬和高爲兩倍於mountNode的寬高,而後經過style去設置canvas顯示成和mountNode同樣的尺寸。這裏就是canvas的自身的寬高屬性和canvas style的寬高以前的區別的理解和應用。
圖片跨域問題 在canvas操做圖片時,可能會碰到這樣的錯誤信息:Unable to get image data from canvas because the canvas has been tainted by cross-origin data.
關於這個的官方解釋是:
在canvas上能夠繪製沒有跨域許可的圖片資源(images without CORS approval),可是這樣作會「感染(taints)」的canvas,而在感染的(tainted)canvas上調用
toBlog()
,toDataURL()
,getImageData()
會拋出上面的安全方面的錯誤。
在CircleSplit.setImage(imageUrl)
時,可能就會碰到這個問題。
解決方案,首先須要圖片有跨域許可。這個須要在提供圖片服務的server上進行配置。這裏很少介紹,有跨域許可的圖片被加載時,在控制檯上應該能看到:(這裏我使用的七牛的圖片)
其次,須要在加載圖片時,設置crossOrigin屬性:
var image = new Image(); image.crossOrigin = 'anonymous'; image.onload = function () {}; image.src = imageUrl;
其實我的很喜歡最後完成的交互效果(有點強迫症,喜歡不斷的戳掉泡泡),因而將這個小效果作了一個簡單的H5頁面,在年末這個時間點裏,講述和回顧在2016年的大事件。你也能夠來體驗下:2016-recap