2013年的時候曾經使用canvas實現了一個擦除效果的需求,即模擬用戶在模糊的玻璃上擦除水霧看到清晰景色的交互效果。好在2012年的時候學習HTML5的時候研究過canvas了,所以在比較短的時間內實現了一個方案【下文方案一】,後來繼續探索之後進一步更新了這個方案【下文方案二】,提高了交互的性能,也提升了用戶體驗。
今年初的另一個項目,提出了一個比較類似的需求,不過不是擦除效果,需要在一張地圖上動態顯示霧霾驅散的效果,這個交互需求有個小難點,霧霾的邊緣是模糊的,而不是常見的那種整齊的。
這裡說明一點,用canvas實現擦除的基本原理是與視覺效果剛好相反的,從視覺和直覺邏輯上看,擦除就是擦掉表層的圖像而顯露出底層的圖案,但是在技術實現上,剛好相反,需要被擦除的圖像如模糊的玻璃是直接顯示的,而擦除後顯示的清晰圖案則是在其上繪制的,看上去就像是擦除了模糊的玻璃。
方案一:持續重繪思路下的擦除
這個方案的思路主要是利用canvas的clip方法,該方法可以在指定的位置以特定的形狀來裁剪圖片,這樣就可以實現蒙版效果,因為該方法在調用的時候需要指定位置,因此要實現根據手指或者鼠標動態地指定不同位置的最直接的思路就是canvas動畫的基本思路--持續重繪,就是在一個持續不斷的循環中調用該接口,傳遞給該接口的坐標是手指的實際位置。
HTML結構:
<div>
<img src="foo.jpg" />
<canvas texsrc="foo.jpg" imgsrc='bar.jpg' width="100" height="100" style='position:absolute;width:100px; height:100px; left:0px; top:0px;background: transparent;'></canvas>
</div>
從HTML結構可以看出上面所說的【原理相反】:需要被擦除的圖片(foo.jpg)是位於底層的,而擦除後顯示的圖片(bar.jpg)是位於上層的。因為canvas的background樣式設置為了透明,這也就從視覺上欺騙了用戶,它其實是在上層,但是因為透明,所以除了繪制的部分,其他部分看不見,形成它在下層的錯覺。
主體JS代碼如下:
function CanvasDoodle(canvas){
this.canvas=canvas;
this.ctx=canvas.getContext("2d");
this.imgSrc=canvas.getAttribute("imgsrc");
this.width=canvas.width;
this.height=canvas.height;
this.left=parseInt(canvas.style.left);
this.top=parseInt(canvas.style.top);
this.touchX=0;
this.touchY=0;
this.requireLoop=false;
this.init();
}
CanvasDoodle.prototype={
init:function(){
document.body.setAttribute("needRefresh","true");
var _self=this;
this.img=new Image();
this.img.src=this.imgSrc;
this.canvas.addEventListener('mousedown',function(e){
e.preventDefault();
_self.requireLoop=true;
_self.touchX= e.clientX-_self.left,_self.touchY= e.clientY-_self.top;
_self.loop();
},false);
this.canvas.addEventListener('mousemove',function(e){
e.preventDefault();
if(_self.requireLoop){
_self.touchX= e.clientX-_self.left,_self.touchY= e.clientY-_self.top;
}
},false);
this.canvas.addEventListener('mouseup',function(e){
e.preventDefault();
_self.requireLoop=false;
});
},
loop:function(){
if(this.requireLoop){
var _self=this;
requesetAnimFrame(function () {_self.loop()});
this.render();
}
},
render:function(){
var _self=this;
_self.ctx.save();
_self.ctx.beginPath();
_self.ctx.arc(_self.touchX,_self.touchY,15,0,Math.PI*2,true);
_self.ctx.clip();
_self.ctx.drawImage(_self.img,0,0,_self.width,_self.height,0,0,_self.width,_self.height);
_self.ctx.restore();
}
};
new CanvasDoodle(document.getElementById('CanvasDoodle'));
實際效果如圖:
代碼比較簡單,核心部分就是render方法,根據當前鼠標或者手指的位置在canvas的上下文中繪制一個圓形,然後裁剪,這樣在下一步drawImage的時候就會在上下文中繪制一個圓形的局部圖形而不是整個圖片。這樣在鼠標或者手指移動的時候就會動態繪制很多小圓,連起來就像是擦除了。
requesetAnimFrame相信大家不會陌生,是個循環調用。這裡為了節省性能,設置了一個變量requireLoop來表示是否需要重繪,只有在鼠標按下或者手指接觸的時候設置為真,在每個循環中開始重繪canvas(即調用render),結束的時候則設置為假,停止繪制。
這個方案是最原始的方案,有兩大缺陷:
一是循環調用,盡管有一個requireLoop可以確定是否重繪,在交互發生的時候始終是在循環,性能並不好;
二是循環調用和用戶的交互速度是不同步的,理想狀況是手指或者鼠標每發生變化就重繪一次,但是現實並非如此,在非常快速滑動的時候,每次動態獲取的坐標並不是緊緊相連的,就造成擦除的效果不是連續的,體驗會變差。
方案二:No more loops
方案一的可優化點最明顯的就是循環,其兩大缺陷都是基於此的。因此方案二的主要思路放棄了clip方法。而是利用了canvas上下文的strokeStyle屬性,該屬性是指在canvas中繪制矢量圖形的時候矢量線的繪制樣式,其值可以為color(顏色值)、gradient(漸變對象)、pattern(pattern對象)。這個方案就是將方案一中的drawImg方式改為將canvas上下文的strokeStyle設置為圖片,然後在繪制的時候直接畫線就可以了,因為矢量線的背景就是需要展示的圖片,這樣就實現了擦除的效果。HTML結構不變,JS代碼如下:
function CanvasDoodle(canvas){
this.canvas=canvas;
this.ctx=canvas.getContext("2d");
this.imgSrc=canvas.getAttribute("imgsrc");
this.width=canvas.width;
this.height=canvas.height;
this.left=parseInt(canvas.style.left);
this.top=parseInt(canvas.style.top);
this.touchX=0;
this.touchY=0;
this.needDraw=false;
this.init();
}
CanvasDoodle.prototype={
init:function(){
var _self=this;
var img=new Image();
img.onload=function(){
var pat=_self.ctx.createPattern(img,"no-repeat");
_self.ctx.strokeStyle=pat;
_self.ctx.lineCap="round";
_self.ctx.lineJoin="round";
_self.ctx.lineWidth="25";
}
img.src=this.imgSrc;
this.canvas.addEventListener('mousedown',function(e){
e.preventDefault();
_self.needDraw=true;
_self.ctx.beginPath();
_self.ctx.moveTo(e.clientX-_self.left,e.clientY-_self.top);
},false);
this.canvas.addEventListener('mousemove',function(e){
e.preventDefault();
if(_self.needDraw){
_self.ctx.lineTo(e.clientX-_self.left,e.clientY-_self.top);
_self.ctx.stroke();
}
},false);
this.canvas.addEventListener('mouseup',function(e){
e.preventDefault();
_self.needDraw=false;
});
}
};
new CanvasDoodle(document.getElementById('CanvasDoodle'));
可以看到,已經沒有循環調用了,只是在初始化的時候就設置strokeStyle為圖片,在鼠標移動的時候直接lineTo然後stroke就可以了,即簡單,又高效,並且即使快速移動鼠標也不會出現鋸齒邊緣了,因此這個改進的方案完全替代了方案一。效果如下:
新需求,新方案
相信今年年初的霧霾應該是婦孺皆知的了,因為一位前央視記者自費做了一個長期調查然後做了一次演講,掀起了軒然大波。這個需求正是在這個時期提出的。希望在地圖上動態顯示霧霾的驅散效果。
先來點簡單的
這裡我們先分析另外一個略微簡單一些的需求,循序漸進。
稍微變一下,是需要展示霧霾擴散效果,這將會是相對來說比較容易實現的,因為霧霾可以理解為均勻的灰色,即使不是均勻,也可以表示為自圓心向邊緣不同程度的灰色漸變,上文說到過canvas的strokeStyle可以設置為漸變的,因此正好利用漸變就可以實現邊緣的模糊。(並且還可以給canvas設置一些css3的動畫,比如從小變大,或者由暗變明)。
HTML結構基本不變,主體JS代碼如下:
function CanvasFade(canvas){
this.canvas=canvas;
this.ctx=canvas.getContext("2d");
this.width=canvas.width;
this.height=canvas.height;
}
CanvasFade.prototype={
draw:function(config){
var _self=this;
var cfg=config?config:{x:200,y:200,r:120};
var ratio=cfg.r/2;
var grd = _self.ctx.createRadialGradient(cfg.x, cfg.y, 0.000, cfg.x, cfg.y, cfg.r);
grd.addColorStop(0.000, 'rgba(255, 0, 0, 0.900)');
grd.addColorStop(0.5, 'rgba(255, 0, 0, 0.600)');
grd.addColorStop(1.0, 'rgba(255, 0, 0, 0.000)');
_self.ctx.fillStyle = grd;
_self.ctx.arc(cfg.x, cfg.y, cfg.r,0,Math.PI*2,true);
_self.ctx.fill();
}
};
var canvasFade=new CanvasFade(Jquery('#theCanvas')[0]);
canvasFade.draw({x:100,y:200y,r:20r});
可以看出實現起來非常簡單,這段代碼是測試使用的,顏色值是紅色,看起來就像熱區圖,要實現霧霾擴散,只需要更改顏色值即可,實際效果如下:
ps. 這裡是多次調用了draw的最終效果。
硬骨頭
僅從視覺上看,這兩個需求非常接近,因此很容易誤以為目標就要實現了。程序開發的一大特點就是[There is more to it then meets the eyes]。那些看起來很炫酷的交互對於開發者可能非常容易實現,因為廠商可能在底層已經實現了。而那些看起來很簡單的東西,可能需要花費更多的力氣,這也常常成為產品人員和開發人員摩擦的一個原因。
我們來分析需求,霧霾本身覆蓋在地圖上,並不是均勻的(當然也可以簡化成均勻的,這裡不是主要困難點),主要問題就是霧霾驅散之後顯示出來的是沒有霧霾覆蓋的地圖,而不是純顏色(可以參看文末gif圖片)。邊緣的模糊效果就很難實現,因為在設置strokeStyle的時候,如果設置為漸變色,很容易實現邊緣模糊,但是就只能用顏色值,如果把strokeStyle設置為pattern就可以使用圖片,可是這時就沒法設置漸變了,邊緣就是整齊切割的,無法滿足需求,在反復嘗試和求助google之後,終於在stackoverflow上找到了一點線索,貌似有個外國哥們也撞上了類似的需求,不過他非常聰明地繞過了strokeStyle這個問題,所以最終的實現方案就是受到他的啟發而實現的,並非由我原創的。
先看代碼:
function clipArc(ctx, x, y, r, f) {
var temp = document.createElement('canvas'),
tx = temp.getContext('2d');
temp.width = ctx.canvas.width;
temp.height = ctx.canvas.height;
tx.translate(-temp.width, 0);
tx.shadowOffsetX = temp.width;
tx.shadowOffsetY = 0;
tx.shadowColor = '#000';
tx.shadowBlur = f;
tx.arc(x, y, r, 0, 2 * Math.PI);
tx.closePath();
tx.fill();
ctx.save();
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(temp, 0, 0);
ctx.restore();
}
function CanvasFade(canvas){
this.canvas=canvas;
this.ctx=canvas.getContext("2d");
this.imgSrc=canvas.getAttribute("imgSrc");
this.width=canvas.width;
this.height=canvas.height;
}
CanvasFade.prototype={
init:function(config){
var _self=this;
var cfg=config?config:{x:100,y:100,r:120,f:40};
var img=new Image();
img.onload=function(){
var pat=_self.ctx.createPattern(img,"no-repeat");
_self.ctx.fillStyle=pat;
_self.ctx.fillRect(0, 0, _self.width, _self.height);
clipArc(_self.ctx, cfg.x, cfg.y, cfg.r, cfg.f);
};
img.src=this.imgSrc;
}
};
var c=document.querySelector('#theCanvas');
var cf=new CanvasFade(c);
cf.init();
這裡的秘密武器就是利用了shadow,即陰影,在canvas裡邊繪制圖形的時候,可以給圖形添加陰影,而陰影可以有邊緣模糊的效果。這裡在實際繪制的時候,先創建了一個過渡canvas(這個canvas本身並不繪制圖形,主要起模糊剪切的作用),將這個canvas向左平移了一個寬度,這樣它就移出了當前canvas的可視范圍,然而精妙之處在於它上下文的shadowOffsetX設置為了向右一個寬度,這樣其內部任何圖形的陰影剛好又落在了當前canvas的正確位置,這裡設置了它的陰影顏色為黑色,但是有一定的羽化效果(tx.shadowBlur = f),另一個秘密武器就是globalCompositeOperation,這個屬性用來設定如何將一個源(新的)圖像繪制到目標(已有的)的圖像上,其詳細信息可以參考http://www.html5canvastutorials.com/advanced/html5-canvas-global-composite-operations-tutorial/ ,實際效果如下圖:
可以看出實際效果還是非常不錯的。並且動畫的形式可以是更多樣的,而且這種形式也可以有更多的變種,以滿足更廣泛的需求。
至此,利用canvas制作擦除和擴散效果的方法介紹完畢了。歡迎批評指正^_^
--------------------------------------分割線 --------------------------------------
HTML5 地理位置定位(HTML5 Geolocation)原理及應用 http://www.linuxidc.com/Linux/2012-07/65129.htm
HTML5移動開發即學即用(雙色) PDF+源碼 http://www.linuxidc.com/Linux/2013-09/90351.htm
HTML5入門學習筆記 http://www.linuxidc.com/Linux/2013-09/90089.htm
HTML5移動Web開發筆記 http://www.linuxidc.com/Linux/2013-09/90088.htm
HTML5 開發中的本地存儲的安全風險 http://www.linuxidc.com/Linux/2013-06/86486.htm
《HTML5與CSS3權威指南》及相配套源碼 http://www.linuxidc.com/Linux/2013-02/79950.htm
關於 HTML5 令人激動的 10 項預測 http://www.linuxidc.com/Linux/2013-02/79917.htm
HTML5與CSS3實戰指南 PDF http://www.linuxidc.com/Linux/2013-02/79910.htm
--------------------------------------分割線 --------------------------------------