跳转到内容

画布 2D 网页应用/框架

来自维基教科书,开放世界中的开放书籍

本章介绍了 cui2d 框架,它将在后续章节中使用(并详细解释)。cui2d 是一个轻量级的 JavaScript 函数集合,支持使用画布 2D 上下文创建用户界面。因此,与其在每一章从头开始并逐个介绍 GUI 元素,不如提供一个包含这些 GUI 元素的框架。这种方法有几个优点

  • 你可以开始并应用 GUI 元素,而无需深入了解它们的实现细节。
  • 所有在本维基教科书中讨论的 GUI 元素都可以通过包含一个脚本文件(cui2d.js)来获得,并且它们(应该)能够无缝协同工作。
  • cui2d 的自动生成(由 JSDoc3 生成)的参考文档可在 网上 获得。
  • 通过查看它们如何协同工作的整体 picture,更容易理解单个 GUI 元素的实现。事实上,实现的某些方面在没有这个整体 picture 的情况下是无法理解的。

接下来,我们将通过一个示例介绍该框架。

一个“Hello, World!”示例

[编辑 | 编辑源代码]

本章的示例仅显示了一个使用 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”函数,如果参数 eventnull,则该函数会重新绘制 GUI 组件(或整个画布,对于页面而言)。否则(如果 event 不为 null),它会尝试处理用户事件。因此,每个 process 函数都有两个任务。即使本示例中的 myPageProcess 也执行了这两个任务;但是,对于用户事件,它只返回 false,这意味着用户事件没有被处理。在本示例中,这些事件允许使用鼠标拖动页面,或者使用双指手势以多种方式变换页面。如果将代码更改为返回 true,则假定 process 函数已“使用”了这些事件,因此页面无法以任何方式变换(除非更改移动设备的方向)。

在没有指定事件的情况下,该函数只是在页面的中心写入一些文本,设置新的填充颜色,并填充整个页面。请注意,我们从前到后渲染,这是 cui2d 的标准做法。还要注意,“this”指的是 cuiPage 对象;因此,this.widththis.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 <[email protected]>
 */

/** 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 设置为 truecuiRenderLoop() 会检查这个变量

...
/** 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
}
...

如果 cuiCanvasNeedsRepainttrue(以及如果 cuiAnimationsArePlayingtrue,但这又是另一个故事),cuiRenderLoop() 会调用 cuiProcess(null) 来重新绘制当前页面。

在最后一行,cuiRenderLoop() 使用 HTML5 函数 setTimeout 调用自身(在 cuiAnimationStep 指定的时间后)。由于它总是调用自身,因此它会在 web 应用关闭之前一直持续在一个无限循环中。

下一个函数是 cuiProcess()。但是,此函数大量使用了可拖动对象,这些对象将在后面讨论。因此,cuiProcess 的讨论必须等到那时。

渲染循环的好处

[编辑 | 编辑源代码]

渲染循环似乎是一种复杂的方式来完成调用渲染函数(即 cui2d 中带参数 null 的 process 函数)这样简单的事情。但是,使用渲染循环有充分的理由

  • 渲染函数不应针对每个用户事件都调用,因为用户事件可能太多;例如,鼠标的每次移动都产生一个新的事件。
  • 在动画中,渲染函数应该定期调用(例如,每秒 60 帧),而无需任何事件触发渲染。

渲染循环通过尽可能频繁地调用渲染函数来解决这些问题,但不会过于频繁。


< Canvas 2D Web 应用程序

除非另有说明,本页所有示例源代码均为公共领域。
华夏公益教科书