畫布 2D Web 應用/頁面
本章介紹如何設定多個頁面,並使用 響應式按鈕 幫助使用者在頁面之間導航。
本章的示例(可線上獲取 線上;也可作為 可下載版本)允許使用者使用四個按鈕在三個不同尺寸的頁面之間導航。頁面會自動縮放以適應瀏覽器視窗或移動裝置螢幕的尺寸。以下部分將討論如何設定頁面。有關其他部分的討論,請參見 響應式按鈕 章和之前的章節。
<!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() {
// get images
imageNormalButton.src = "normal.png";
imageNormalButton.onload = cuiRepaint;
imageFocusedButton.src = "selected.png";
imageFocusedButton.onload = cuiRepaint;
imagePressedButton.src = "depressed.png";
imagePressedButton.onload = cuiRepaint;
// initialize and start cui2d
cuiInit(firstPage);
}
// first page
var firstPage = new cuiPage(400, 300, firstPageProcess);
var button0 = new cuiButton();
var imageNormalButton = new Image();
var imageFocusedButton = new Image();
var imagePressedButton = new Image();
function firstPageProcess(event) {
if (button0.process(event, 300, 50, 80, 50, "next",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button0.isClicked()) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = secondPage;
cuiRepaint();
}
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("First page using landcape format.", 200, 150);
cuiContext.fillStyle = "#E0E0E0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false; // event has not been processed
}
// second page
var secondPage = new cuiPage(400, 400, secondPageProcess);
var button1 = new cuiButton();
var button2 = new cuiButton();
function secondPageProcess(event) {
if (button1.process(event, 20, 50, 120, 50, "previous",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button1.isClicked()) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = firstPage;
cuiRepaint();
}
return true;
}
if (button2.process(event, 300, 50, 80, 50, "next",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button2.isClicked()) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = thirdPage;
cuiRepaint();
}
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("Second page using square format.", 200, 200);
cuiContext.fillStyle = "#FFF0E0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false;
}
// third page
var thirdPage = new cuiPage(400, 533, thirdPageProcess);
var button3 = new cuiButton();
function thirdPageProcess(event) {
if (button3.process(event, 20, 50, 120, 50, "previous",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button3.isClicked()) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = secondPage;
cuiRepaint();
}
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("Third page using portrait format.", 200, 266);
cuiContext.fillStyle = "#FFE0F0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false;
}
</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>
為了實現多個頁面,我們需要為每個頁面建立一個 cuiPage 物件。在示例中,這些物件是透過以下方式建立的
...
var firstPage = new cuiPage(400, 300, firstPageProcess);
...
var secondPage = new cuiPage(400, 400, secondPageProcess);
...
var thirdPage = new cuiPage(400, 533, thirdPageProcess);
...
每個建構函式呼叫都會定義頁面的寬度和高度以及頁面的處理函式。第一個處理函式如下所示
function firstPageProcess(event) {
if (button0.process(event, 300, 50, 80, 50, "next",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button0.isClicked()) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = secondPage;
cuiRepaint();
}
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("First page using landcape format.", 200, 150);
cuiContext.fillStyle = "#E0E0E0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false; // event has not been processed
}
它檢查 button0 是否已處理事件以及按鈕是否被點選(使用 button0.isClicked())。如果是,它會將全域性變數 cuiIgnoreEventEnds 設定為當前時間(以 1970 年 1 月 1 日以來的毫秒數表示)加上 50 毫秒,以便在接下來的 50 毫秒內忽略所有事件。這很有用,因為當前的使用者互動不應應用於下一個頁面,下一個頁面透過將另一個 cuiPage 分配給全域性變數 cuiCurrentPage 來設定。最後,透過呼叫 cuiRepaint() 來繪製新頁面。
否則,如果按鈕沒有被點選,則在 event 為 null 時呈現頁面的背景。
其他兩個頁面的處理函式的工作方式類似,只是第二個頁面有兩個按鈕
function secondPageProcess(event) {
if (button1.process(event, 20, 50, 120, 50, "previous",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button1.isClicked()) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = firstPage;
cuiRepaint();
}
return true;
}
if (button2.process(event, 300, 50, 80, 50, "next",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button2.isClicked()) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = thirdPage;
cuiRepaint();
}
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("Second page using square format.", 200, 200);
cuiContext.fillStyle = "#FFF0E0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false;
}
第三個頁面有一個按鈕
function thirdPageProcess(event) {
if (button3.process(event, 20, 50, 120, 50, "previous",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button3.isClicked()) {
cuiIgnoreEventsEnd = (new Date()).getTime() + 50;
// ignore events for 50 milliseconds
cuiCurrentPage = secondPage;
cuiRepaint();
}
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("Third page using portrait format.", 200, 266);
cuiContext.fillStyle = "#FFE0F0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false;
}
cuiPage 物件的建構函式定義如下
/**
* Pages are the top-level structure of a cui2d user interface: There is always exactly one page visible.
* (In the future there might be a hierarchy of pages visible but then there is always one root page.)
* @typedef cuiPage
*/
/**
* Creates a new cuiPage of specified width and height with the specified process(event) function
* to process an event (with process(event) which should return true to indicate that the event has
* been processed and therefore to prevent the default gestures for manipulating pages) and
* to repaint the page (with process(null) which should always return false).
* Each page has a coordinate system with the origin in the top, left corner and x coordinates between
* 0 and width, and y coordinates between 0 and height.
* @constructor
*/
function cuiPage(width, height, process) {
this.width = width;
this.height = height;
this.process = process;
this.isDraggableWithOneFinger = true; // can be disallowed by setting it to false
this.view = new cuiTransformable(); // the page transformed by the user (set by cuiProcess())
}
cuiPages 只定義了一個方法,該方法只與頁面之間的動畫轉換有關;請參見 過渡 章。
請注意,每個頁面都使用一個 cuiTransformable 物件(稱為 view)進行其變換。這在 cuiProcess 函式(也呼叫頁面的使用者定義處理函式)中應用。該函式比較複雜,因為它必須透過考慮頁面的尺寸和螢幕的尺寸來最佳地縮放頁面。此外,它必須應用可變換物件 view 的變換。然後,它必須應用事件點的逆變換,以便它們被正確變換。
/**
* Either process the event (if event != null) or repaint the canvas (if event == null).
*/
function cuiProcess(event) {
// ignore events if necessary
if (null != event && cuiIgnoringEventsEnd > 0) {
if ((new Date()).getTime() < cuiIgnoringEventsEnd) {
return;
}
}
// clear repaint flag
if (null == event) {
cuiCanvasNeedsRepaint = false;
}
// determine initial scale and position for the page to fit it into the window
var scaleFactor = 1.0;
var offsetX = 0.0;
var offsetY = 0.0;
if (window.innerWidth / cuiCurrentPage.width < window.innerHeight / cuiCurrentPage.height) {
// required X scaling is smaller: use it
scaleFactor = window.innerWidth / cuiCurrentPage.width;
offsetX = 0.0; // X is scaled for full window
offsetY = 0.5 * (window.innerHeight - cuiCurrentPage.height * scaleFactor);
// scaling is too small for Y: offset to center content
}
else { // required Y scaling is smaller: use it
scaleFactor = window.innerHeight / cuiCurrentPage.height;
offsetX = 0.5 * (window.innerWidth - cuiCurrentPage.width * scaleFactor);
// scaling is too small for X: offset to center content
offsetY = 0.0;
}
// adapt coordinates of event
if (null != event) {
// transformation in cuiCurrentPage.view
var mappedX = event.clientX - cuiCurrentPage.view.translationX;
var mappedY = event.clientY - cuiCurrentPage.view.translationY;
mappedX = mappedX - offsetX - 0.5 * cuiCurrentPage.width * scaleFactor;
mappedY = mappedY - offsetY - 0.5 * cuiCurrentPage.height * scaleFactor;
var angle = -cuiCurrentPage.view.rotation * Math.PI / 180.0;
var tempX = Math.cos(angle) * mappedX - Math.sin(angle) * mappedY;
mappedY = Math.sin(angle) * mappedX + Math.cos(angle) * mappedY;
mappedX = tempX / cuiCurrentPage.view.scale;
mappedY = mappedY / cuiCurrentPage.view.scale;
mappedX = mappedX + offsetX + 0.5 * cuiCurrentPage.width * scaleFactor;
mappedY = mappedY + offsetY + 0.5 * cuiCurrentPage.height * scaleFactor;
// initial transformation for fitting the page into the window
event.eventX = (mappedX - offsetX) / scaleFactor;
event.eventY = (mappedY - offsetY) / scaleFactor;
}
// initialize drawing
if (null == event) {
cuiCanvas.width = window.innerWidth;
cuiCanvas.height = window.innerHeight;
// The following line is not necessary because we set the canvas size:
// cuiContext.clearRect(0, 0, cuiCanvas.width, cuiCanvas.height);
// Some people recommend to avoid setting the canvas size every frame,
// but I had trouble with rendering a transition effect on Firefox without it.
// transformation in cuiCurrentPage.view
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.translate(cuiCurrentPage.view.translationX, cuiCurrentPage.view.translationY);
cuiContext.translate(offsetX + 0.5 * cuiCurrentPage.width * scaleFactor,
offsetY + 0.5 * cuiCurrentPage.height * scaleFactor);
cuiContext.rotate(cuiCurrentPage.view.rotation * Math.PI / 180.0);
cuiContext.scale(cuiCurrentPage.view.scale, cuiCurrentPage.view.scale);
cuiContext.translate(-offsetX - 0.5 * cuiCurrentPage.width * scaleFactor,
-offsetY - 0.5 * cuiCurrentPage.height * scaleFactor);
// initial transformation for fitting the page into the window
cuiContext.translate(offsetX, offsetY);
cuiContext.scale(scaleFactor, scaleFactor);
cuiContext.globalCompositeOperation = "destination-over";
cuiContext.globalAlpha = 1.0;
cuiContext.font = cuiDefaultFont;
cuiContext.textAlign = cuiDefaultTextAlign;
cuiContext.textBaseline = cuiDefaultTextBaseline;
cuiContext.fillStyle = cuiDefaultFillStyle;
}
if (!cuiCurrentPage.process(event) && cuiCurrentPage != cuiPageForTransitions && null != event) {
// event hasn't been processed, not a transition, and we have an event?
event.eventX = event.clientX; // we don't need any transformation here because the initial ...
event.eventY = event.clientY; // ... transformation is applied to the arguments of ...
// ... view.process() and the transformation in view is applied internally in view.process()
if (cuiCurrentPage.view.process(event, offsetX, offsetY, cuiCurrentPage.width * scaleFactor,
cuiCurrentPage.height * scaleFactor,
null, null, null, null, null, cuiCurrentPage.isDraggableWithOneFinger)) {
// limit translation such that users don't lose the page
if (cuiCurrentPage.view.translationX < -0.5 * window.innerWidth *
Math.max(1.0, cuiCurrentPage.view.scale)) {
cuiCurrentPage.view.translationX = -0.5 * window.innerWidth *
Math.max(1.0, cuiCurrentPage.view.scale);
}
if (cuiCurrentPage.view.translationX > 0.5 * window.innerWidth *
Math.max(1.0, cuiCurrentPage.view.scale)) {
cuiCurrentPage.view.translationX = 0.5 * window.innerWidth *
Math.max(1.0, cuiCurrentPage.view.scale);
}
if (cuiCurrentPage.view.translationY < -0.5 * window.innerHeight *
Math.max(1.0, cuiCurrentPage.view.scale)) {
cuiCurrentPage.view.translationY = -0.5 * window.innerHeight *
Math.max(1.0, cuiCurrentPage.view.scale);
}
if (cuiCurrentPage.view.translationY > 0.5 * window.innerHeight *
Math.max(1.0, cuiCurrentPage.view.scale)) {
cuiCurrentPage.view.translationY = 0.5 * window.innerHeight *
Math.max(1.0, cuiCurrentPage.view.scale);
}
}
}
// draw background
if (null == event) {
cuiContext.globalCompositeOperation = "destination-over";
cuiContext.globalAlpha = 1.0;
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.fillStyle = cuiBackgroundFillStyle;
cuiContext.fillRect(0, 0, cuiCanvas.width, cuiCanvas.height);
}
}