天天看點

《HTML5+JavaScript動畫基礎》——2.3 用代碼實作動畫

本節書摘來自異步社群《html5+javascript動畫基礎》一書中的第2章,第2.3節,作者:【美】billy lamberta , keith peters著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

在準備好html5檔案的基本結構之後,我們已經了解了足夠多的基礎知識,可以開始編碼了。我們需要一個文本編輯器用來輸入示例代碼以及一個支援html5的web浏覽器運作這些示例。同時我們需要熟悉該浏覽器内置的開發者控制台。在準備好這些工具後(它們很可能已經在你的電腦上)我們就可以出發了,讓我們深入學習一些動畫吧。

2.3.1 動畫循環

幾乎所有的程式動畫都會表現為某種形式的循環。我們會建立一個展現一系列圖像的流程圖以實作逐幀動畫,其中每一幀隻需要繪制出來即可,如圖2-2所示。

《HTML5+JavaScript動畫基礎》——2.3 用代碼實作動畫

當你開始繪制圖像時,盡管每幅圖有細小的差别,javascript代碼也不會為每一幀建立并儲存一幅新的圖像,即便是逐幀動畫。我們會為每一幀圖像儲存其繪制到canvas上的每一個對象的位置、尺寸、顔色及其他屬性。是以如果要實作一個球滾過螢幕的動畫,我們會在每一幀中儲存球的位置資訊。例如,第1幀标明球處在距離左邊緣第10個像素的位置,第2幀則标明在第15個像素的位置,諸如此類。代碼會讀取這些相關的資料,并根據這些資料将對象繪制出來,進而顯示這些幀。據此過程可以衍生出如下流程圖,如圖2-3所示。

《HTML5+JavaScript動畫基礎》——2.3 用代碼實作動畫

而描述一個動态的,編碼的動畫的流程圖則如圖2-4所示。

《HTML5+JavaScript動畫基礎》——2.3 用代碼實作動畫

從圖2-4中可以看出,其中并不存在第1幀,第2幀等概念。程式動畫通常并總是可以通過一幀就完成動畫。由此可以看出我們所指的循環的含義。

首先,設定初始狀态,比如,用canvas内置的繪圖api向螢幕上繪制一個圓形,渲染并顯示這一幀。然後應用規則,規則可以簡單如“球向右移動5個像素”,當然也可能由幾十條遵循複雜三角函數的線構成。本書中的示例包含從簡單到複雜的各種情況。應用規則後會變遷到新的狀态,此時會根據一個新的圖像描述渲染并顯示。随後同樣的規則會重複應用。

同一套規則會不斷重複地應用,而不會出現為第一幀關聯一套規則而又為第二幀關聯另一套規則的情況。是以這裡的挑戰在于如何拿出一套規則,使之能夠處理場景中可能出現的各種情況。當球滾出canvas的右邊界怎麼辦?這套規則需要考慮到這種情況。你是否希望使用者可以使用滑鼠與球産生互動?這套規則也需要将其處理好。

這聽上去有些令人氣餒,不過實際上并沒想象中那麼複雜。可以通過建立一兩個規則實作一些簡單的行為,以此為基礎再不斷添加新的規則。這裡所指的規則實際上就是程式語句。每條規則可以由一條或多條語句構成。以球向右移動5個像素為例,該規則在javascript中可以通過以下語句表達:

<code>ball.x = ball.x + 5;</code>

以上語句表示在目前球體所處的x軸(水準軸)上右移5個像素,以此作為球體在x軸上的新位置。甚至可以将語句簡化成以下這樣:

<code>ball.x += 5;</code>

+=操作符會将右側的值與左側的變量相加,并把結果賦給左邊的變量。

下面是一套更加進階的規則,它會出現在本書的後續章節中:

現在我們不用擔心以上語句的含義,隻需知道這些代碼會重複執行,進而不斷産生新的幀。

那麼,這些循環如何運作呢?下面的解釋是許多入門的程式員都會有的一種誤解:循環的運作依賴于幾乎存在于所有程式設計語言中的while循環結構。通過以下代碼設定一個無限循環用于更新球體的位置:

這段代碼看上去很簡單:因為ture值永遠為真,while子句的條件判斷永遠成立,是以循環不斷執行。在循環中球體的x軸坐标每次增加1個像素,從0到1、2、3、4等。球體在canvas上從左向右不斷地移動。

如果你也犯過類似的錯誤,你會知道通過以上代碼你并不會看到球體穿越canvas的動畫,實際上你壓根看不到球體,因為它已經越過了螢幕的右邊界。為什麼球體沒有移動到循環中的各個位置?實際上,它已經移動到那些位置。你之是以看不到它是因為我們沒有更新canvas元素的顯示。圖2-5的流程圖展現了本質上發生的事件:

