Canvas 2D Web Apps/Overlays

This chapter extends the chapter on pages by adding a semitransparent overlay to the three pages and moving the buttons from the pages to that overlay.

An overlay in cui2d is just a page (i.e. a cuiPage), which is used as an overlay in another page (i.e. its processOverlay method is called in the process function of the other page). Apart from that, they are just ordinary pages with their own process function (which is called by processOverlay); thus, an overlay page can in fact be used just like any other page.

The Example

edit

The example of this chapter (which is available online; also as downloadable version) adds another (overlay) page and its process function to the example of the chapter about pages. Two buttons are then called from the overlay page's process function. This chapter focuses on how to create an overlay page and how the other pages include the overlay page. See the chapters on pages and responsive buttons for other 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() {
        // 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);
      }

      // overlay page
      var imageNormalButton = new Image();
      var imageFocusedButton = new Image();
      var imagePressedButton = new Image();
      var button0 = new cuiButton();
      var button1 = new cuiButton();

      var overlayPage = new cuiPage(600, 90, overlayPageProcess);
      overlayPage.isAdjustingHeight = false; // only adjust width
      overlayPage.verticalAlignment = -1; // top align
      overlayPage.interactionBits = (cuiConstants.isDraggableWithOneFinger | cuiConstants.isLimitedToVerticalDragging);

      function overlayPageProcess(event) {
        cuiContext.fillStyle = "#FFFFFF"; // draw in white

        if (button0.process(event, 20, 20, 120, 50, "previous",
          imageNormalButton, imageFocusedButton, imagePressedButton)) {
          if (button0.isClicked()) {
            if (cuiCurrentPage == secondPage) {
              cuiIgnoreEventsEnd = (new Date()).getTime() + 50; 
                  // ignore events for 50 milliseconds
              cuiCurrentPage = firstPage;          
            }
            else if (cuiCurrentPage == thirdPage) {
              cuiIgnoreEventsEnd = (new Date()).getTime() + 50; 
              cuiCurrentPage = secondPage;   
            }
            cuiRepaint(); 
          }
          return true;
        }
        if (button1.process(event, 150, 20, 80, 50, "next",
          imageNormalButton, imageFocusedButton, imagePressedButton)) {
          if (button1.isClicked()) {
            if (cuiCurrentPage == firstPage) {
              cuiIgnoreEventsEnd = (new Date()).getTime() + 50; 
                  // ignore events for 50 milliseconds
              cuiCurrentPage = secondPage;          
            }
            else if (cuiCurrentPage == secondPage) {
              cuiIgnoreEventsEnd = (new Date()).getTime() + 50; 
              cuiCurrentPage = thirdPage;   
            }
            cuiRepaint(); 
          }
          return true;
        }
        if (null == event) {
          // draw background
          cuiContext.globalAlpha = 0.3;
          cuiContext.fillRect(0, 0, this.width, this.height);
        }
        return false;  // event has not been processed
      }

      // first page

      var firstPage = new cuiPage(400, 300, firstPageProcess);

      function firstPageProcess(event) {
        if (overlayPage.processOverlay(event)) {
          return true;
        }
        if (null == event) {
          // draw background
          cuiContext.fillText("First page using landscape format.", 200, 150);
          cuiContext.fillStyle = "#C0C0C0";
          cuiContext.fillRect(0, 0, this.width, this.height);
        }
        return false;  // event has not been processed
      }

      // second page

      var secondPage = new cuiPage(400, 400, secondPageProcess);

      function secondPageProcess(event) {
        if (overlayPage.processOverlay(event)) {
          return true;
        }
        if (null == event) {
          // draw background
          cuiContext.fillText("Second page using square format.", 200, 200);
          cuiContext.fillStyle = "#DDD0C0";
          cuiContext.fillRect(0, 0, this.width, this.height);
        }
        return false;
      }

      // third page

      var thirdPage = new cuiPage(400, 533, thirdPageProcess);

      function thirdPageProcess(event) {
        if (overlayPage.processOverlay(event)) {
          return true;
        }
        if (null == event) {
          // draw background
          cuiContext.fillText("Third page using portrait format.", 200, 266);
          cuiContext.fillStyle = "#DDC0D0";
          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>


Creating Overlay Pages

edit

Pages that should be used as overlay pages are created like other pages. However, overlays are often not transformable. In this example, the overlay can only be dragged vertically with one finger. If both the page and its overlay are not transformable, then they should have the same dimensions and should use the same layout. If, however, the page is transformable but the overlay is not transformable, things get a bit more complicated. Often, the overlay will be adjusted to either the width or the height of the screen and it will be aligned with one of the corresponding edges. In the example, the width is adjusted and the top edge of the overlay is aligned:

      ...
      var overlayPage = new cuiPage(600, 90, overlayPageProcess);
      overlayPage.isAdjustingHeight = false; // only adjust width
      overlayPage.verticalAlignment = -1; // top align
      overlayPage.interactionBits = (cuiConstants.isDraggableWithOneFinger | cuiConstants.isLimitedToVerticalDragging);
      ...

The process function of the overlay page includes two buttons (“previous” and “next”). Since the same overlay is used for all three pages, these buttons are also used for all three pages and, therefore, have to check the current value of cuiCurrentPage in order to set it to the correct new value:

      ...
      function overlayPageProcess(event) {
        cuiContext.fillStyle = "#FFFFFF"; // draw in white

        if (button0.process(event, 20, 20, 120, 50, "previous",
          imageNormalButton, imageFocusedButton, imagePressedButton)) {
          if (button0.isClicked()) {
            if (cuiCurrentPage == secondPage) {
              cuiIgnoreEventsEnd = (new Date()).getTime() + 50; 
                  // ignore events for 50 milliseconds
              cuiCurrentPage = firstPage;          
            }
            else if (cuiCurrentPage == thirdPage) {
              cuiIgnoreEventsEnd = (new Date()).getTime() + 50; 
              cuiCurrentPage = secondPage;   
            }
            cuiRepaint(); 
          }
          return true;
        }
        if (button1.process(event, 150, 20, 80, 50, "next",
          imageNormalButton, imageFocusedButton, imagePressedButton)) {
          if (button1.isClicked()) {
            if (cuiCurrentPage == firstPage) {
              cuiIgnoreEventsEnd = (new Date()).getTime() + 50; 
                  // ignore events for 50 milliseconds
              cuiCurrentPage = secondPage;          
            }
            else if (cuiCurrentPage == secondPage) {
              cuiIgnoreEventsEnd = (new Date()).getTime() + 50; 
              cuiCurrentPage = thirdPage;   
            }
            cuiRepaint(); 
          }
          return true;
        }
        if (null == event) {
          // draw background
          cuiContext.globalAlpha = 0.3;
          cuiContext.fillRect(0, 0, this.width, this.height);
        }
        return false;  // event has not been processed
      }
      ...


Using Overlay Pages

edit

In order to use a page as an overlay to another page, its processOverlay(event) methods should be called from the process function of that other page. In the example:

      ...
      // first page

      var firstPage = new cuiPage(400, 300, firstPageProcess);

      function firstPageProcess(event) {
        if (overlayPage.processOverlay(event)) {
          return true;
        }
        if (null == event) {
          // draw background
          cuiContext.fillText("First page using landscape format.", 200, 150);
          cuiContext.fillStyle = "#C0C0C0";
          cuiContext.fillRect(0, 0, this.width, this.height);
        }
        return false;  // event has not been processed
      }
      ...

As you might have expected, processOverlay(event) returns true if it has processed the event, and false otherwise.

The other two pages work in exactly the same way.

Uses for Overlays

edit

The main use of overlays are menus, icons, and buttons that always stay in the same place on the screen. However, they can also be used for draggable (or transformable) palettes and toolboxes. Moreover, they are useful for dialog boxes. They can even be used to implement a simple window system (however without scrollable content and without the possibility to change the aspect ratio of the “windows”).

Since they are so versatile, one should ask when not to use them? The main difference of overlays on the one hand and draggables or transformables on the other hand is that overlays are not affected by the transformation of their page. A specific example might be helpful: Imagine an application that presents a zoomable map where the user can click on certain points on the map to get info boxes with additional information about those points. Should these info boxes be overlays or transformables? If they are overlays, the user might quickly clutter the whole screen with info boxes. If they are transformables, the opened info boxes would just move out of sight as the user pans to other parts of the map. However, if the user zooms out, the content of a transformable quickly becomes unreadable while the overlay keeps its readable size. On the other hand, the position of an overlay doesn't tell the user much about the location of the clicked point on the map while a transformable stays close to the clicked point unless the user drags it away. But what if a user wants to compare the content of the info boxes of two points that are far apart? That's much easier with overlays.

Thus, there are multiple advantages and disadvantages of overlays compared to transformables and draggables depending on the specific application. Therefore, the decision for or against overlays has to be made for each specific case.

Implementation of Overlays

edit

The implementation of processOverlay is relatively straightforward. The steps are:

  • Transform the event coordinates since the overlay is usually using a different transformation than the page (for which the event coordinates have been transformed).
  • Set the geometric transformation for cuiContext (again because the page is usually using a different transformation).
  • Call the process function of the overlay page.
  • Process any unprocessed events by the transformable view member of the overlay page.

In code:

/** 
 * Either process the event (if event != null) and return true if the event has been processed, 
 * or draw the page as an overlay (to another page) in the rectangle, which is specified in 
 * window coordinates (if event == null) and return false. This function is usually called
 * by {@link cuiPage.process} of another page.
 * @param {Object} event - An object describing a user event by its "type", coordinates in 
 * window coordinates ("clientX" and "clientY"), an "identifier" for touch events, and optionally
 * "buttons" to specify which mouse buttons are depressed. If null, the function should
 * redraw the overlay page. 
 * @returns {boolean} True if event != null and the event has been processed (implying that 
 * no other GUI elements should process it). False otherwise.
 */ 
cuiPage.prototype.processOverlay = function(event) {
  var orgEvent = event;

  var transform = {scale : 1.0, x : 0.0, y : 0.0};
  this.computeInitialTransformation(transform);

  if (null != orgEvent) {
    event = {clientX : orgEvent.clientX, clientY : orgEvent.clientY, 
      eventX : orgEvent.eventX, eventY : orgEvent.eventY,
      type : orgEvent.type, buttons : orgEvent.buttons, 
      deltaY : orgEvent.deltaY, wheelDelta : orgEvent.wheelDelta};
    this.computeEventCoordinates(event, transform); // set event coordinates for our transformation
  }
  if (null == orgEvent) {
    cuiContext.save();
    this.setPageTransformation(transform); // set our transformation in cuiContext
  }
  var flag = this.process(event); // call our process function
  
  if (!flag && null != event && (this.interactionBits != cuiConstants.none)) { 
    // event hasn't been processed 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()
    
    var oldTranslationX = this.view.translationX;
    var oldTranslationY = this.view.translationY;
    var oldFlag = this.view.isProcessingOuterEvents;
    this.view.isProcessingOuterEvents = false; // don't let a page process outer events if it is an overlay

    if (this.view.process(event, transform.x, transform.y, this.width * transform.scale, 
      this.height * transform.scale,
      null, null, null, null, null, this.interactionBits)) {
      flag = true;
      // enforce interaction constraints with page
      if (cuiConstants.isLimitedToVerticalDragging & this.interactionBits) {
        this.view.translationX = oldTranslationX;
      }
      if (cuiConstants.isLimitedToHorizontalDragging & this.interactionBits) {
        this.view.translationY = oldTranslationY;
      }
    }
    this.view.isProcessingOuterEvents = oldFlag; // restore page's setting
  }

  if (null == orgEvent) { 
    cuiContext.restore();
  }
  return flag;
}


< Canvas 2D Web Apps

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