成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

JavaScript 編程精解 中文第三版 十七、在畫布上繪圖

habren / 3453人閱讀

摘要:貝塞爾曲線方法可以繪制一種類似的曲線。不同的是貝塞爾曲線需要兩個(gè)控制點(diǎn)而不是一個(gè),線段的每一個(gè)端點(diǎn)都需要一個(gè)控制點(diǎn)。下面是描述貝塞爾曲線的簡(jiǎn)單示例。

來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目

原文:Drawing on Canvas

譯者:飛龍

協(xié)議:CC BY-NC-SA 4.0

自豪地采用谷歌翻譯

部分參考了《JavaScript 編程精解(第 2 版)》

繪圖就是欺騙。

M.C. Escher,由 Bruno Ernst 在《The Magic Mirror of M.C. Escher》中引用

瀏覽器為我們提供了多種繪圖方式。最簡(jiǎn)單的方式是用樣式來規(guī)定普通 DOM 對(duì)象的位置和顏色。就像在上一章中那個(gè)游戲展示的,我們可以使用這種方式實(shí)現(xiàn)很多功能。我們可以為節(jié)點(diǎn)添加半透明的背景圖片,來獲得我們希望的節(jié)點(diǎn)外觀。我們也可以使用transform樣式來旋轉(zhuǎn)或傾斜節(jié)點(diǎn)。

但是,在一些場(chǎng)景中,使用 DOM 并不符合我們的設(shè)計(jì)初衷。比如我們很難使用普通的 HTML 元素畫出任意兩點(diǎn)之間的線段這類圖形。

這里有兩種解決辦法。第一種方法基于 DOM,但使用可縮放矢量圖形(SVG,Scalable Vector Graphics)代替 HTML。我們可以將 SVG 看成文檔標(biāo)記方言,專用于描述圖形而非文字。你可以在 HTML 文檔中嵌入 SVG,還可以在

xmlns屬性把一個(gè)元素(以及他的子元素)切換到一個(gè)不同的 XML 命名空間。這個(gè)由url定義的命名空間,規(guī)定了我們當(dāng)前使用的語(yǔ)言。在 HTML 中不存在標(biāo)簽,但這些標(biāo)簽在 SVG 中是有意義的,你可以通過這些標(biāo)簽的屬性來繪制圖像并指定樣式與位置。

和 HTML 標(biāo)簽一樣,這些標(biāo)簽會(huì)創(chuàng)建 DOM 元素,腳本可以和它們交互。例如,下面的代碼可以把元素的顏色替換為青色。

let circle = document.querySelector("circle");
circle.setAttribute("fill", "cyan");
canvas元素

我們可以在元素中繪制畫布圖形。你可以通過設(shè)置widthheight屬性來確定畫布尺寸(單位為像素)。

新的畫布是空的,意味著它是完全透明的,看起來就像文檔中的空白區(qū)域一樣。

標(biāo)簽允許多種不同風(fēng)格的繪圖。要獲取真正的繪圖接口,首先我們要?jiǎng)?chuàng)建一個(gè)能夠提供繪圖接口的方法的上下文(context)。目前有兩種得到廣泛支持的繪圖接口:用于繪制二維圖形的"2d"與通過openGL接口繪制三維圖形的"webgl"。

本書只討論二維圖形,而不討論 WebGL。但是如果你對(duì)三維圖形感興趣,我強(qiáng)烈建議大家自行深入研究 WebGL。它提供了非常簡(jiǎn)單的現(xiàn)代圖形硬件接口,同時(shí)你也可以使用 JavaScript 來高效地渲染非常復(fù)雜的場(chǎng)景。

您可以用getContext方法在 DOM 元素上創(chuàng)建一個(gè)上下文。

Before canvas.

After canvas.

在創(chuàng)建完context對(duì)象之后,作為示例,我們畫出一個(gè)紅色矩形。該矩形寬 100 像素,高 50 像素,它的左上點(diǎn)坐標(biāo)為(10,10)。

與 HTML(或者 SVG)相同,畫布使用的坐標(biāo)系統(tǒng)將(0,0)放置在左上角,并且y軸向下增長(zhǎng)。所以(10,10)是相對(duì)于左上角向下并向右各偏移 10 像素的位置。

直線和平面

我們可以使用畫布接口填充圖形,也就是賦予某個(gè)區(qū)域一個(gè)固定的填充顏色或填充模式。我們也可以描邊,也就是沿著圖形的邊沿畫出線段。SVG 也使用了相同的技術(shù)。

fillRect方法可以填充一個(gè)矩形。他的輸入為矩形框左上角的第一個(gè)xy坐標(biāo),然后是它的寬和高。相似地,strokeRect方法可以畫出一個(gè)矩形的外框。

兩個(gè)方法都不需要其他任何參數(shù)。填充的顏色以及輪廓的粗細(xì)等等都不能由方法的參數(shù)決定(像你的合理預(yù)期一樣),而是由上下文對(duì)象的屬性決定。

設(shè)置fillStyle參數(shù)控制圖形的填充方式。我們可以將其設(shè)置為描述顏色的字符串,使用 CSS 所用的顏色表示法。

strokeStyle屬性的作用很相似,但是它用于規(guī)定輪廓線的顏色。線條的寬度由lineWidth屬性決定。lineWidth的值都為正值。