《HTML5+JavaScript動畫基礎》——2.3 用代碼實作動畫

應用規則并把球體移動到新的位置,新的圖像得以建立,不過我們始終沒有機會将其顯示出來,因為在幀結束的時候并沒有将對象繪制到canvas上。而這才是重點。

為了實作動畫,需要為每一幀執行以下操作:

(1)執行該幀所要調用到的代碼;

(2)将所有對象繪制到canvas上;

(3)重複這一過程渲染下一幀。

将這些步驟記在腦子裡,為此建立一個函數用于不斷更新對象的位置并将它繪制到canvas元素上。然後建立一個javascript定時器啟動循環:

以上代碼定義drawframe函數用于更新球體的位置并使用draw方法(尚未建立)将其繪制到canvas上。然後将drawframe作為參數傳遞給window.setinterval方法,該方法會根據第二個參數指定的間隔時間以毫秒為機關重複執行drawframe函數。在本例中,間隔時間為1000/60,即一秒60幀,差不多17ms一幀。

在相當長時間内,開發者通過以上方式使用javascript建立一個動畫循環。如果你堅持,你仍舊可以在本書的所有示例代碼中運用此方式。不過問題在于,javascript定時器并不是為實作動畫設計的。因為它無法達到毫秒級的精确度(每個浏覽器的定時器分辨率不同),是以無法依賴它實作高品質的動畫。除此之外,通過第二個參數指定的間隔時間僅僅是請求在那一時刻執行而已。如果同一時刻在浏覽器的任務隊列中有其他任務的話,動畫代碼的執行任務将不得不等待在那裡。

由于動畫并不是之前html規範所包含的功能,是以浏覽器廠商并沒有将此種優化放在一個優先級很高的位置上。不過,随着html5中canvas元素的引入以及對多媒體内容的關注,不同的浏覽器之間再次在性能和速度上展開了激烈競争。由于認識到動畫已成為web應用中一個日益重要的組成部分,浏覽器廠商開始不斷提出新的解決方案以應對這一需求。

2.3.2 使用requestanimationframe的動畫循環

由于開發者對基于html5的動畫興趣日增,是以web浏覽器為此專門為javascript的開發者實作了一個api,通過它提供了基于浏覽器的優化實作。window.requestanimationframe函數接收一個回調函數作為參數,并確定在重繪螢幕前執行該回調函數。在某些浏覽器中,還支援一個可選的第二參數,可以通過它指定的一個html元素提供動畫的可視區域。在回調函數中對程式的修改必定發生在下一個浏覽器重繪事件之前。可以通過對requestanimationframe函數的鍊式調用實作動畫循環:

這看上去隻是一小段代碼,但是了解其工作方式非常重要,因為這是實作動畫循環的核心思想,它貫穿了本書的所有示例。這裡定義了drawframe函數,其中包含了每一幀将要執行的動畫代碼。該函數的第一行代碼調用了window.requestanimationframe函數并将draw frame函數自身的引用作為參數值傳入。第二個可選參數是要繪制的canvas。你可能會覺得驚訝,我們居然可以在完成一個函數的定義之前就把它作為一個參數值傳入另一個函數。切記,在函數運作到需要将它作為參數值傳入時,它早已定義好了。

當執行drawframe函數時,window.requestanimationframe将drawframe函數放入隊列等待在下一個動畫間隔中再次執行,而當它再次執行時又會重複這一過程。由于不斷地請求執行該函數,是以就串聯成了一個循環。是以,該函數中定義的代碼會不斷地調用,使得我們可以在canvas上以細微的間隔時間繪制動畫。

為了啟動循環,在定義好drawframe函數後,就用一個圓括号将其包起來并立即調用它。這是一種更節省空間,也更加清晰(這裡有争議)的做法,另一種傳統的方式是首先定義函數,然後立即在下一行代碼中調用它。

由于requestanimationframe是一個相對新的功能,是以目前的浏覽器還緻力于各自的實作。如果你希望代碼具備更好的跨平台性,下面這一小段代碼就可以用來規範該函數在不同浏覽器中的實作。

這段代碼先檢查了window.requestanimationframe函數的定義是否存在,如果不存在,就周遊已知的各種浏覽器實作并替代該函數。如果它還是找不到一個與浏覽器相關的實作,它最終會采用基于javascript定時器的動畫以每秒60幀的間隔調用settimeout函數。

由于以上針對浏覽器的環境檢查會被所有示例用到,是以把這個函數放入了utils.js檔案中以導入html5檔案中。如此,就可以確定動畫循環可以工作在多個浏覽器上,而通過這種方式也使得腳本更加簡潔,進而可以把注意力放在了解每個示例的核心思想上。