摘要:在中任意從本地選擇一張圖片,然后通過鼠標移動或者移動端就能實現圓形分裂的效果。個人習慣在構造函數最后加上方法,方法里做一些準備工作,完成前的一些必要的事情。繪制第一個,也是最大的一個圓形。
Online Demo在看D3.js的時候,無意間看到了一個例子,覺得很有趣,像是會分裂的圓形馬賽克。看了下代碼,使用svg完成的,但是具體實現方式使得在手機端無法把玩,于是就自己實現了一個canvas版本的。代碼很簡單,canvas初學者可以自己試試當做練筆,還是挺有趣的一個效果。
online demo
在demo中任意從本地選擇一張圖片,然后通過鼠標移動或者移動端touchmove就能實現圓形分裂的效果。
使用如果覺得用得著,你可以在自己的項目中安裝使用這個效果。npm i circle-split -S
難點說是難點,其實根本不難。一開始看到的時候會好奇大大小小的圓形的顏色是怎么計算的,計算該面積下的平均值?其實很簡單,就是從繪制了圖片的canvas上獲取圓心坐標在圖片對應位置上的顏色值。這樣的算法在圓形半徑較大的時候,對被遮蓋的圖片區域顏色代表性其實不好,但是從整個分裂過程來看,這個取色方案的效果還不錯。
關鍵技術點canvas繪圖:CanvasRenderingContext2D.drawImage()
canvas繪制圓形:CanvasRenderingContext2D.arc()
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
原文地址:http://blog.jackyang.me/blog/...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/50320.html
摘要:在中任意從本地選擇一張圖片,然后通過鼠標移動或者移動端就能實現圓形分裂的效果。個人習慣在構造函數最后加上方法,方法里做一些準備工作,完成前的一些必要的事情。繪制第一個,也是最大的一個圓形。 在看D3.js的時候,無意間看到了一個例子,覺得很有趣,像是會分裂的圓形馬賽克。看了下代碼,使用svg完成的,但是具體實現方式使得在手機端無法把玩,于是就自己實現了一個canvas版本的。代碼很簡單...
摘要:注以下所有代碼托管到動畫的數理分析有了前面的基礎知識,現在我們就會想如果我們能夠在每秒幀,內渲染張圖像,并且每一張圖像的內容發生微調,那么在秒鐘整個畫面就會產生動畫效果了。 什么是動畫? 就像思考哲學問題無法回避思維和存在的關系一樣,制作動畫同樣無法逃避的問題是動畫的原理是什么?這里提一句題外話,任何原理的東西通常難以讓你短期拾掇成果,但在隱約的未來會起到難以置信的效果,不信就看接下來...
閱讀 1523·2021-09-22 15:35
閱讀 2005·2021-09-14 18:04
閱讀 876·2019-08-30 15:55
閱讀 2449·2019-08-30 15:53
閱讀 2680·2019-08-30 12:45
閱讀 1203·2019-08-29 17:01
閱讀 2577·2019-08-29 15:30
閱讀 3514·2019-08-29 15:09