畫布 2D 網頁應用/框架
本章介紹了 cui2d 框架,它將在後續章節中使用(並詳細解釋)。cui2d 是一個輕量級的 JavaScript 函式集合,支援使用畫布 2D 上下文建立使用者介面。因此,與其在每一章從頭開始並逐個介紹 GUI 元素,不如提供一個包含這些 GUI 元素的框架。這種方法有幾個優點
- 你可以開始並應用 GUI 元素,而無需深入瞭解它們的實現細節。
- 所有在本華夏公益教科書中討論的 GUI 元素都可以透過包含一個指令碼檔案(
cui2d.js)來獲得,並且它們(應該)能夠無縫協同工作。 - cui2d 的自動生成(由 JSDoc3 生成)的參考文件可在 網上 獲得。
- 透過檢視它們如何協同工作的整體 picture,更容易理解單個 GUI 元素的實現。事實上,實現的某些方面在沒有這個整體 picture 的情況下是無法理解的。
接下來,我們將透過一個示例介紹該框架。
本章的示例僅顯示了一個使用 cui2d 顯示“Hello, World!”文字的頁面。它也可以在 網上 獲得,並且應該在桌面和移動 web 瀏覽器上正常工作。下載 web 應用到移動裝置的部分缺失,但包含這些部分的版本可在 網上 獲得。(有關這些部分的討論,請參見有關 iOS 網頁應用 的章節。)
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no">
<script src="cui2d.js"></script>
<script>
function init() {
// set defaults for all pages
cuiBackgroundFillStyle = "#A06000";
cuiDefaultFont = "bold 40px Helvetica, sans-serif";
cuiDefaultFillStyle = "#402000";
// initialize cui2d and start with myPage
cuiInit(myPage);
}
// create a new page of size 400x300 and attach myPageProcess
var myPage = new cuiPage(400, 300, myPageProcess);
// a function to repaint the canvas and return false (if null == event)
// or to process user events (if null != event) and return true
// if the event has been processed
function myPageProcess(event) {
if (null == event) { // repaint this page
cuiContext.fillText("Hello, World!", 200, 150);
cuiContext.fillStyle = "#E0FFE0"; // set page color
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false; // event has not been processed
}
</script>
</head>
<body bgcolor="#000000" onload="init()"
style="-webkit-user-drag:none; -webkit-user-select:none; ">
<span style="color:white;">A canvas element cannot be displayed.</span>
</body>
</html>
前幾行只是啟動了 HTML 檔案。這行
<meta name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no">
被包含進來是為了避免在移動裝置上縮放內容時的複雜情況。事實上,此示例中的頁面不僅可縮放,還可以透過雙指手勢進行旋轉和拖動。(使用滑鼠,頁面只能拖動。抱歉。)但是,所有這些變換都是由 web 應用控制,而不是由作業系統控制。
這行
<script src="cui2d.js"></script>
包含了檔案 cui2d.js;因此,該檔案應該與 HTML 頁面位於同一目錄下,以便 web 瀏覽器可以找到它。
函式 init() 在 HTML 頁面的末尾的 body 標籤中指定,並且將在頁面載入後呼叫。
...
<script>
function init() {
// set defaults for all pages
cuiBackgroundFillStyle = "#A06000";
cuiDefaultFont = "bold 40px Helvetica, sans-serif";
cuiDefaultFillStyle = "#402000";
// initialize cui2d and start with myPage
cuiInit(myPage);
}
...
init() 首先設定一些預設選項,這些選項會在 cui2d 框架在任何頁面重新繪製之前應用。這使得可以更改所有頁面的這些選項,而只需在一個地方進行操作。之後,它使用呼叫 cuiInit(myPage); 來初始化 cui2d 框架,並開始顯示 myPage,它將在下面定義
...
// create a new page of size 400x300 and attach myPageProcess
var myPage = new cuiPage(400, 300, myPageProcess);
...
這定義了一個全域性變數 myPage,並建立了一個寬度為 400 畫素,高度為 300 畫素的新 cuiPage 物件。該頁面的座標系原點 (0,0) 位於左上角,寬度為 400 畫素,高度為 300 畫素。頁面的內容和行為由函式 myPageProcess() 定義,該函式作為 cuiPage 物件建構函式的第三個引數指定,並在下面定義
...
// a function to repaint the canvas and return false (if null == event)
// or to process user events (if null != event) and return true
// if the event has been processed
function myPageProcess(event) {
if (null == event) { // repaint this page
cuiContext.fillText("Hello, World!", 200, 150);
cuiContext.fillStyle = "#E0FFE0"; // set page color
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false; // event has not been processed
}
</script>
</head>
...
cui2d 中的每個 GUI 元件都有一個單獨的“process”函式,如果引數 event 為 null,則該函式會重新繪製 GUI 元件(或整個畫布,對於頁面而言)。否則(如果 event 不為 null),它會嘗試處理使用者事件。因此,每個 process 函式都有兩個任務。即使本示例中的 myPageProcess 也執行了這兩個任務;但是,對於使用者事件,它只返回 false,這意味著使用者事件沒有被處理。在本示例中,這些事件允許使用滑鼠拖動頁面,或者使用雙指手勢以多種方式變換頁面。如果將程式碼更改為返回 true,則假定 process 函式已“使用”了這些事件,因此頁面無法以任何方式變換(除非更改移動裝置的方向)。
在沒有指定事件的情況下,該函式只是在頁面的中心寫入一些文字,設定新的填充顏色,並填充整個頁面。請注意,我們從前到後渲染,這是 cui2d 的標準做法。還要注意,“this”指的是 cuiPage 物件;因此,this.width 和 this.height 只是 myPage 的寬度和高度。
最後,HTML 程式碼定義了 body 標籤,其中包含一條訊息,以防畫布出於某些原因無法顯示;例如,如果 web 瀏覽器不支援畫布元素。
...
<body bgcolor="#000000" onload="init()"
style="-webkit-user-drag:none; -webkit-user-select:none; ">
<span style="color:white;">A canvas element cannot be displayed.</span>
</body>
</html>
函式 init() 被指定為 onload 事件的處理程式;因此,它在頁面載入時被呼叫。背景顏色設定為黑色。當桌面瀏覽器的視窗正在調整大小以及移動裝置方向更改的動畫過程中,這種背景顏色有時會顯示出來。WebKit style 屬性有助於避免任何預設的使用者與 HTML 頁面的互動。
對於一個 hello-world 示例來說,這段程式碼相當長。這部分是由於它支援多個平臺,部分是由於自定義顏色的定義,部分是由於 cui2d 框架的結構,它需要一個 cuiPage 物件才能顯示任何內容。下一節將討論 cui2d 框架在內部是如何工作的。
本節將討論 cui2d 框架如何渲染動態圖形,即互動式操作的圖形和動畫。如果您想要修改或擴充套件框架,或者想要實現您自己的框架,這將特別有用。
cui2d.js 中的程式碼以許多全域性變數開始。在您瞭解了它們的用法之後,大多數變數才會變得有意義。它們在這裡是為了完整性而包含的
/**
* @file cui2d.js is a light-weight collection of JavaScript functions for creating
* graphical user interfaces in an HTML5 canvas 2d context.
* @version 0.30814
* @license public domain
* @author Martin Kraus <martin@create.aau.dk>
*/
/** The canvas element. Set by cuiInit(). */
var cuiCanvas;
/** The 2d context of the canvas element. Set by cuiInit(). */
var cuiContext;
/** Boolean flag for requesting a repaint of the canvas. Cleared only by cuiProcess(). */
var cuiCanvasNeedsRepaint;
/**
* Currently displayed page.
* @type {cuiPage}
*/
var cuiCurrentPage;
/** Time (in milliseconds after January 1, 1970) when events are no longer ignored. */
var cuiIgnoringEventsEnd;
/** Minimum time between frames in milliseconds. */
var cuiAnimationStep = 15;
/** Time (in milliseconds after January 1, 1970) when the last animation should stop. */
var cuiAnimationsEnd;
/** Boolean flag indicating whether any animation is playing. Set by cuiPlayTransition(). */
var cuiAnimationsArePlaying;
/**
* The animation for all transition effects.
* @type {cuiAnimation}
*/
var cuiAnimationForTransitions;
/**
* The page for all transition effects.
* @type {cuiPage}
*/
var cuiPageForTransitions;
/** Background color. */
var cuiBackgroundFillStyle = "#000000";
/** Default font. */
var cuiDefaultFont = "bold 20px Helvetica, sans-serif";
/** Default horizontal text alignment. */
var cuiDefaultTextAlign = "center";
/** Default vertical text alignment. */
var cuiDefaultTextBaseline = "middle";
/** Default fill style (e.g. for text). */
var cuiDefaultFillStyle = "#000000";
...
這些全域性變數之後是函式 cuiInit()、cuiResize()、一些其他使用者事件處理程式(將在後面的章節中討論)、cuiRepaint() 和 cuiProcess() 的定義。在閱讀程式碼之前,您應該瞭解它們如何協同工作。請考慮以下圖
| cuiInit(startPage) | → | 呼叫 cuiRepaint() | ||
| ↓ | 呼叫 | |||
| cuiRenderLoop() | → | 在一個無限迴圈中呼叫自身 | ||
| ↓ | 呼叫(如果使用 cuiRepaint() 請求重新繪製) | |||
| cuiProcess(null) | ||||
| ↓ | 呼叫 | |||
| cuiCurrentPage.process(null) | → | 呼叫頁面上所有元素的 process() | ||
如上圖所示,示例中的呼叫 cuiInit(myPage) 啟動了一系列連鎖反應,最終呼叫了 cuiCurrentPage.process(null),即我們示例中的 myPage.process(null),它不過是 myPageProcess(null),即我們為頁面定義的 process 函式。此外,cuiRenderLoop() 在一個無限迴圈中呼叫自身,以便在必要時(例如,當用戶拖動頁面時)不斷呼叫 myPageProcess(null) 來重新繪製頁面。從技術角度來說,只要自上次重新繪製以來呼叫了 cuiRepaint(),重新繪製就是“必要的”。該圖還包含 cuiProcess(),它負責當前頁面的正確幾何變換(以及處理頁面 process 函式返回 false 的事件)。
cuiInit() 的定義主要是將一個畫布元素新增到 HTML 頁面的主體中,新增事件監聽器(將在後面討論),然後為 cui2d 框架的各個部分初始化全域性變數。在最後兩行中,它呼叫了 cuiRepaint() 和 cuiRenderLoop()。
...
/**
* Initializes cui2d.
* @param {cuiPage} startPage - The page to display first.
*/
function cuiInit(startPage) {
cuiCanvas = document.createElement("canvas");
cuiCanvas.style.position = "absolute";
cuiCanvas.style.top = 0;
cuiCanvas.style.left = 0;
document.body.appendChild(cuiCanvas);
window.addEventListener("resize", cuiResize);
document.body.addEventListener("click", cuiIgnoreEvent);
document.body.addEventListener("mousedown", cuiMouse);
document.body.addEventListener("mouseup", cuiMouse);
document.body.addEventListener("mousemove", cuiMouse);
document.body.addEventListener("touchstart", cuiTouch);
document.body.addEventListener("touchmove", cuiTouch);
document.body.addEventListener("touchcancel", cuiTouch);
document.body.addEventListener("touchend", cuiTouch);
// initialize globals
cuiContext = cuiCanvas.getContext("2d");
cuiCurrentPage = startPage;
cuiIgnoringEventsEnd = 0;
cuiAnimationsEnd = 0;
cuiAnimationsArePlaying = false;
if (undefined == cuiAnimationStep || 0 >= cuiAnimationStep) {
animationStep = 15;
}
// initialize transitions
cuiAnimationForTransitions = new cuiAnimation();
cuiAnimationForTransitions.previousCanvas = null;
cuiAnimationForTransitions.nextCanvas = null;
cuiAnimationForTransitions.nextPage = "";
cuiAnimationForTransitions.isPreviousOverNext = false;
cuiAnimationForTransitions.isFrontMaskAnimated = false;
cuiPageForTransitions = new cuiPage();
cuiPageForTransitions.process = function(event) {
if (null == event) {
cuiDrawTransition();
}
}
cuiRepaint();
cuiRenderLoop();
}
...
我們在這裡提到的唯一事件處理程式是 cuiResize(),因為它非常簡單
...
/** Resize handler. */
function cuiResize() {
cuiRepaint();
}
...
即,它只是呼叫 cuiRepaint()
...
/** Request to repaint the canvas (usually because some state change requires it). */
function cuiRepaint() {
cuiCanvasNeedsRepaint = true; // is checked by cuiRenderLoop() and cleared by cuiProcess(null)
}
...
cuiRepaint() 只是將全域性變數 cuiCanvasNeedsRepaint 設定為 true。cuiRenderLoop() 會檢查這個變數
...
/** Render loop of cui2d, which calls cuiProcess(null) if needed. */
function cuiRenderLoop() {
var now = (new Date()).getTime();
if (cuiAnimationsEnd < now ) { // all animations over?
if (cuiAnimationsArePlaying) {
cuiRepaint();
// repaint one more time since the rendering might differ
// after the animations have stopped
}
cuiAnimationsArePlaying = false;
}
else {
cuiAnimationsArePlaying = true;
}
if (cuiCanvasNeedsRepaint || cuiAnimationsArePlaying) {
cuiProcess(null);
}
window.setTimeout("cuiRenderLoop()", cuiAnimationStep); // call myself again
// using setTimeout allows to easily change cuiAnimationStep dynamically
}
...
如果 cuiCanvasNeedsRepaint 為 true(以及如果 cuiAnimationsArePlaying 為 true,但這又是另一個故事),cuiRenderLoop() 會呼叫 cuiProcess(null) 來重新繪製當前頁面。
在最後一行,cuiRenderLoop() 使用 HTML5 函式 setTimeout 呼叫自身(在 cuiAnimationStep 指定的時間後)。由於它總是呼叫自身,因此它會在 web 應用關閉之前一直持續在一個無限迴圈中。
下一個函式是 cuiProcess()。但是,此函式大量使用了可拖動物件,這些物件將在後面討論。因此,cuiProcess 的討論必須等到那時。
渲染迴圈似乎是一種複雜的方式來完成呼叫渲染函式(即 cui2d 中帶引數 null 的 process 函式)這樣簡單的事情。但是,使用渲染迴圈有充分的理由
- 渲染函式不應針對每個使用者事件都呼叫,因為使用者事件可能太多;例如,滑鼠的每次移動都產生一個新的事件。
- 在動畫中,渲染函式應該定期呼叫(例如,每秒 60 幀),而無需任何事件觸發渲染。
渲染迴圈透過儘可能頻繁地呼叫渲染函式來解決這些問題,但不會過於頻繁。