<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                企業??AI智能體構建引擎,智能編排和調試,一鍵部署,支持知識庫和私有化部署方案 廣告
                ## 十七、在畫布上繪圖 > 原文:[Drawing on Canvas](https://eloquentjavascript.net/17_canvas.html) > > 譯者:[飛龍](https://github.com/wizardforcel) > > 協議:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) > > 自豪地采用[谷歌翻譯](https://translate.google.cn/) > > 部分參考了[《JavaScript 編程精解(第 2 版)》](https://book.douban.com/subject/26707144/) > 繪圖就是欺騙。 > > M.C. Escher,由 Bruno Ernst 在《The Magic Mirror of M.C. Escher》中引用 ![](https://img.kancloud.cn/8d/d3/8dd3d2f4b0c26b4ec4afda10ec22d509_490x310.jpg) 瀏覽器為我們提供了多種繪圖方式。最簡單的方式是用樣式來規定普通 DOM 對象的位置和顏色。就像在上一章中那個游戲展示的,我們可以使用這種方式實現很多功能。我們可以為節點添加半透明的背景圖片,來獲得我們希望的節點外觀。我們也可以使用`transform`樣式來旋轉或傾斜節點。 但是,在一些場景中,使用 DOM 并不符合我們的設計初衷。比如我們很難使用普通的 HTML 元素畫出任意兩點之間的線段這類圖形。 這里有兩種解決辦法。第一種方法基于 DOM,但使用可縮放矢量圖形(SVG,Scalable Vector Graphics)代替 HTML。我們可以將 SVG 看成文檔標記方言,專用于描述圖形而非文字。你可以在 HTML 文檔中嵌入 SVG,還可以在`<img>`標簽中引用它。 我們將第二種方法稱為畫布(canvas)。畫布是一個能夠封裝圖片的 DOM 元素。它提供了在空白的`html`節點上繪制圖形的編程接口。SVG 與畫布的最主要區別在于 SVG 保存了對于圖像的基本信息的描述,我們可以隨時移動或修改圖像。 另外,畫布在繪制圖像的同時會把圖像轉換成像素(在柵格中的具有顏色的點)并且不會保存這些像素表示的內容。唯一的移動圖形的方法就是清空畫布(或者圍繞著圖形的部分畫布)并在新的位置重畫圖形。 ## SVG 本書不會深入研究 SVG 的細節,但是我會簡單地解釋其工作原理。在本章的結尾,我會再次來討論,對于某個具體的應用來說,我們應該如何權衡利弊選擇一種繪圖方式。 這是一個帶有簡單的 SVG 圖片的 HTML 文檔。 ```html <p>Normal HTML here.</p> <svg xmlns="http://www.w3.org/2000/svg"> <circle r="50" cx="50" cy="50" fill="red"/> <rect x="120" y="5" width="90" height="90" stroke="blue" fill="none"/> </svg> ``` `xmlns`屬性把一個元素(以及他的子元素)切換到一個不同的 XML 命名空間。這個由`url`定義的命名空間,規定了我們當前使用的語言。在 HTML 中不存在`<circle>`與`<rect>`標簽,但這些標簽在 SVG 中是有意義的,你可以通過這些標簽的屬性來繪制圖像并指定樣式與位置。 和 HTML 標簽一樣,這些標簽會創建 DOM 元素,腳本可以和它們交互。例如,下面的代碼可以把`<circle>`元素的顏色替換為青色。 ```html let circle = document.querySelector("circle"); circle.setAttribute("fill", "cyan"); ``` ## `canvas`元素 我們可以在`<canvas>`元素中繪制畫布圖形。你可以通過設置`width`與`height`屬性來確定畫布尺寸(單位為像素)。 新的畫布是空的,意味著它是完全透明的,看起來就像文檔中的空白區域一樣。 `<canvas>`標簽允許多種不同風格的繪圖。要獲取真正的繪圖接口,首先我們要創建一個能夠提供繪圖接口的方法的上下文(context)。目前有兩種得到廣泛支持的繪圖接口:用于繪制二維圖形的`"2d"`與通過openGL接口繪制三維圖形的`"webgl"`。 本書只討論二維圖形,而不討論 WebGL。但是如果你對三維圖形感興趣,我強烈建議大家自行深入研究 WebGL。它提供了非常簡單的現代圖形硬件接口,同時你也可以使用 JavaScript 來高效地渲染非常復雜的場景。 您可以用`getContext`方法在`<canvas>` DOM 元素上創建一個上下文。 ```html <p>Before canvas.</p> <canvas width="120" height="60"></canvas> <p>After canvas.</p> <script> let canvas = document.querySelector("canvas"); let context = canvas.getContext("2d"); context.fillStyle = "red"; context.fillRect(10, 10, 100, 50); </script> ``` 在創建完`context`對象之后,作為示例,我們畫出一個紅色矩形。該矩形寬 100 像素,高 50 像素,它的左上點坐標為(10,10)。 與 HTML(或者 SVG)相同,畫布使用的坐標系統將`(0,0)`放置在左上角,并且`y`軸向下增長。所以`(10,10)`是相對于左上角向下并向右各偏移 10 像素的位置。 ## 直線和平面 我們可以使用畫布接口填充圖形,也就是賦予某個區域一個固定的填充顏色或填充模式。我們也可以描邊,也就是沿著圖形的邊沿畫出線段。SVG 也使用了相同的技術。 `fillRect`方法可以填充一個矩形。他的輸入為矩形框左上角的第一個`x`和`y`坐標,然后是它的寬和高。相似地,`strokeRect`方法可以畫出一個矩形的外框。 兩個方法都不需要其他任何參數。填充的顏色以及輪廓的粗細等等都不能由方法的參數決定(像你的合理預期一樣),而是由上下文對象的屬性決定。 設置`fillStyle`參數控制圖形的填充方式。我們可以將其設置為描述顏色的字符串,使用 CSS 所用的顏色表示法。 `strokeStyle`屬性的作用很相似,但是它用于規定輪廓線的顏色。線條的寬度由`lineWidth`屬性決定。`lineWidth`的值都為正值。 ```html <canvas></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); cx.strokeStyle = "blue"; cx.strokeRect(5, 5, 50, 50); cx.lineWidth = 5; cx.strokeRect(135, 5, 50, 50); </script> ``` 當沒有設置`width`或者`height`參數時,正如示例一樣,畫布元素的默認寬度為 300 像素,默認高度為 150 像素。 ## 路徑 路徑是線段的序列。2D `canvas`接口使用一種奇特的方式來描述這樣的路徑。路徑的繪制都是間接完成的。我們無法將路徑保存為可以后續修改并傳遞的值。如果你想修改路徑,必須要調用多個方法來描述他的形狀。 ```html <canvas></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); cx.beginPath(); for (let y = 10; y < 100; y += 10) { cx.moveTo(10, y); cx.lineTo(90, y); } cx.stroke(); </script> ``` 本例創建了一個包含很多水平線段的路徑,然后用`stroke`方法勾勒輪廓。每個線段都是由`lineTo`以當前位置為路徑起點繪制的。除非調用了`moveTo`,否則這個位置通常是上一個線段的終點位置。如果調用了`moveTo`,下一條線段會從`moveTo`指定的位置開始。 當使用`fill`方法填充一個路徑時,我們需要分別填充這些圖形。一個路徑可以包含多個圖形,每個`moveTo`都會創建一個新的圖形。但是在填充之前我們需要封閉路徑(路徑的起始節點與終止節點必須是同一個點)。如果一個路徑尚未封閉,會出現一條從終點到起點的線段,然后才會填充整個封閉圖形。 ```html <canvas></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); cx.beginPath(); cx.moveTo(50, 10); cx.lineTo(10, 70); cx.lineTo(90, 70); cx.fill(); </script> ``` 本例畫出了一個被填充的三角形。注意只顯示地畫出了三角形的兩條邊。第三條從右下角回到上頂點的邊是沒有顯示地畫出,因而在勾勒路徑的時候也不會存在。 你也可以使用`closePath`方法顯示地通過增加一條回到路徑起始節點的線段來封閉一個路徑。這條線段在勾勒路徑的時候將被顯示地畫出。 ## 曲線 路徑也可能會包含曲線。繪制曲線更加復雜。 `quadraticCurveTo`方法繪制到某一個點的曲線。為了確定一條線段的曲率,需要設定一個控制點以及一個目標點。設想這個控制點會吸引這條線段,使其成為曲線。線段不會穿過控制點。但是,它起點與終點的方向會與兩個點到控制點的方向平行。見下例: ```html <canvas></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); cx.beginPath(); cx.moveTo(10, 90); // control=(60,10) goal=(90,90) cx.quadraticCurveTo(60, 10, 90, 90); cx.lineTo(60, 10); cx.closePath(); cx.stroke(); </script> ``` 我們從左到右繪制一個二次曲線,曲線的控制點坐標為`(60,10)`,然后畫出兩條穿過控制點并且回到線段起點的線段。繪制的結果類似一個星際迷航的圖章。你可以觀察到控制點的效果:從下端的角落里發出的線段朝向控制點并向他們的目標點彎曲。 `bezierCurve`(貝塞爾曲線)方法可以繪制一種類似的曲線。不同的是貝塞爾曲線需要兩個控制點而不是一個,線段的每一個端點都需要一個控制點。下面是描述貝塞爾曲線的簡單示例。 ```html <canvas></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); cx.beginPath(); cx.moveTo(10, 90); // control1=(10,10) control2=(90,10) goal=(50,90) cx.bezierCurveTo(10, 10, 90, 10, 50, 90); cx.lineTo(90, 10); cx.lineTo(10, 10); cx.closePath(); cx.stroke(); </script> ``` 兩個控制點規定了曲線兩個端點的方向。兩個控制點相對兩個端點的距離越遠,曲線就會越向這個方向凸出。 由于我們沒有明確的方法,來找出我們希望繪制圖形所對應的控制點,所以這種曲線還是很難操控。有時候你可以通過計算得到他們,而有時候你只能通過不斷的嘗試來找到合適的值。 `arc`方法是一種沿著圓的邊緣繪制曲線的方法。 它需要弧的中心的一對坐標,半徑,然后是起始和終止角度。 我們可以使用最后兩個參數畫出部分圓。角度是通過弧度來測量的,而不是度數。這意味著一個完整的圓擁有`2π`的弧度,或者`2*Math.PI`(大約為 6.28)的弧度。弧度從圓心右邊的點開始并以順時針的方向計數。你可以以 0 起始并以一個比`2π`大的數值(比如 7)作為終止值,畫出一個完整的圓。 ```html <canvas></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); cx.beginPath(); // center=(50,50) radius=40 angle=0 to 7 cx.arc(50, 50, 40, 0, 7); // center=(150,50) radius=40 angle=0 to ?π cx.arc(150, 50, 40, 0, 0.5 * Math.PI); cx.stroke(); </script> ``` 上面這段代碼繪制出的圖形包含了一條從完整圓(第一次調用`arc`)的右側到四分之一圓(第二次調用`arc`)的左側的直線。`arc`與其他繪制路徑的方法一樣,會自動連接到上一個路徑上。你可以調用`moveTo`或者開啟一個新的路徑來避免這種情況。 ## 繪制餅狀圖 設想你剛剛從 EconomiCorp 獲得了一份工作,并且你的第一個任務是畫出一個描述其用戶滿意度調查結果的餅狀圖。`results`綁定包含了一個表示調查結果的對象的數組。 ```js 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"} ]; ``` 要想畫出一個餅狀圖,我們需要畫出很多個餅狀圖的切片,每個切片由一個圓弧與兩條到圓心的線段組成。我們可以通過把一個整圓(`2π`)分割成以調查結果數量為單位的若干份,然后乘以做出相應選擇的用戶的個數來計算每個圓弧的角度。 ```html <canvas width="200" height="200"></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); let total = results .reduce((sum, {count}) => sum + count, 0); // Start at the top let currentAngle = -0.5 * Math.PI; for (let result of results) { let sliceAngle = (result.count / total) * 2 * Math.PI; cx.beginPath(); // center=100,100, radius=100 // from current angle, clockwise by slice's angle cx.arc(100, 100, 100, currentAngle, currentAngle + sliceAngle); currentAngle += sliceAngle; cx.lineTo(100, 100); cx.fillStyle = result.color; cx.fill(); } </script> ``` 但表格并沒有告訴我們切片代表的含義,它毫無用處。因此我們需要將文字畫在畫布上。 ## 文本 2D 畫布的`context`對象提供了`fillText`方法和`strokeText`方法。第二個方法可以用于繪制字母輪廓,但通常情況下我們需要的是`fillText`方法。該方法使用當前的`fillColor`來填充特定文字的輪廓。 ```html <canvas></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); cx.font = "28px Georgia"; cx.fillStyle = "fuchsia"; cx.fillText("I can draw text, too!", 10, 50); </script> ``` 你可以通過`font`屬性來設定文字的大小,樣式和字體。本例給出了一個字體的大小和字體族名稱。也可以添加`italic`或者`bold`來選擇樣式。 傳遞給`fillText`和`strokeText`的后兩個參數用于指定繪制文字的位置。默認情況下,這個位置指定了文字的字符基線(`baseline`)的起始位置,我們可以將其假想為字符所站立的位置,基線不考慮`j`或`p`字母中那些向下突出的部分。你可以設置`textAlign`屬性(`end`或`center`)來改變起始點的水平位置,也可以設置`textBaseline`屬性(`top`、`middle`或`bottom`)來設置基線的豎直位置。 在本章末尾的練習中,我們會回顧餅狀圖,并解決給餅狀圖分片標注的問題。 ## 圖像 計算機圖形學領域經常將矢量圖形和位圖圖形分開來討論。本章一直在討論第一種圖形,即通過對圖形的邏輯描述來繪圖。而位圖則相反,不需要設置實際圖形,而是通過處理像素數據來繪制圖像(光柵化的著色點)。 我們可以使用`drawImage`方法在畫布上繪制像素值。此處的像素數值可以來自`<img>`元素,或者來自其他的畫布。下例創建了一個獨立的`<img>`元素,并且加載了一張圖像文件。但我們無法馬上使用該圖片進行繪制,因為瀏覽器可能還沒有完成圖片的獲取操作。為了處理這個問題,我們在圖像元素上注冊一個`"load"`事件處理程序并且在圖片加載完之后開始繪制。 ```html <canvas></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); let img = document.createElement("img"); img.src = "img/hat.png"; img.addEventListener("load", () => { for (let x = 10; x < 200; x += 30) { cx.drawImage(img, x, 10); } }); </script> ``` 默認情況下,`drawImage`會根據原圖的尺寸繪制圖像。你也可以增加兩個參數來設置不同的寬度和高度。 如果我們向`drawImage`函數傳入 9 個參數,我們可以用其繪制出一張圖片的某一部分。第二個到第五個參數表示需要拷貝的源圖片中的矩形區域(`x`,`y`坐標,寬度和高度),同時第六個到第九個參數給出了需要拷貝到的目標矩形的位置(在畫布上)。 ![](https://box.kancloud.cn/2015-10-31_563439aa15f50.png) 該方法可以用于在單個圖像文件中放入多個精靈(圖像單元)并畫出你需要的部分。 我們可以改變繪制的人物造型,來展現一段看似人物在走動的動畫。 `clearRect`方法可以幫助我們在畫布上繪制動畫。該方法類似于`fillRect`方法,但是不同的是`clearRect`方法會將目標矩形透明化,并移除掉之前繪制的像素值,而不是著色。 我們知道每個精靈和每個子畫面的寬度都是 24 像素,高度都是 30 像素。下面的代碼裝載了一幅圖片并設置定時器(會重復觸發的定時器)來定時繪制下一幀。 ```html <canvas></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); let img = document.createElement("img"); img.src = "img/player.png"; let spriteW = 24, spriteH = 30; img.addEventListener("load", () => { let cycle = 0; setInterval(() => { cx.clearRect(0, 0, spriteW, spriteH); cx.drawImage(img, // source rectangle cycle * spriteW, 0, spriteW, spriteH, // destination rectangle 0, 0, spriteW, spriteH); cycle = (cycle + 1) % 8; }, 120); }); </script> ``` `cycle`綁定用于記錄角色在動畫圖像中的位置。每顯示一幀,我們都要將`cycle`加 1,并通過取余數確保`cycle`的值在 0~7 這個范圍內。我們隨后使用該綁定計算精靈當前形象在圖片中的`x`坐標。 ## 變換 但是,如果我們希望角色可以向左走而不是向右走該怎么辦?誠然,我們可以繪制另一組精靈,但我們也可以使用另一種方式在畫布上繪圖。 我們可以調用`scale`方法來縮放之后繪制的任何元素。該方法接受兩個輸入參數,第一個參數是水平縮放比例,第二個參數是豎直縮放比例。 ```html <canvas></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); cx.scale(3, .5); cx.beginPath(); cx.arc(50, 50, 40, 0, 7); cx.lineWidth = 3; cx.stroke(); </script> ``` 因為調用了`scale`,因此圓形長度變為原來的 3 倍,高度變為原來的一半。`scale`可以調整圖像所有特征,包括線寬、預定拉伸或壓縮。如果將縮放值設置為負值,可以將圖像翻轉。由于翻轉發生在坐標`(0,0)`處,這意味著也會同時反轉坐標系的方向。當水平縮放 –1 時,在`x`坐標為 100 的位置畫出的圖形會繪制在縮放之前`x`坐標為 –100 的位置。 為了翻轉一張圖片,只是在`drawImage`之前添加`cx.scale(–1,–1)`是沒用的,因為這樣會將我們的圖片移出到畫布之外,導致圖片不可見。為了避免這個問題,我們還需要調整傳遞給`drawImage`的坐標,將繪制圖形的`x`坐標改為 –50 而不是 0。另一個解決方案是在縮放時調整坐標軸,這樣代碼就不需要知道整個畫布的縮放的改變。 除了`scale`方法還有一些其他方法可以影響畫布里坐標系統的方法。你可以使用`rotate`方法旋轉繪制完的圖形,也可以使用`translate`方法移動圖形。畢竟有趣但也容易引起誤解的是這些變換以棧的方式工作,也就是說每個變換都會作用于前一個變換的結果之上。 如果我們沿水平方向將畫布平移兩次,每次移動 10 像素,那么所有的圖形都會在右方 20 像素的位置重新繪制。如果我們先把坐標系的原點移動到`(50, 50)`的位置,然后旋轉 20 度(大約`0.1π`弧度),此次的旋轉會圍繞點`(50,50)`進行。 ![](https://img.kancloud.cn/d6/01/d6012b464b84dff54644a411971b0e8e.svg) 但是如果我們先旋轉 20 度,然后平移原點到`(50,50)`,此次的平移會發生在已經旋轉過的坐標系中,因此會有不同的方向。變換發生順序會影響最后的結果。 我們可以使用下面的代碼,在指定的`x`坐標處豎直反轉一張圖片。 ```html function flipHorizontally(context, around) { context.translate(around, 0); context.scale(-1, 1); context.translate(-around, 0); } ``` 我們先把`y`軸移動到我們希望鏡像所在的位置,然后進行鏡像翻轉,最后把`y`軸移動到被翻轉的坐標系當中相應的位置。下面的圖片解釋了以上代碼是如何工作的: ![](https://img.kancloud.cn/03/3a/033a6c57bc7236b529efb50cec1c0d71.svg) 上圖顯示了通過中線進行鏡像翻轉前后的坐標系。對三角形編號來說明每一步。如果我們在`x`坐標為正值的位置繪制一個三角形,默認情況下它會出現在圖中三角形 1 的位置。調用`filpHorizontally`首先做一個向右的平移,得到三角形 2。然后將其翻轉到三角形 3 的位置。這不是它的根據給定的中線翻轉之后應該在的最終位置。第二次調用`translate`方法解決了這個問題。它“去除”了最初的平移的效果,并且使三角形 4 變成我們希望的效果。 我們可以沿著特征的豎直中心線翻轉整個坐標系,這樣就可以畫出位置為`(100,0)`處的鏡像特征。 ```html <canvas></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); let img = document.createElement("img"); img.src = "img/player.png"; let spriteW = 24, spriteH = 30; img.addEventListener("load", () => { flipHorizontally(cx, 100 + spriteW / 2); cx.drawImage(img, 0, 0, spriteW, spriteH, 100, 0, spriteW, spriteH); }); </script> ``` ## 存儲與清除圖像的變換狀態 圖像變換的效果會保留下來。我們繪制出一次鏡像特征后,繪制其他特征時都會產生鏡像效果,這可能并不方便。 對于需要臨時轉換坐標系統的函數來說,我們經常需要保存當前的信息,畫一些圖,變換圖像然后重新加載之前的圖像。首先,我們需要將當前函數調用的所有圖形變換信息保存起來。接著,函數完成其工作,并添加更多的變換。最后我們恢復之前保存的變換狀態。 2D 畫布上下文的`save`與`restore`方法執行這個變換管理。這兩個方法維護變換狀態堆棧。`save`方法將當前狀態壓到堆棧中,`restore`方法將堆棧頂部的狀態彈出,并將該狀態作為當前`context`對象的狀態。 下面示例中的`branch`函數首先修改變換狀態,然后調用其他函數(本例中就是該函數自身)繼續在特定變換狀態中進行繪圖。 這個方法通過畫出一條線段,并把坐標系的中心移動到線段的端點,然后調用自身兩次,先向左旋轉,接著向右旋轉,來畫出一個類似樹一樣的圖形。每次調用都會減少所畫分支的長度,當長度小于 8 的時候遞歸結束。 ```html <canvas width="600" height="300"></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); function branch(length, angle, scale) { cx.fillRect(0, 0, 1, length); if (length < 8) return; cx.save(); cx.translate(0, length); cx.rotate(-angle); branch(length * scale, angle, scale); cx.rotate(2 * angle); branch(length * scale, angle, scale); cx.restore(); } cx.translate(300, 0); branch(60, 0.5, 0.8); </script> ``` 如果沒有調用`save`與`restore`方法,第二次遞歸調用`branch`將會在第一次調用的位置結束。它不會與當前的分支相連接,而是更加靠近中心偏右第一次調用所畫出的分支。結果圖像會很有趣,但是它肯定不是一棵樹。 ## 回到游戲 我們現在已經了解了足夠多的畫布繪圖知識,我們已經可以使用基于畫布的顯示系統來改造前面幾章中開發的游戲了。新的界面不會再是一個個色塊,而使用`drawImage`來繪制游戲中元素對應的圖片。 我們定義了一種對象類型,叫做`CanvasDisplay`,支持第 14 章中的`DOMDisplay`的相同接口,也就是`setState`方法與`clear`方法。 這個對象需要比`DOMDisplay`多保存一些信息。該對象不僅需要使用 DOM 元素的滾動位置,還需要追蹤自己的視口(viewport)。視口會告訴我們目前處于哪個關卡。最后,該對象會保存一個`filpPlayer`屬性,確保即便玩家站立不動時,它面朝的方向也會與上次移動所面向的方向一致。 ```js 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`方法首先計算一個新的視口,然后在適當的位置繪制游戲場景。 ```js CanvasDisplay.prototype.setState = function(state) { this.updateViewport(state); this.clearDisplay(state.status); this.drawBackground(state.level); this.drawActors(state.actors); }; ``` 與`DOMDisplay`相反,這種顯示風格確實必須在每次更新時重新繪制背景。 因為畫布上的形狀只是像素,所以在我們繪制它們之后,沒有什么好方法來移動它們(或將它們移除)。 更新畫布顯示的唯一方法,是清除它并重新繪制場景。 我們也可能發生了滾動,這要求背景處于不同的位置。 `updateViewport`方法與`DOMDisplay`的`scrollPlayerintoView`方法相似。它檢查玩家是否過于接近屏幕的邊緣,并且當這種情況發生時移動視口。 ```js 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); } }; ``` 對`Math.max`和`Math.min`的調用保證了視口不會顯示當前這層之外的物體。`Math.max(x,0)保證了結果數值不會小于 0。同樣地,`Math.min`保證了數值保持在給定范圍內。 在清空圖像時,我們依據游戲是獲勝(明亮的顏色)還是失敗(灰暗的顏色)來使用不同的顏色。 ```js 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); }; ``` 要畫出一個背景,我們使用來自上一節的`touches`方法中的相同技巧,遍歷在當前視口中可見的所有瓦片。 ```js let otherSprites = document.createElement("img"); otherSprites.src = "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`包含了描述除了玩家之外需要用到的圖片。它包含了從左到右的墻上的瓦片,火山巖瓦片以及精靈硬幣。 ![](https://box.kancloud.cn/2015-10-31_563439aa3e087.png) 背景瓦片是`20×20`像素的,因為我們將要用到`DOMDisplay`中的相同比例。因此,火山巖瓦片的偏移是 20,墻面的偏移是 0。 我們不需要等待精靈圖片加載完成。調用`drawImage`時使用一幅并未加載完畢的圖片不會有任何效果。因為圖片仍然在加載當中,我們可能無法正確地畫出游戲的前幾幀。但是這不是一個嚴重的問題,因為我們持續更新熒幕,正確的場景會在加載完畢之后立即出現。 前面展示過的走路的特征將會被用來代替玩家。繪制它的代碼需要根據玩家的當前動作選擇正確的動作和方向。前 8 個子畫面包含一個走路的動畫。當玩家沿著地板移動時,我們根據當前時間把他圍起來。我們希望每 60 毫秒切換一次幀,所以時間先除以 60。當玩家站立不動時,我們畫出第九張子畫面。當豎直方向的速度不為 0,從而被判斷為跳躍時,我們使用第 10 張,也是最右邊的子畫面。 因為子畫面寬度為 24 像素而不是 16 像素,會稍微比玩家的對象寬,這時為了騰出腳和手的空間,該方法需要根據某個給定的值(`playerXOverlap`)調整`x`坐標的值以及寬度值。 ```js let playerSprites = document.createElement("img"); playerSprites.src = "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`方法調用,該方法負責畫出游戲中的所有角色。 ```js 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); } } }; ``` 當需要繪制一些非玩家元素時,我們首先檢查它的類型,來找到與正確的子畫面的偏移值。熔巖瓷磚出現在偏移為 20 的子畫面,金幣的子畫面出現在偏移值為 40 的地方(放大了兩倍)。 當計算角色的位置時,我們需要減掉視口的位置,因為`(0,0)`在我們的畫布坐標系中代表著視口層面的左上角,而不是該關卡的左上角。我們也可以使用`translate`方法,這樣可以作用于所有元素。 這個文檔將新的顯示屏插入`runGame`中: ```html <body> <script> runGame(GAME_LEVELS, CanvasDisplay); </script> </body> ``` ## 選擇圖像接口 所以當你需要在瀏覽器中繪圖時,你都可以選擇純粹的 HTML、SVG 或畫布。沒有唯一的最適合的且在所有動畫中都是最好的方法。每個選擇都有它的利與弊。 單純的 HTML 的優點是簡單。它也可以很好地與文字集成使用。SVG 與畫布都可以允許你繪制文字,但是它們不會只通過一行代碼來幫助你放置`text`或者包裝它,在一個基于 HTML 的圖像中,包含文本塊更加簡單。 SVG 可以被用來制造可以任意縮放而仍然清晰的圖像。與 HTML 相反,它實際上是為繪圖而設計的,因此更適合于此目的。 SVG 與 HTML 都會構建一個新的數據結構(DOM),它表示你的圖片。這使得在繪制元素之后對其進行修改更為可能。如果你需要重復的修改在一張大圖片中的一小部分,來對用戶的動作進行響應或者作為動畫的一部分時,在畫布里做這件事情將會極其的昂貴。DOM 也可以允許我們在圖片上的每一個元素(甚至在 SVG 畫出的圖形上)注冊鼠標事件的處理器。在畫布里則實現不了。 但是畫布的基于像素的方法在需要繪制大量的微小元素時會有優勢。它不會構建新的數據結構而是僅僅重復的在同一個像素上繪制,這使得畫布在每個圖形上擁有更低的消耗。 有一些效果,像在逐像素的渲染一個場景(比如,使用光線追蹤)或者使用 javaScript 對一張圖片進行后加工(虛化或者扭曲),只能通過基于像素的技術來進行真實的處理。在某些情況下,你可能想要將這些技術整合起來使用。比如,你可能用 SVG 或者畫布畫出一個圖形,但是通過將一個 HTML 元素放在圖片的頂端來展示像素信息。 對于一些要求低的程序來說,選擇哪個接口并沒有什么太大的區別。因為不需要繪制文字,處理鼠標交互或者處理大量的元素。我們在本章為游戲構建的顯示屏,可以通過使用三種圖像技術中的任意一種來實現。 ## 本章小結 在本章中,我們討論了在瀏覽器中繪制圖形的技術,重點關注了`<canvas>`元素。 一個`canvas`節點代表了我們的程序可以繪制在文檔中的一片區域。這個繪圖動作是通過一個由`getContext`方法創建的繪圖上下文對象完成的。 2D 繪圖接口允許我們填充或者拉伸各種各樣的圖形。這個上下文的`fillStyle`屬性決定了圖形的填充方式。`strokeStyle`和`lineWidth`屬性用來控制線條的繪制方式。 矩形與文字可以通過使用一個簡單的方法調用來繪制。采用`fillRect`和`strokeRect`方法繪制矩形,同時采用`fillText`和`strokeText`方法繪制文字。要創建一個自定義的圖形,我們必須首先建立一個路徑。 調用`beginPath`會創建一個新的路徑。很多其他的方法可以向當前的路徑添加線條和曲線。比如,`lineTo`方法可以添加一條直線。當一條路徑畫完時,它可以被`fill`方法填充或者被`stroke`方法勾勒輪廓。 從一張圖片或者另一個畫布上移動像素到我們的畫布上可以用`drawImage`方法實現。默認情況下,這個方法繪制了整個原圖像,但是通過給它更多的參數,你可以拷貝一張圖片的某一個特定的區域。我們在游戲中使用了這項技術,從包括許多動作的圖像中拷貝出游戲角色的單個獨立動作。 圖形變換允許你向多個方向繪制圖片。2D 繪制上下文擁有一個當前的可以通過`translate`、`scale`與`rotate`進行變換。這些會影響所有的后續的繪制操作。一個變換的狀態可以通過`save`方法來保存,通過`restore`方法來恢復。 在一個畫布上展示動畫時,`clearRect`方法可以用來在重繪之前清除畫布的某一部分。 ## 習題 ### 形狀 編寫一個程序,在畫布上畫出下面的圖形。 1. 一個梯形(一個在一邊比較長的矩形) 2. 一個紅色的鉆石(一個矩形旋轉45度角) 3. 一個鋸齒線 4. 一個由 100 條直線線段構成的螺旋 5. 一個黃色的星星 ![](https://box.kancloud.cn/2015-10-31_563439aa495e7.png) 當繪制最后兩個圖形時,你可以參考第 14 章中的`Math.cos`和`Math.sin`的解釋,它描述了如何使用這兩個函數獲得圓上的坐標。 建議你為每一個圖形創建一個方法,傳入坐標信息,以及其他的一些參數,比如大小或者點的數量。另一種方法,可以在你的代碼中硬編碼,會使得你的代碼變得難以閱讀和修改。 ```html <canvas width="600" height="200"></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); // Your code here. </script> ``` ### 餅狀圖 在本章的前部分,我們看到一個繪制餅狀圖的樣例程序。修改這個程序,使得每個部分的名字可以被顯示在相應的切片旁邊。試著找到一個合適的方法來自動放置這些文字,同時也可以適用于其他數據。你可以假設分類大到足以為標簽留出空間。 你可能還會需要`Math.sin`和`Math.cos`方法,像第 14 章描述的一樣。 ```html <canvas width="600" height="300"></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); let total = results .reduce((sum, {count}) => sum + count, 0); let currentAngle = -0.5 * Math.PI; let centerX = 300, centerY = 150; // Add code to draw the slice labels in this loop. results.forEach(function(result) { for (let result of results) { let sliceAngle = (result.count / total) * 2 * Math.PI; cx.arc(centerX, centerY, 100, currentAngle, currentAngle + sliceAngle); currentAngle += sliceAngle; cx.lineTo(centerX, centerY); cx.fillStyle = result.color; cx.fill(); } </script> ``` ### 彈力球 使用在第 14 章和第 16 章出現的`requestAnimationFrame`方法畫出一個裝有彈力球的盒子。這個球勻速運動并且當撞到盒子的邊緣的時候反彈。 ```html <canvas width="400" height="400"></canvas> <script> let cx = document.querySelector("canvas").getContext("2d"); let lastTime = null; function frame(time) { if (lastTime != null) { updateAnimation(Math.min(100, time - lastTime) / 1000); } lastTime = time; requestAnimationFrame(frame); } requestAnimationFrame(frame); function updateAnimation(step) { // Your code here. } </script> ``` ### 預處理鏡像 當進行圖形變換時,繪制位圖圖像會很慢。每個像素的位置和大小都必須進行變換,盡管將來瀏覽器可能會更加聰明,但這會導致繪制位圖所需的時間顯著增加。 在一個像我們這樣的只繪制一個簡單的子畫面圖像變換的游戲中,這個不是問題。但是如果我們需要繪制成百上千的角色或者爆炸產生的旋轉粒子時,這將會成為一個問題。 思考一種方法來允許我們不需要加載更多的圖片文件就可以畫出一個倒置的角色,并且不需要在每一幀調用`drawImage`方法。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看