画布 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);
}
}