實驗6 鳥巢
簡介
本實驗的目的是利用CanvasRenderingContext2D的一些API繪制一個鳥巢圖像,如圖1-20所示。

圖1-20 鳥巢
涉及的知識點和技巧包括:三次貝塞爾曲線的繪制,CanvasRenderingContext2D的translate和rotate等API。
橢圓繪制
在CanvasRenderingContext2D開放的一些曲線繪制API當中,有繪制線段、矩形和圓形的,沒有繪制橢圓的。所以需要自己去封裝一個繪制橢圓的方法,如:
function drawEllipse(x, y, w, h) { //code here }
其中,x、y為橢圓的左上角的坐標(不是中心坐標),w和h分別為長軸和短軸。
在實現這個方法之前,很容易就可以想到下面幾種預選方案:
● 根據橢圓笛卡兒坐標系方程繪制;
● 根據橢圓極坐標方程繪制;
● 利用四條貝塞爾曲線繪制。
第一種和第二種方式都是基于點的,以不斷地連接橢圓上的所有點來擬合一個完整的橢圓,這樣會帶來大量的計算和重復調用CanvasRenderingContext2D的API。在Canvas性能優化中很重要的一點就是盡量少地調用CanvasRenderingContext2D的API。比如下面兩份代碼的對比,第二段代碼的性能遠遠優于第一段。
代碼一:
for (var i=0; i < points.length-1; i++) { var p1=points[i]; var p2=points[i+1]; context.beginPath(); context.moveTo(p1.x, p1.y); context.lineTo(p2.x, p2.y); context.stroke(); }
代碼二:
context.beginPath(); for (var i=0; i < points.length-1; i++) { var p1=points[i]; var p2=points[i+1]; context.moveTo(p1.x, p1.y); context.lineTo(p2.x, p2.y); } context.stroke();
第三種,也是性能最好的一種,繪制過程只需調用4次CanvasRenderingContext2D. bezierCurveTo,這樣可以避免復雜的計算和大量重復調用CanvasRenderingContext2D的API。
所以采用第三種方式來繪制橢圓。代碼如下所示:
function drawEllipse(x, y, w, h) { var k=0.55228475; var ox=(w / 2) * k; var oy=(h / 2) * k; var xe=x+w; var ye=y+h; var xm=x+w / 2; var ym=y+h / 2; ctx.beginPath(); ctx.moveTo(x, ym); ctx.bezierCurveTo(x, ym-oy, xm-ox, y, xm, y); ctx.bezierCurveTo(xm+ox, y, xe, ym-oy, xe, ym); ctx.bezierCurveTo(xe, ym+oy, xm+ox, ye, xm, ye); ctx.bezierCurveTo(xm-ox, ye, x, ym+oy, x, ym); ctx.stroke(); }
三次貝塞爾曲線
上面通過繪制4段三次貝塞爾曲線來繪制一個橢圓。要知道以上代碼中系數k的由來,可推導如下:
如圖1-21所示,P0、P1、P2、P3四個點在平面或在三維空間中定義了三次貝塞爾曲線。曲線起始于P0走向P1,并從P2的方向來到P3。一般不會經過P1或P2,這兩個點只是在那里提供方向信息。P0和 P1之間的間距決定了曲線在轉而趨近 P3之前,走向 P2方向的“長度有多長”。

圖1-21 三次貝塞爾曲線
曲線的參數形式為:
B(t)=P0 (1-t)3+3P1 t(1-t)2+3P2 t2 (1-t)+P3 t3 , t ∈ [0,1]
貝塞爾曲線擬合弧如圖1-22所示。

圖1-22 貝塞爾曲線擬合弧
從圖1-22可看出,x0=0,x2=1,x3=1,x1就是需要求的k的值。根據對稱性,解出x1就等于得到y2的值。將這些值代入三次貝塞爾曲線,馬上能得到如下方程:
x=(3x1-2)t3+ (3-6x1)t2+3x1t
令t=0.5時,曲線上的點落在四分之一圓的45°切點上,于是有
sin(π/4)=0.125×(3x1-2)+0.25×(3- 6x1)+0.5×(3x1)
解得x1的值為0.55228475,也就是k的值。
旋轉橢圓
繪制完橢圓,需要旋轉橢圓來形成鳥巢。這里的旋轉不是繞上面的drawEllipse的前兩個參數x,y旋轉,而是繞橢圓的中心旋轉。所以僅僅使用CanvasRenderingContext2D.rotate是不夠的,因為CanvasRenderingContext2D.rotate是繞畫布的左上角(0,0)旋轉的。所以先要把(0,0)通過CanvasRenderingContext2D.translate到橢圓的中心,然后再drawEllipse (-a/2,-b/2,a, b)。
以上就是繞中心旋轉的核心。這里還可以推廣到任意圖形或者圖片(假設有約定的中心),如圖1-23所示。

圖1-23 Canvas坐標變換
完整的代碼:
var canvas; var ctx; var px=0; var py=0; function init() { canvas=document.getElementById("myCanvas2"); ctx=canvas.getContext("2d"); ctx.strokeStyle="#fff"; ctx.translate(70, 70); } init(); var i=0; function drawEllipse(x, y, w, h) { var k=0.5522848; var ox=(w / 2) * k; var oy=(h / 2) * k; var xe=x+w; var ye=y+h; var xm=x+w / 2; var ym=y+h / 2; ctx.beginPath(); ctx.moveTo(x, ym); ctx.bezierCurveTo(x, ym-oy, xm-ox, y, xm, y); ctx.bezierCurveTo(xm+ox, y, xe, ym-oy, xe, ym); ctx.bezierCurveTo(xe, ym+oy, xm+ox, ye, xm, ye); ctx.bezierCurveTo(xm-ox, ye, x, ym+oy, x, ym); ctx.stroke(); ctx.translate(x+70, y+100); px=-70; py=-100; ctx.rotate(10 * Math.PI * 2 / 360); } var ct; var drawAsync=eval(Jscex.compile("async", function (ct) { while (true) { drawEllipse(px, py, 140, 200) $await(Jscex.Async.sleep(200, ct)); } })) function start() { ct=new Jscex.Async.CancellationToken(); drawAsync(ct).start(); } function stop() { ct.cancel(); }
這里加入了Jscex最新的取消模型。當然也可以用下面幾種方式取消任務。
第一種:
var xxxAsync=eval(Jscex.compile("async", function () { while (condition) { .... dosomething .... $await(Jscex.Async.sleep(1000)); } }))
第二種:
var xxxAsync=eval(Jscex.compile("async", function () { while (true) { if (condition) { //dosomething break; } //dosomething $await(Jscex.Async.sleep(1000)); } }))
第二種方式的好處是可以在if(condition)中做一些初始化設置。
因為break只能跳出當前循環,所以有些場景要使用一些技巧。如下面這種場景:
var xxxAsync=eval(Jscex.compile("async", function () { while (true) { for (i in XXX) { if (condition) { //要在這里跳出最外層的循環。 } } //dosomething $await(Jscex.Async.sleep(1000)); } }))
其解決方案是:
var xxxAsync=eval(Jscex.compile("async", function () { while (true) { for (i in XXX) { if (condition) { //要在這里跳出最外層的循環。 breakTag=true; } } if (breakTag) break; //dosomething $await(Jscex.Async.sleep(1000)); } }))
另外一種復雜的場景如下所示:
var countAsync1=eval(Jscex.compile("async", function () { while (true) { for (i in XXX) { if (condition) { //要在這里跳出最外層的循環 } } $await(Jscex.Async.sleep(1000)); } })) var countAsync2=eval(Jscex.compile("async", function () { while (true) { for (i in XXX) { if (condition) { //要在這里跳出最外層的循環。 } } $await(Jscex.Async.sleep(1000)); } })) var executeAsyncQueue=eval(Jscex.compile("async", function () { while (true) { $await(countAsync1()) $await(countAsync2()) $await(Jscex.Async.sleep(1000)); } })) executeAsyncQueue().start();
其解決方案如下:
在if(condition)中設置breakTag=true,然后執行隊列時跳出整個循環:
var executeAsyncQueue=eval(Jscex.compile("async", function () { while (true) { $await(countAsync1()) $await(countAsync2()) if (breakTag) break; $await(Jscex.Async.sleep(1000)); } }))
最后一種場景依然利用breakTag跳出整個循環,如下所示:
var xxAsync=eval(Jscex.compile("async", function () { while (true) { //dosomething $await(xxxAsync()) if (breakTag) break; $await(Jscex.Async.sleep("1000")); } })) var xxxAsync=eval(Jscex.compile("async", function () { if (condition) { breakTag=true; } $await(Jscex.Async.sleep("1000")); }))
小結
通過本實驗了解了貝塞爾曲線的運用及其擬合橢圓的原理,然后分析了Jscex的各種取消方法和其自身的取消模型。通過這個取消模型,也可以體會到Jscex的優勢,Jscex使程序員可以使用線性的思維寫程序,而不必關心回調,在一些線性混合深度嵌套的場景下,優勢特別明顯。
- 網站建設與網頁設計從入門到精通Dreamweaver+Flash+Photoshop+HTML+CSS+JavaScript
- 柳工出海:中國制造的全球化探索
- 版面設計與制作
- 網頁配色從入門到精通
- Dreamweaver CS5+ASP動態網站設計實用手冊
- 眾妙之門:網站UI設計之道2
- jQuery+Bootstrap Web開發案例教程(在線實訓版)
- 園區網互聯及網站建設
- CSS+DIV網頁樣式與布局案例指導
- Div+CSS網頁制作實戰教程
- 網頁制作與網站建設寶典
- Dreamweaver CC中文版網頁設計與制作從新手到高手
- 社交網站界面設計(原書第2版)
- Highcharts網頁圖表制作實例詳解 (Web開發典藏大系)
- Dreamweaver CS6網頁設計入門、進階與提高