跳转到内容

画布 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() 来绘制新页面。

否则,如果按钮没有被点击,则在 eventnull 时呈现页面的背景。

其他两个页面的处理函数的工作方式类似,只是第二个页面有两个按钮

      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 的实现

[编辑 | 编辑源代码]

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

< 画布 2D Web 应用

除非另有说明,否则本页上的所有示例源代码均授予公有领域。
华夏公益教科书