當(dāng)沒有設(shè)置width或者height參數(shù)時(shí),正如示例一樣,畫布元素的默認(rèn)寬度為 300 像素,默認(rèn)高度為 150 像素。

路徑

路徑是線段的序列。2D canvas接口使用一種奇特的方式來描述這樣的路徑。路徑的繪制都是間接完成的。我們無法將路徑保存為可以后續(xù)修改并傳遞的值。如果你想修改路徑,必須要調(diào)用多個(gè)方法來描述他的形狀。


本例創(chuàng)建了一個(gè)包含很多水平線段的路徑,然后用stroke方法勾勒輪廓。每個(gè)線段都是由lineTo以當(dāng)前位置為路徑起點(diǎn)繪制的。除非調(diào)用了moveTo,否則這個(gè)位置通常是上一個(gè)線段的終點(diǎn)位置。如果調(diào)用了moveTo,下一條線段會(huì)從moveTo指定的位置開始。

當(dāng)使用fill方法填充一個(gè)路徑時(shí),我們需要分別填充這些圖形。一個(gè)路徑可以包含多個(gè)圖形,每個(gè)moveTo都會(huì)創(chuàng)建一個(gè)新的圖形。但是在填充之前我們需要封閉路徑(路徑的起始節(jié)點(diǎn)與終止節(jié)點(diǎn)必須是同一個(gè)點(diǎn))。如果一個(gè)路徑尚未封閉,會(huì)出現(xiàn)一條從終點(diǎn)到起點(diǎn)的線段,然后才會(huì)填充整個(gè)封閉圖形。


本例畫出了一個(gè)被填充的三角形。注意只顯示地畫出了三角形的兩條邊。第三條從右下角回到上頂點(diǎn)的邊是沒有顯示地畫出,因而在勾勒路徑的時(shí)候也不會(huì)存在。

你也可以使用closePath方法顯示地通過增加一條回到路徑起始節(jié)點(diǎn)的線段來封閉一個(gè)路徑。這條線段在勾勒路徑的時(shí)候?qū)⒈伙@示地畫出。

曲線

路徑也可能會(huì)包含曲線。繪制曲線更加復(fù)雜。

quadraticCurveTo方法繪制到某一個(gè)點(diǎn)的曲線。為了確定一條線段的曲率,需要設(shè)定一個(gè)控制點(diǎn)以及一個(gè)目標(biāo)點(diǎn)。設(shè)想這個(gè)控制點(diǎn)會(huì)吸引這條線段,使其成為曲線。線段不會(huì)穿過控制點(diǎn)。但是,它起點(diǎn)與終點(diǎn)的方向會(huì)與兩個(gè)點(diǎn)到控制點(diǎn)的方向平行。見下例:


我們從左到右繪制一個(gè)二次曲線,曲線的控制點(diǎn)坐標(biāo)為(60,10),然后畫出兩條穿過控制點(diǎn)并且回到線段起點(diǎn)的線段。繪制的結(jié)果類似一個(gè)星際迷航的圖章。你可以觀察到控制點(diǎn)的效果:從下端的角落里發(fā)出的線段朝向控制點(diǎn)并向他們的目標(biāo)點(diǎn)彎曲。

bezierCurve(貝塞爾曲線)方法可以繪制一種類似的曲線。不同的是貝塞爾曲線需要兩個(gè)控制點(diǎn)而不是一個(gè),線段的每一個(gè)端點(diǎn)都需要一個(gè)控制點(diǎn)。下面是描述貝塞爾曲線的簡(jiǎn)單示例。


兩個(gè)控制點(diǎn)規(guī)定了曲線兩個(gè)端點(diǎn)的方向。兩個(gè)控制點(diǎn)相對(duì)兩個(gè)端點(diǎn)的距離越遠(yuǎn),曲線就會(huì)越向這個(gè)方向凸出。

由于我們沒有明確的方法,來找出我們希望繪制圖形所對(duì)應(yīng)的控制點(diǎn),所以這種曲線還是很難操控。有時(shí)候你可以通過計(jì)算得到他們,而有時(shí)候你只能通過不斷的嘗試來找到合適的值。

arc方法是一種沿著圓的邊緣繪制曲線的方法。 它需要弧的中心的一對(duì)坐標(biāo),半徑,然后是起始和終止角度。

我們可以使用最后兩個(gè)參數(shù)畫出部分圓。角度是通過弧度來測(cè)量的,而不是度數(shù)。這意味著一個(gè)完整的圓擁有的弧度,或者2*Math.PI(大約為 6.28)的弧度?;《葟膱A心右邊的點(diǎn)開始并以順時(shí)針的方向計(jì)數(shù)。你可以以 0 起始并以一個(gè)比大的數(shù)值(比如 7)作為終止值,畫出一個(gè)完整的圓。


上面這段代碼繪制出的圖形包含了一條從完整圓(第一次調(diào)用arc)的右側(cè)到四分之一圓(第二次調(diào)用arc)的左側(cè)的直線。arc與其他繪制路徑的方法一樣,會(huì)自動(dòng)連接到上一個(gè)路徑上。你可以調(diào)用moveTo或者開啟一個(gè)新的路徑來避免這種情況。

繪制餅狀圖

