Canvas 2D Web Apps/Framework

From Wikibooks, open books for an open world
Jump to navigation Jump to search

This chapter introduces the cui2d framework, which is used (and explained in detail) in the following chapters. cui2d is a light-weight collection of JavaScript functions that supports the creation of user interfaces with the canvas 2D context. Thus, instead of starting from scratch in every chapter and introducing GUI elements one by one, a framework is presented that includes these GUI elements. There are a couple of advantages to this approach:

  • You can get started and apply the GUI elements without having to work through the details of their implementation.
  • All GUI elements that are discussed in this wikibook are available by including one script file (cui2d.js) and they (are supposed to) work together without problems.
  • Automatically generated (by JSDoc3) reference documentation of cui2d is available online.
  • It is easier to understand the implementation of individual GUI elements by seeing the big picture of how they work together. In fact, some of the aspects of the implementation just don't make sense without this big picture.

Next, the framework is introduced with the help of an example.

A “Hello, World!” Example[edit | edit source]

The example of this chapter just shows a page with the text “Hello, World!” using cui2d. It is also available online and should work fine on desktop and mobile web browsers. The parts for downloading the web app to a mobile device are missing but a version that includes them is available online. (See the chapter on iOS web apps for a discussion of these parts.)

<!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>

Discussion[edit | edit source]

The first few lines just start the HTML file. The line

    <meta name="viewport" 
      content="width=device-width, initial-scale=1.0, user-scalable=no">

was included to avoid complications with scaling the content on mobile devices. In fact, the page in this example is not only scalable but can also be rotated and dragged around with two-finger gestures. (With a mouse, the page can only be dragged. Sorry.) However, all these transformations are controlled by the web app instead of the operating system.

The line

    <script src="cui2d.js"></script>

includes the file cui2d.js; thus, that file should be in the same directory as the HTML page such that the web browser can find it.

The function init() is specified in the body tag at the end of the HTML page and will be called after the page has been loaded.

    ...
    <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() first sets some default options, which are applied by the cui2d framework before any page is repainted. This makes it possible to change these options for all pages in a single place. After that, it uses the call cuiInit(myPage); to initialize the cui2d framework and start with displaying myPage which is defined next:

      ...
      // create a new page of size 400x300 and attach myPageProcess
      var myPage = new cuiPage(400, 300, myPageProcess);
      ...

This defines a global variable myPage and creates a new cuiPage object of width 400 pixels and height 300 pixels. The coordinate system for this page has its origin (0,0) in the top, left corner, is 400 pixels wide and 300 pixels high. The content and the behavior of the page is defined by the function myPageProcess(), which is specified as third argument in the constructor of the cuiPage object and is defined next:

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

Every GUI component in cui2d has a single “process” function, which repaints the GUI component (or the whole canvas in the case of a page) if the argument event is null. Otherwise (if event is not null), it tries to process the user event. Thus, each process function has two tasks. Even myPageProcess in this example performs both tasks; however, for user events it only returns false, which means that the user event has not been processed. In this example, these events allow the page to be dragged with a mouse or transformed in multiple ways with two-finger gestures. If you change the code to return true then it is assumed that the process function has “consumed” these events and, thus, the page cannot be transformed in any way (except by changing the orientation of a mobile device).

In the case that no event has been specified, the function just writes some text at the center of the page, sets a new fill color and fills the whole page. Note that we render from front to back, which is the standard in cui2d. Also note that “this” refers to the cuiPage object; thus, this.width and this.height are just the width and height of myPage.

Lastly, the HTML code defines the body tag with a message in case the canvas is not displayed for some reason; for example, if the web browser does not support the canvas element.

  ...
  <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>

The function init() is specified as the handler of the onload event; thus, it is called when the page is loaded. The background color is set to black. This background color is sometimes visible when the window of a desktop browser is being resized and during the animation of an orientation change of a mobile device. The WebKit style attributes help to avoid any default user interaction with the HTML page.

The code is rather long for a hello-world example. This is partly due to the fact that it supports multiple platforms, partly due to the definition of customized colors, and partly due to the structure of the cui2d framework, which requires a cuiPage object in order to display anything. The next section discusses how the cui2d framework actually works internally.

Implementation of the Render Loop[edit | edit source]

This section discusses how the cui2d framework renders dynamic graphics, i.e. interactively manipulated graphics and animations. This is particularly useful if you want to modify or extend the framework, or if you want to implement your own framework.

The code in cui2d.js starts with many global variables. Most of them will only make sense once you see how they are used. They are included here just for completeness:

/**
 * @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";
...

These global variables are followed by the definition of the functions cuiInit(), cuiResize(), some more user event handlers (which are discussed in a later chapter), cuiRepaint(), and cuiProcess(). Before reading the code, you should understand how they work together. Consider the following diagram:


cuiInit(startPage) calls cuiRepaint()
calls
cuiRenderLoop() calls itself in an infinite loop
calls (if repainting requested with cuiRepaint())
cuiProcess(null)
calls
cuiCurrentPage.process(null) calls process() for all elements on the page


As illustrated, the call cuiInit(myPage) in the example starts a chain reaction that in the end calls cuiCurrentPage.process(null), i.e. myPage.process(null) in our example, which is nothing but myPageProcess(null), i.e. the process function that we have defined for our page. Furthermore, cuiRenderLoop() calls itself in an infinite loop such that it keeps calling myPageProcess(null) to repaint our page whenever necessary (for example, when the user has dragged the page). In technical terms, repainting is “necessary” whenever cuiRepaint() has been called since the last repaint. The diagram also includes cuiProcess(), which is responsible for the correct geometric transformation of the current page (and for the processing of events for which the process function of the page returns false).

The definition of cuiInit() mainly adds a canvas element to the body of the HTML page, adds event listeners (which will be discussed later), and then initializes global variables for various parts of the cui2d framework. In the last two lines, it calls cuiRepaint() and 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();
}   
...

The only event handler that we mention here is cuiResize() because it is extremely simple:

...
/** Resize handler. */
function cuiResize() {
  cuiRepaint();
}
...

I.e., it just calls 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() just sets the global variable cuiCanvasNeedsRepaint to true. This variable is checked by 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
}
...

cuiRenderLoop() calls cuiProcess(null) to repaint the current page if cuiCanvasNeedsRepaint is true (and also if cuiAnimationsArePlaying is true but that is another story).

In the last line, cuiRenderLoop() calls itself (after a time specified by cuiAnimationStep) by using the HTML5 function setTimeout. Since it always calls itself again, it continues in an endless loop until the web app is closed.

The next function is cuiProcess(). However, this function makes heavy use of draggable objects, which will be discussed later. Thus, the discussion of cuiProcess has to wait until then.

Benefits of a Render Loop[edit | edit source]

A render loop might appear to be a complicated way to do such a simple thing as calling a render function (i.e. the process function with argument null in cui2d). However, there are good reasons for a render loop:

  • The render function should not be called for every user event since there can be too many user events; for example, a new event for every movement of the mouse.
  • In animations, the render function should be called regularly (e.g., 60 frames per second) without any event triggering the rendering.

A render loop solves these problems by calling the render function as often as necessary but not more often.


< Canvas 2D Web Apps

Unless stated otherwise, all example source code on this page is granted to the public domain.