設(shè)想你剛剛從 EconomiCorp 獲得了一份工作,并且你的第一個(gè)任務(wù)是畫出一個(gè)描述其用戶滿意度調(diào)查結(jié)果的餅狀圖。results綁定包含了一個(gè)表示調(diào)查結(jié)果的對(duì)象的數(shù)組。

const results = [
  {name: "Satisfied", count: 1043, color: "lightblue"},
  {name: "Neutral", count: 563, color: "lightgreen"},
  {name: "Unsatisfied", count: 510, color: "pink"},
  {name: "No comment", count: 175, color: "silver"}
];

要想畫出一個(gè)餅狀圖,我們需要畫出很多個(gè)餅狀圖的切片,每個(gè)切片由一個(gè)圓弧與兩條到圓心的線段組成。我們可以通過把一個(gè)整圓()分割成以調(diào)查結(jié)果數(shù)量為單位的若干份,然后乘以做出相應(yīng)選擇的用戶的個(gè)數(shù)來計(jì)算每個(gè)圓弧的角度。


但表格并沒有告訴我們切片代表的含義,它毫無用處。因此我們需要將文字畫在畫布上。

文本

2D 畫布的context對(duì)象提供了fillText方法和strokeText方法。第二個(gè)方法可以用于繪制字母輪廓,但通常情況下我們需要的是fillText方法。該方法使用當(dāng)前的fillColor來填充特定文字的輪廓。


你可以通過font屬性來設(shè)定文字的大小,樣式和字體。本例給出了一個(gè)字體的大小和字體族名稱。也可以添加italic或者bold來選擇樣式。

傳遞給fillTextstrokeText的后兩個(gè)參數(shù)用于指定繪制文字的位置。默認(rèn)情況下,這個(gè)位置指定了文字的字符基線(baseline)的起始位置,我們可以將其假想為字符所站立的位置,基線不考慮jp字母中那些向下突出的部分。你可以設(shè)置textAlign屬性(endcenter)來改變起始點(diǎn)的水平位置,也可以設(shè)置textBaseline屬性(topmiddlebottom)來設(shè)置基線的豎直位置。

在本章末尾的練習(xí)中,我們會(huì)回顧餅狀圖,并解決給餅狀圖分片標(biāo)注的問題。

圖像

計(jì)算機(jī)圖形學(xué)領(lǐng)域經(jīng)常將矢量圖形和位圖圖形分開來討論。本章一直在討論第一種圖形,即通過對(duì)圖形的邏輯描述來繪圖。而位圖則相反,不需要設(shè)置實(shí)際圖形,而是通過處理像素?cái)?shù)據(jù)來繪制圖像(光柵化的著色點(diǎn))。

我們可以使用drawImage方法在畫布上繪制像素值。此處的像素?cái)?shù)值可以來自元素,或者來自其他的畫布。下例創(chuàng)建了一個(gè)獨(dú)立的元素,并且加載了一張圖像文件。但我們無法馬上使用該圖片進(jìn)行繪制,因?yàn)闉g覽器可能還沒有完成圖片的獲取操作。為了處理這個(gè)問題,我們?cè)趫D像元素上注冊(cè)一個(gè)"load"事件處理程序并且在圖片加載完之后開始繪制。


默認(rèn)情況下,drawImage會(huì)根據(jù)原圖的尺寸繪制圖像。你也可以增加兩個(gè)參數(shù)來設(shè)置不同的寬度和高度。

如果我們向drawImage函數(shù)傳入 9 個(gè)參數(shù),我們可以用其繪制出一張圖片的某一部分。第二個(gè)到第五個(gè)參數(shù)表示需要拷貝的源圖片中的矩形區(qū)域(xy坐標(biāo),寬度和高度),同時(shí)第六個(gè)到第九個(gè)參數(shù)給出了需要拷貝到的目標(biāo)矩形的位置(在畫布上)。

該方法可以用于在單個(gè)圖像文件中放入多個(gè)精靈(圖像單元)并畫出你需要的部分。

我們可以改變繪制的人物造型,來展現(xiàn)一段看似人物在走動(dòng)的動(dòng)畫。

clearRect方法可以幫助我們?cè)诋嫴忌侠L制動(dòng)畫。該方法類似于fillRect方法,但是不同的是clearRect方法會(huì)將目標(biāo)矩形透明化,并移除掉之前繪制的像素值,而不是著色。

我們知道每個(gè)精靈和每個(gè)子畫面的寬度都是 24 像素,高度都是 30 像素。下面的代碼裝載了一幅圖片并設(shè)置定時(shí)器(會(huì)重復(fù)觸發(fā)的定時(shí)器)來定時(shí)繪制下一幀。


cycle綁定用于記錄角色在動(dòng)畫圖像中的位置。每顯示一幀,我們都要將cycle加 1,并通過取余數(shù)確保cycle的值在 0~7 這個(gè)范圍內(nèi)。我們隨后使用該綁定計(jì)算精靈當(dāng)前形象在圖片中的x坐標(biāo)。

變換

但是,如果我們希望角色可以向左走而不是向右走該怎么辦?誠(chéng)然,我們可以繪制另一組精靈,但我們也可以使用另一種方式在畫布上繪圖。

我們可以調(diào)用scale方法來縮放之后繪制的任何元素。該方法接受兩個(gè)輸入?yún)?shù),第一個(gè)參數(shù)是水平縮放比例,第二個(gè)參數(shù)是豎直縮放比例。


因?yàn)檎{(diào)用了scale,因此圓形長(zhǎng)度變?yōu)樵瓉淼?3 倍,高度變?yōu)樵瓉淼囊话搿?b>scale可以調(diào)整圖像所有特征,包括線寬、預(yù)定拉伸或壓縮。如果將縮放值設(shè)置為負(fù)值,可以將圖像翻轉(zhuǎn)。由于翻轉(zhuǎn)發(fā)生在坐標(biāo)(0,0)處,這意味著也會(huì)同時(shí)反轉(zhuǎn)坐標(biāo)系的方向。當(dāng)水平縮放 –1 時(shí),在x坐標(biāo)為 100 的位置畫出的圖形會(huì)繪制在縮放之前x坐標(biāo)為 –100 的位置。

為了翻轉(zhuǎn)一張圖片,只是在drawImage之前添加cx.scale(–1,–1)是沒用的,因?yàn)檫@樣會(huì)將我們的圖片移出到畫布之外,導(dǎo)致圖片不可見。為了避免這個(gè)問題,我們還需要調(diào)整傳遞給drawImage的坐標(biāo),將繪制圖形的x坐標(biāo)改為 –50 而不是 0。另一個(gè)解決方案是在縮放時(shí)調(diào)整坐標(biāo)軸,這樣代碼就不需要知道整個(gè)畫布的縮放的改變。

除了scale方法還有一些其他方法可以影響畫布里坐標(biāo)系統(tǒng)的方法。你可以使用rotate方法旋轉(zhuǎn)繪制完的圖形,也可以使用translate方法移動(dòng)圖形。畢竟有趣但也容易引起誤解的是這些變換以棧的方式工作,也就是說每個(gè)變換都會(huì)作用于前一個(gè)變換的結(jié)果之上。

如果我們沿水平方向?qū)嫴计揭苾纱?,每次移?dòng) 10 像素,那么所有的圖形都會(huì)在右方 20 像素的位置重新繪制。如果我們先把坐標(biāo)系的原點(diǎn)移動(dòng)到(50, 50)的位置,然后旋轉(zhuǎn) 20 度(大約0.1π弧度),此次的旋轉(zhuǎn)會(huì)圍繞點(diǎn)(50,50)進(jìn)行。

但是如果我們先旋轉(zhuǎn) 20 度,然后平移原點(diǎn)到(50,50),此次的平移會(huì)發(fā)生在已經(jīng)旋轉(zhuǎn)過的坐標(biāo)系中,因此會(huì)有不同的方向。變換發(fā)生順序會(huì)影響最后的結(jié)果。

我們可以使用下面的代碼,在指定的x坐標(biāo)處豎直反轉(zhuǎn)一張圖片。

function flipHorizontally(context, around) {
  context.translate(around, 0);
  context.scale(-1, 1);
  context.translate(-around, 0);
}

我們先把y軸移動(dòng)到我們希望鏡像所在的位置,然后進(jìn)行鏡像翻轉(zhuǎn),最后把y軸移動(dòng)到被翻轉(zhuǎn)的坐標(biāo)系當(dāng)中相應(yīng)的位置。下面的圖片解釋了以上代碼是如何工作的:

上圖顯示了通過中線進(jìn)行鏡像翻轉(zhuǎn)前后的坐標(biāo)系。對(duì)三角形編號(hào)來說明每一步。如果我們?cè)?b>x坐標(biāo)為正值的位置繪制一個(gè)三角形,默認(rèn)情況下它會(huì)出現(xiàn)在圖中三角形 1 的位置。調(diào)用filpHorizontally首先做一個(gè)向右的平移,得到三角形 2。然后將其翻轉(zhuǎn)到三角形 3 的位置。這不是它的根據(jù)給定的中線翻轉(zhuǎn)之后應(yīng)該在的最終位置。第二次調(diào)用translate方法解決了這個(gè)問題。它“去除”了最初的平移的效果,并且使三角形 4 變成我們希望的效果。

我們可以沿著特征的豎直中心線翻轉(zhuǎn)整個(gè)坐標(biāo)系,這樣就可以畫出位置為(100,0)處的鏡像特征。


存儲(chǔ)與清除圖像的變換狀態(tài)

圖像變換的效果會(huì)保留下來。我們繪制出一次鏡像特征后,繪制其他特征時(shí)都會(huì)產(chǎn)生鏡像效果,這可能并不方便。

對(duì)于需要臨時(shí)轉(zhuǎn)換坐標(biāo)系統(tǒng)的函數(shù)來說,我們經(jīng)常需要保存當(dāng)前的信息,畫一些圖,變換圖像然后重新加載之前的圖像。首先,我們需要將當(dāng)前函數(shù)調(diào)用的所有圖形變換信息保存起來。接著,函數(shù)完成其工作,并添加更多的變換。最后我們恢復(fù)之前保存的變換狀態(tài)。

2D 畫布上下文的saverestore方法執(zhí)行這個(gè)變換管理。這兩個(gè)方法維護(hù)變換狀態(tài)堆棧。save方法將當(dāng)前狀態(tài)壓到堆棧中,restore方法將堆棧頂部的狀態(tài)彈出,并將該狀態(tài)作為當(dāng)前context對(duì)象的狀態(tài)。

下面示例中的branch函數(shù)首先修改變換狀態(tài),然后調(diào)用其他函數(shù)(本例中就是該函數(shù)自身)繼續(xù)在特定變換狀態(tài)中進(jìn)行繪圖。

這個(gè)方法通過畫出一條線段,并把坐標(biāo)系的中心移動(dòng)到線段的端點(diǎn),然后調(diào)用自身兩次,先向左旋轉(zhuǎn),接著向右旋轉(zhuǎn),來畫出一個(gè)類似樹一樣的圖形。每次調(diào)用都會(huì)減少所畫分支的長(zhǎng)度,當(dāng)長(zhǎng)度小于 8 的時(shí)候遞歸結(jié)束。


如果沒有調(diào)用saverestore方法,第二次遞歸調(diào)用branch將會(huì)在第一次調(diào)用的位置結(jié)束。它不會(huì)與當(dāng)前的分支相連接,而是更加靠近中心偏右第一次調(diào)用所畫出的分支。結(jié)果圖像會(huì)很有趣,但是它肯定不是一棵樹。

回到游戲

我們現(xiàn)在已經(jīng)了解了足夠多的畫布繪圖知識(shí),我們已經(jīng)可以使用基于畫布的顯示系統(tǒng)來改造前面幾章中開發(fā)的游戲了。新的界面不會(huì)再是一個(gè)個(gè)色塊,而使用drawImage來繪制游戲中元素對(duì)應(yīng)的圖片。

我們定義了一種對(duì)象類型,叫做CanvasDisplay,支持第 14 章中的DOMDisplay的相同接口,也就是setState方法與clear方法。

這個(gè)對(duì)象需要比DOMDisplay多保存一些信息。該對(duì)象不僅需要使用 DOM 元素的滾動(dòng)位置,還需要追蹤自己的視口(viewport)。視口會(huì)告訴我們目前處于哪個(gè)關(guān)卡。最后,該對(duì)象會(huì)保存一個(gè)filpPlayer屬性,確保即便玩家站立不動(dòng)時(shí),它面朝的方向也會(huì)與上次移動(dòng)所面向的方向一致。

class CanvasDisplay {
  constructor(parent, level) {
    this.canvas = document.createElement("canvas");
    this.canvas.width = Math.min(600, level.width * scale);
    this.canvas.height = Math.min(450, level.height * scale);
    parent.appendChild(this.canvas);
    this.cx = this.canvas.getContext("2d");

    this.flipPlayer = false;

    this.viewport = {
      left: 0,
      top: 0,
      width: this.canvas.width / scale,
      height: this.canvas.height / scale
    };
  }

  clear() {
    this.canvas.remove();
  }
}

setState方法首先計(jì)算一個(gè)新的視口,然后在適當(dāng)?shù)奈恢美L制游戲場(chǎng)景。

CanvasDisplay.prototype.setState = function(state) {
  this.updateViewport(state);
  this.clearDisplay(state.status);
  this.drawBackground(state.level);
  this.drawActors(state.actors);
};

DOMDisplay相反,這種顯示風(fēng)格確實(shí)必須在每次更新時(shí)重新繪制背景。 因?yàn)楫嫴忌系男螤钪皇窍袼兀栽谖覀兝L制它們之后,沒有什么好方法來移動(dòng)它們(或?qū)⑺鼈円瞥?更新畫布顯示的唯一方法,是清除它并重新繪制場(chǎng)景。 我們也可能發(fā)生了滾動(dòng),這要求背景處于不同的位置。

updateViewport方法與DOMDisplayscrollPlayerintoView方法相似。它檢查玩家是否過于接近屏幕的邊緣,并且當(dāng)這種情況發(fā)生時(shí)移動(dòng)視口。

CanvasDisplay.prototype.updateViewport = function(state) {
  let view = this.viewport, margin = view.width / 3;
  let player = state.player;
  let center = player.pos.plus(player.size.times(0.5));

  if (center.x < view.left + margin) {
    view.left = Math.max(center.x - margin, 0);
  } else if (center.x > view.left + view.width - margin) {
    view.left = Math.min(center.x + margin - view.width,
                         state.level.width - view.width);
  }
  if (center.y < view.top + margin) {
    view.top = Math.max(center.y - margin, 0);
  } else if (center.y > view.top + view.height - margin) {
    view.top = Math.min(center.y + margin - view.height,
                        state.level.height - view.height);
  }
};

對(duì)Math.maxMath.min的調(diào)用保證了視口不會(huì)顯示當(dāng)前這層之外的物體。Math.max(x,0)保證了結(jié)果數(shù)值不會(huì)小于 0。同樣地,Math.min`保證了數(shù)值保持在給定范圍內(nèi)。

在清空?qǐng)D像時(shí),我們依據(jù)游戲是獲勝(明亮的顏色)還是失敗(灰暗的顏色)來使用不同的顏色。

CanvasDisplay.prototype.clearDisplay = function(status) {
  if (status == "won") {
    this.cx.fillStyle = "rgb(68, 191, 255)";
  } else if (status == "lost") {
    this.cx.fillStyle = "rgb(44, 136, 214)";
  } else {
    this.cx.fillStyle = "rgb(52, 166, 251)";
  }
  this.cx.fillRect(0, 0,
                   this.canvas.width, this.canvas.height);
};

要畫出一個(gè)背景,我們使用來自上一節(jié)的touches方法中的相同技巧,遍歷在當(dāng)前視口中可見的所有瓦片。

let otherSprites = document.createElement("img");
otherSprites.src = "https://gitee.com/wizardforcel/eloquent-js-3e-zh/raw/master/img/sprites.png";

CanvasDisplay.prototype.drawBackground = function(level) {
  let {left, top, width, height} = this.viewport;
  let xStart = Math.floor(left);
  let xEnd = Math.ceil(left + width);
  let yStart = Math.floor(top);
  let yEnd = Math.ceil(top + height);
  for (let y = yStart; y < yEnd; y++) {
    for (let x = xStart; x < xEnd; x++) {
      let tile = level.rows[y][x];
      if (tile == "empty") continue;
      let screenX = (x - left) * scale;
      let screenY = (y - top) * scale;
      let tileX = tile == "lava" ? scale : 0;
      this.cx.drawImage(otherSprites,
                        tileX,         0, scale, scale,
                        screenX, screenY, scale, scale);
    }
  }
};

非空的瓦片是使用drawImage繪制的。otherSprites包含了描述除了玩家之外需要用到的圖片。它包含了從左到右的墻上的瓦片,火山巖瓦片以及精靈硬幣。

背景瓦片是20×20像素的,因?yàn)槲覀儗⒁玫?b>DOMDisplay中的相同比例。因此,火山巖瓦片的偏移是 20,墻面的偏移是 0。

我們不需要等待精靈圖片加載完成。調(diào)用drawImage時(shí)使用一幅并未加載完畢的圖片不會(huì)有任何效果。因?yàn)閳D片仍然在加載當(dāng)中,我們可能無法正確地畫出游戲的前幾幀。但是這不是一個(gè)嚴(yán)重的問題,因?yàn)槲覀兂掷m(xù)更新熒幕,正確的場(chǎng)景會(huì)在加載完畢之后立即出現(xiàn)。

前面展示過的走路的特征將會(huì)被用來代替玩家。繪制它的代碼需要根據(jù)玩家的當(dāng)前動(dòng)作選擇正確的動(dòng)作和方向。前 8 個(gè)子畫面包含一個(gè)走路的動(dòng)畫。當(dāng)玩家沿著地板移動(dòng)時(shí),我們根據(jù)當(dāng)前時(shí)間把他圍起來。我們希望每 60 毫秒切換一次幀,所以時(shí)間先除以 60。當(dāng)玩家站立不動(dòng)時(shí),我們畫出第九張子畫面。當(dāng)豎直方向的速度不為 0,從而被判斷為跳躍時(shí),我們使用第 10 張,也是最右邊的子畫面。

因?yàn)樽赢嬅鎸挾葹?24 像素而不是 16 像素,會(huì)稍微比玩家的對(duì)象寬,這時(shí)為了騰出腳和手的空間,該方法需要根據(jù)某個(gè)給定的值(playerXOverlap)調(diào)整x坐標(biāo)的值以及寬度值。

let playerSprites = document.createElement("img");
playerSprites.src = "https://gitee.com/wizardforcel/eloquent-js-3e-zh/raw/master/img/player.png";
const playerXOverlap = 4;

CanvasDisplay.prototype.drawPlayer = function(player, x, y,
                                              width, height){
  width += playerXOverlap * 2;
  x -= playerXOverlap;
  if (player.speed.x != 0) {
    this.flipPlayer = player.speed.x < 0;
  }

  let tile = 8;
  if (player.speed.y != 0) {
    tile = 9;
  } else if (player.speed.x != 0) {
    tile = Math.floor(Date.now() / 60) % 8;
  }

  this.cx.save();
  if (this.flipPlayer) {
    flipHorizontally(this.cx, x + width / 2);
  }
  let tileX = tile * width;
  this.cx.drawImage(playerSprites, tileX, 0, width, height,
                                   x,     y, width, height);
  this.cx.restore();
};

drawPlayer方法由drawActors方法調(diào)用,該方法負(fù)責(zé)畫出游戲中的所有角色。

CanvasDisplay.prototype.drawActors = function(actors) {
  for (let actor of actors) {
    let width = actor.size.x * scale;
    let height = actor.size.y * scale;
    let x = (actor.pos.x - this.viewport.left) * scale;
    let y = (actor.pos.y - this.viewport.top) * scale;
    if (actor.type == "player") {
      this.drawPlayer(actor, x, y, width, height);
    } else {
      let tileX = (actor.type == "coin" ? 2 : 1) * scale;
      this.cx.drawImage(otherSprites,
                        tileX, 0, width, height,
                        x,     y, width, height);
    }
  }
};

當(dāng)需要繪制一些非玩家元素時(shí),我們首先檢查它的類型,來找到與正確的子畫面的偏移值。熔巖瓷磚出現(xiàn)在偏移為 20 的子畫面,金幣的子畫面出現(xiàn)在偏移值為 40 的地方(放大了兩倍)。

當(dāng)計(jì)算角色的位置時(shí),我們需要減掉視口的位置,因?yàn)?b>(0,0)在我們的畫布坐標(biāo)系中代表著視口層面的左上角,而不是該關(guān)卡的左上角。我們也可以使用translate方法,這樣可以作用于所有元素。

這個(gè)文檔將新的顯示屏插入runGame中:


  
選擇圖像接口

所以當(dāng)你需要在瀏覽器中繪圖時(shí),你都可以選擇純粹的 HTML、SVG 或畫布。沒有唯一的最適合的且在所有動(dòng)畫中都是最好的方法。每個(gè)選擇都有它的利與弊。

單純的 HTML 的優(yōu)點(diǎn)是簡(jiǎn)單。它也可以很好地與文字集成使用。SVG 與畫布都可以允許你繪制文字,但是它們不會(huì)只通過一行代碼來幫助你放置text或者包裝它,在一個(gè)基于 HTML 的圖像中,包含文本塊更加簡(jiǎn)單。

SVG 可以被用來制造可以任意縮放而仍然清晰的圖像。與 HTML 相反,它實(shí)際上是為繪圖而設(shè)計(jì)的,因此更適合于此目的。

SVG 與 HTML 都會(huì)構(gòu)建一個(gè)新的數(shù)據(jù)結(jié)構(gòu)(DOM),它表示你的圖片。這使得在繪制元素之后對(duì)其進(jìn)行修改更為可能。如果你需要重復(fù)的修改在一張大圖片中的一小部分,來對(duì)用戶的動(dòng)作進(jìn)行響應(yīng)或者作為動(dòng)畫的一部分時(shí),在畫布里做這件事情將會(huì)極其的昂貴。DOM 也可以允許我們?cè)趫D片上的每一個(gè)元素(甚至在 SVG 畫出的圖形上)注冊(cè)鼠標(biāo)事件的處理器。在畫布里則實(shí)現(xiàn)不了。

但是畫布的基于像素的方法在需要繪制大量的微小元素時(shí)會(huì)有優(yōu)勢(shì)。它不會(huì)構(gòu)建新的數(shù)據(jù)結(jié)構(gòu)而是僅僅重復(fù)的在同一個(gè)像素上繪制,這使得畫布在每個(gè)圖形上擁有更低的消耗。

有一些效果,像在逐像素的渲染一個(gè)場(chǎng)景(比如,使用光線追蹤)或者使用 javaScript 對(duì)一張圖片進(jìn)行后加工(虛化或者扭曲),只能通過基于像素的技術(shù)來進(jìn)行真實(shí)的處理。在某些情況下,你可能想要將這些技術(shù)整合起來使用。比如,你可能用 SVG 或者畫布畫出一個(gè)圖形,但是通過將一個(gè) HTML 元素放在圖片的頂端來展示像素信息。

對(duì)于一些要求低的程序來說,選擇哪個(gè)接口并沒有什么太大的區(qū)別。因?yàn)椴恍枰L制文字,處理鼠標(biāo)交互或者處理大量的元素。我們?cè)诒菊聻橛螒驑?gòu)建的顯示屏,可以通過使用三種圖像技術(shù)中的任意一種來實(shí)現(xiàn)。

本章小結(jié)

在本章中,我們討論了在瀏覽器中繪制圖形的技術(shù),重點(diǎn)關(guān)注了元素。

一個(gè)canvas節(jié)點(diǎn)代表了我們的程序可以繪制在文檔中的一片區(qū)域。這個(gè)繪圖動(dòng)作是通過一個(gè)由getContext方法創(chuàng)建的繪圖上下文對(duì)象完成的。

2D 繪圖接口允許我們填充或者拉伸各種各樣的圖形。這個(gè)上下文的fillStyle屬性決定了圖形的填充方式。strokeStylelineWidth屬性用來控制線條的繪制方式。

矩形與文字可以通過使用一個(gè)簡(jiǎn)單的方法調(diào)用來繪制。采用fillRectstrokeRect方法繪制矩形,同時(shí)采用fillTextstrokeText方法繪制文字。要?jiǎng)?chuàng)建一個(gè)自定義的圖形,我們必須首先建立一個(gè)路徑。

調(diào)用beginPath會(huì)創(chuàng)建一個(gè)新的路徑。很多其他的方法可以向當(dāng)前的路徑添加線條和曲線。比如,lineTo方法可以添加一條直線。當(dāng)一條路徑畫完時(shí),它可以被fill方法填充或者被stroke方法勾勒輪廓。

從一張圖片或者另一個(gè)畫布上移動(dòng)像素到我們的畫布上可以用drawImage方法實(shí)現(xiàn)。默認(rèn)情況下,這個(gè)方法繪制了整個(gè)原圖像,但是通過給它更多的參數(shù),你可以拷貝一張圖片的某一個(gè)特定的區(qū)域。我們?cè)谟螒蛑惺褂昧诉@項(xiàng)技術(shù),從包括許多動(dòng)作的圖像中拷貝出游戲角色的單個(gè)獨(dú)立動(dòng)作。

圖形變換允許你向多個(gè)方向繪制圖片。2D 繪制上下文擁有一個(gè)當(dāng)前的可以通過translate、scalerotate進(jìn)行變換。這些會(huì)影響所有的后續(xù)的繪制操作。一個(gè)變換的狀態(tài)可以通過save方法來保存,通過restore方法來恢復(fù)。

在一個(gè)畫布上展示動(dòng)畫時(shí),clearRect方法可以用來在重繪之前清除畫布的某一部分。

習(xí)題 形狀

編寫一個(gè)程序,在畫布上畫出下面的圖形。

一個(gè)不規(guī)則四邊形(一個(gè)在一邊比較長(zhǎng)的矩形)

一個(gè)紅色的鉆石(一個(gè)矩形旋轉(zhuǎn)45度角)

一個(gè)鋸齒線

一個(gè)由 100 條直線線段構(gòu)成的螺旋

一個(gè)黃色的星星

當(dāng)繪制最后兩個(gè)圖形時(shí),你可以參考第 14 章中的Math.cosMath.sin的解釋,它描述了如何使用這兩個(gè)函數(shù)獲得圓上的坐標(biāo)。

建議你為每一個(gè)圖形創(chuàng)建一個(gè)方法,傳入坐標(biāo)信息,以及其他的一些參數(shù),比如大小或者點(diǎn)的數(shù)量。另一種方法,可以在你的代碼中硬編碼,會(huì)使得你的代碼變得難以閱讀和修改。


餅狀圖

在本章的前部分,我們看到一個(gè)繪制餅狀圖的樣例程序。修改這個(gè)程序,使得每個(gè)部分的名字可以被顯示在相應(yīng)的切片旁邊。試著找到一個(gè)合適的方法來自動(dòng)放置這些文字,同時(shí)也可以適用于其他數(shù)據(jù)。你可以假設(shè)分類大到足以為標(biāo)簽留出空間。

你可能還會(huì)需要Math.sinMath.cos方法,像第 14 章描述的一樣。


彈力球

使用在第 14 章和第 16 章出現(xiàn)的requestAnimationFrame方法畫出一個(gè)裝有彈力球的盒子。這個(gè)球勻速運(yùn)動(dòng)并且當(dāng)撞到盒子的邊緣的時(shí)候反彈。


預(yù)處理鏡像

當(dāng)進(jìn)行圖形變換時(shí),繪制位圖圖像會(huì)很慢。每個(gè)像素的位置和大小都必須進(jìn)行變換,盡管將來瀏覽器可能會(huì)更加聰明,但這會(huì)導(dǎo)致繪制位圖所需的時(shí)間顯著增加。

在一個(gè)像我們這樣的只繪制一個(gè)簡(jiǎn)單的子畫面圖像變換的游戲中,這個(gè)不是問題。但是如果我們需要繪制成百上千的角色或者爆炸產(chǎn)生的旋轉(zhuǎn)粒子時(shí),這將會(huì)成為一個(gè)問題。

思考一種方法來允許我們不需要加載更多的圖片文件就可以畫出一個(gè)倒置的角色,并且不需要在每一幀調(diào)用drawImage方法。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/105046.html

相關(guān)文章

  • JavaScript 編程精解 中文三版 十九、項(xiàng)目:像素藝術(shù)編輯器

    摘要:相反,當(dāng)響應(yīng)指針事件時(shí),它會(huì)調(diào)用創(chuàng)建它的代碼提供的回調(diào)函數(shù),該函數(shù)將處理應(yīng)用的特定部分?;卣{(diào)函數(shù)可能會(huì)返回另一個(gè)回調(diào)函數(shù),以便在按下按鈕并且將指針移動(dòng)到另一個(gè)像素時(shí)得到通知。它們?yōu)榻M件構(gòu)造器的數(shù)組而提供。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Project: A Pixel Art Editor 譯者:飛龍 協(xié)議:CC BY-NC-SA 4...

    Meils 評(píng)論0 收藏0
  • JavaScript 編程精解 中文三版 十五、處理事件

    摘要:事件與節(jié)點(diǎn)每個(gè)瀏覽器事件處理器被注冊(cè)在上下文中。事件對(duì)象雖然目前為止我們忽略了它,事件處理器函數(shù)作為對(duì)象傳遞事件對(duì)象。若事件處理器不希望執(zhí)行默認(rèn)行為通常是因?yàn)橐呀?jīng)處理了該事件,會(huì)調(diào)用事件對(duì)象的方法。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Handling Events 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分...

    Clect 評(píng)論0 收藏0
  • JavaScript 編程精解 中文三版 零、前言

    摘要:來源編程精解中文第三版翻譯項(xiàng)目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版,這是一本關(guān)于指導(dǎo)電腦的書。在可控的范圍內(nèi)編寫程序是編程過程中首要解決的問題。我們可以用中文來描述這些指令將數(shù)字存儲(chǔ)在內(nèi)存地址中的位置。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Introduction 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地...

    sanyang 評(píng)論0 收藏0
  • JavaScript 編程精解 中文三版 二、程序結(jié)構(gòu)

    摘要:為了運(yùn)行包裹的程序,可以將這些值應(yīng)用于它們。在瀏覽器中,輸出出現(xiàn)在控制臺(tái)中。在英文版頁(yè)面上運(yùn)行示例或自己的代碼時(shí),會(huì)在示例之后顯示輸出,而不是在瀏覽器的控制臺(tái)中顯示。這被稱為條件執(zhí)行。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Program Structure 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《J...

    ThinkSNS 評(píng)論0 收藏0
  • JavaScript 編程精解 中文三版 十三、瀏覽器中的 JavaScript

    摘要:在本例中,使用屬性指定鏈接的目標(biāo),其中表示超文本鏈接。您應(yīng)該認(rèn)為和元數(shù)據(jù)隱式出現(xiàn)在示例中,即使它們沒有實(shí)際顯示在文本中。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:JavaScript and the Browser 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)》 ...

    zhiwei 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<