Canvas 2D Web Apps/Transitions

This chapter extends the chapter on pages by adding animated transition effects between pages (“transitions” for short). To this end, it also relies on the animation system introduced in the chapter on animations.

The implementation is encapsulated in a function with many arguments. Thus, on the one hand, it is possible to implement a wide range of transitions just by changing the arguments without even looking at the implementation of the function; on the other hand, it might be difficult to understand the meaning of all the arguments and how to use them to implement certain effects. Therefore, a list of 24 implementations of popular transitions is included below. Furthermore, a few guidelines about how to design and choose transitions in web apps are also presented.

The Example

edit

The example of this chapter (which is available online; also as downloadable version) adds four animated transition effects to the transitions between three pages. The following sections will discuss how to use the functions to create these transitions and how these functions were implemented. See the chapters on pages, animations and responsive buttons for other parts.

<!DOCTYPE HTML>
<html>
  <head>
    <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()) {
            cuiPlayTransition(this, secondPage, true, false, 0, 0, 0.33, // page turn
              -1.2, 0.1, 0.2, 1.1, 5, 1.0,
              0, 0, 1, 1, 0, 0.8);
           }
          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()) {
            cuiPlayTransition(this, firstPage, false, false, 0, 0, 0.33, // page turn
              0, 0, 1, 1, 0, 0.8,
              -1.2, 0.1, 0.2, 1.1, 5, 1.0);
          }
          return true;
        }
        if (button2.process(event, 300, 50, 80, 50, "next",
          imageNormalButton, imageFocusedButton, imagePressedButton)) {
          if (button2.isClicked()) {
            var startPoint = {x: 300 + 40, y: 50 + 25}; // start point for maximization
            this.transformPageToTransitionCoordinates(startPoint);
            cuiPlayTransition(this, thirdPage, false, false, 1, 0, 0.25, // maxmize
              0.0, 0.0, 1.0, 1.0, 0, 0.8,
              startPoint.x, startPoint.y, 0.1, 0.1, -5, 1.0);
          }
          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()) {
            var targetPoint = {x: 300 + 40, y: 50 + 25}; // target point for minimization
            secondPage.transformPageToTransitionCoordinates(targetPoint);
            cuiPlayTransition(this, secondPage, true, false, 0, 1, 0.3, // minimize
              targetPoint.x, targetPoint.y, 0.1, 0.1, 5, 1.0,
              0.0, 0.0, 1.0, 1.0, 0, 0.8);
         }
          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>

Using Transitions

edit

In order to initiate a transition between two pages, the function cuiPlayTransition() has to be called. In the example, an animated transition between the first and the second page is started this way:

        ...
        if (button0.process(event, 300, 50, 80, 50, "next",
          imageNormalButton, imageFocusedButton, imagePressedButton)) {
          if (button0.isClicked()) {
            cuiPlayTransition(this, secondPage, true, false, 0, 0, 0.33, // page turn
              -1.2, 0.1, 0.2, 1.1, 5, 1.0,
              0, 0, 1, 1, 0, 0.8);
           }
          return true; 
        }
        ...

The appearance of the transition is completely controlled be the 19 arguments of cuiPlayTransition(), which are discussed in the next section.

Configuring Transitions

edit

The function cuiPlayTransition(previousPage, nextPage, isPreviousOverNext, isFrontMaskAnimated, animationInitialSpeed, animationFinalSpeed, animationLength, previousFinalPositionX, previousFinalPositionY, previousFinalScaleX, previousFinalScaleY, previousFinalRotation, previousFinalOpacity, nextInitialPositionX, nextInitialPositionY, nextInitialScaleX, nextInitialScaleY, nextInitialRotation, nextInitialOpacity) configures everything about a transition — including the timing and the appearance of the transition. The arguments are:

  • previousPage: the cuiPage object of the previous page; the transition is from the previous page to the next page
  • nextPage: the cuiPage object of the next page
  • isPreviousOverNext: whether the previousPage is rendered over the nextPage or vice versa; one possibility has to be chosen for the whole transition
  • isFrontMaskAnimated: whether an opacity mask of the page in front is animated instead of the page itself (if isPreviousOverNext is true then previousPage is the page in front, otherwise it is nextPage); animation of an opacity mask is useful mainly for wipe transitions
  • animationInitialSpeed: initial speed of change of the animated values at the beginning of the transition; 0.0 for slow start, 1.0 for linear interpolation, larger values for faster start; since a cubic Hermite curve is used for interpolation, larger values than about 3 can result in overshoots
  • animationFinalSpeed: final speed of change of the animated values at the end of the transition (see animationInitialSpeed)
  • animationLength: duration of the transition in seconds (usually, this should be at most 0.25; in some cases 0.33 might be OK; longer durations are likely to annoy at least expert users on mobile devices)
  • arguments specifying the final appearance of the previousPage at the end of the transition (the initial appearance is always the same):
    • previousFinalPositionX and previousFinalPositionY: final position of the center of previousPage where the left/top edge of the screen is specified by -1 and the right/bottom edge by +1, thus, the center of the screen is at 0. (The initial position is always (0,0); i.e., previousPage is initially centered.)
    • previousFinalScaleX and previousFinalScaleY: final scale factors for width and height of previousPage. (The initial scaling is always 1; i.e., initially there is no scaling.)
    • previousFinalRotation: final clockwise rotation of previousPage in degrees. (The initial rotation is always 0; i.e., there is no rotation.)
    • previousFinalOpacity: final opacity of previousPage. (The initial opacity is always 1; i.e., the page is completely opaque.)
  • arguments specifying the initial appearance of the nextPage at the beginning of the transition (the final appearance is always the same):
    • nextInitialPositionX and nextInitialPositionY: initial position of the center of nextPage where the left/top edge of the screen is specified by -1 and the right/bottom edge by +1, thus, the center of the screen is at 0. (The final position is always (0,0); i.e., nextPage is finally centered.)
    • nextInitialScaleX and nextInitialScaleY: initial scale factors for width and height of nextPage. (The final scaling is always 1; i.e., initially there is no scaling.)
    • nextInitialRotation: initial clockwise rotation of nextPage in degrees. (The final rotation is always 0; i.e., there is no rotation.)
    • nextInitialOpacity: initial opacity of nextPage. (The final opacity is always 1; i.e., the page is completely opaque.)

Since it is not easy to find suitable values, the following list contains examples of calls to cuiPlayTransition() for popular transition effects. Each effect has a reverse effect, which should be used for the transition between the same pages in the opposite order:

// push to left (reverse of push to right)
              cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
                -2.0, 0.0, 1.0, 1.0, 0, 1.0,
                +2.0, 0.0, 1.0, 1.0, 0, 1.0);

// push to right (reverse of push to left)
              cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
                +2.0, 0.0, 1.0, 1.0, 0, 1.0,
                -2.0, 0.0, 1.0, 1.0, 0, 1.0);

// push down (reverse of push to up)
              cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
                0.0, -2.0, 1.0, 1.0, 0, 1.0,
                0.0, +2.0, 1.0, 1.0, 0, 1.0);

// push up (reverse of push down)
              cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
                0.0, +2.0, 1.0, 1.0, 0, 1.0,
                0.0, -2.0, 1.0, 1.0, 0, 1.0);

// cover from top to bottom (reverse of uncover from bottom to top)
              cuiPlayTransition(firstPage, secondPage, false, false, 2, 0, 0.25,
                0.0, +0.0, 1.0, 1.0, 0, 1.0,
                0.0, -2.0, 1.0, 1.0, 0, 1.0);

// uncover from bottom to top (reverse of cover from top to bottom)
              cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
                 0.0, -2.0, 1.0, 1.0, 0, 1.0,
                 0.0, +0.0, 1.0, 1.0, 0, 1.0);

// cover from bottom to top (reverse of uncover from top to bottom)
              cuiPlayTransition(firstPage, secondPage, false, false, 2, 0, 0.25,
                0.0, -0.0, 1.0, 1.0, 0, 1.0,
                0.0, +2.0, 1.0, 1.0, 0, 1.0);

// uncover from top to bottom (reverse of cover from bottom to top)
              cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
                 0.0, +2.0, 1.0, 1.0, 0, 1.0,
                 0.0, -0.0, 1.0, 1.0, 0, 1.0);

// cover from left to right (reverse of uncover from right to left)
              cuiPlayTransition(firstPage, secondPage, false, false, 2, 0, 0.25,
                +0.0, 0.0, 1.0, 1.0, 0, 1.0,
                -2.0, 0.0, 1.0, 1.0, 0, 1.0);

// uncover from right to left (reverse of cover from left to right)
              cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
                -2.0, 0.0, 1.0, 1.0, 0, 1.0,
                 0.0, 0.0, 1.0, 1.0, 0, 1.0);

// cover from right to left (reverse of uncover from left to right)
              cuiPlayTransition(firstPage, secondPage, false, false, 2, 0, 0.25,
                +0.0, 0.0, 1.0, 1.0, 0, 1.0,
                +2.0, 0.0, 1.0, 1.0, 0, 1.0);

// uncover from left to right (reverse of cover from right to left)
              cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
                +2.0, 0.0, 1.0, 1.0, 0, 1.0,
                 0.0, 0.0, 1.0, 1.0, 0, 1.0);

// page turn uncovering from right to left (reverse of page turn covering from left to right)
              cuiPlayTransition(firstPage, secondPage, true, false, 0, 0, 0.33,
                -1.2, 0.0, 0.2, 1.1, 5, 1.0,
                0, 0, 1, 1, 0, 0.8);

// page turn covering from left to right (reverse of page turn uncovering from left to right)
              cuiPlayTransition(firstPage, secondPage, false, false, 0, 0, 0.33,
                0, 0, 1, 1, 0, 0.8,
                -1.2, 0.0, 0.2, 1.1, -5, 1.0);

// maximize (reverse of minimize)
              var startPoint = {x: 300 + 40, y: 50 + 25}; // start point for maximization
              firstPage.transformPageToTransitionCoordinates(startPoint);
              cuiPlayTransition(firstPage, secondPage, false, false, 1, 0, 0.25,
                0.0, 0.0, 1.0, 1.0, 0, 0.8,
              startPoint.x, startPoint.y, 0.1, 0.1, -5, 1.0);

// minimize (reverse of maximize)
              var targetPoint = {x: 300 + 40, y: 50 + 25}; // target point for minimization
              secondPage.transformPageToTransitionCoordinates(targetPoint);
              cuiPlayTransition(firstPage, secondPage, true, false, 0, 1, 0.3, 
                targetPoint.x, targetPoint.y, 0.1, 0.1, 5, 1.0,
                0.0, 0.0, 1.0, 1.0, 0, 0.8);

// dissolve (reverse of itself)
              cuiPlayTransition(firstPage, secondPage, true, false, 0, 0, 0.33,
                0.0, 0.0, 1.0, 1.0, 0, 0.0,
                0.0, 0.0, 1.0, 1.0, 0, 1.0);

// fade through black (reverse of itself)
              cuiPlayTransition(firstPage, secondPage, true, false, 1, 1, 0.33,
                0.0, 0.0, 1.0, 1.0, 0, -1.0,
                0.0, 0.0, 1.0, 1.0, 0, -1.0);

// materialize from air (reverse of dissolve into air)
              cuiPlayTransition(firstPage, secondPage, true, false, 0, 0, 0.33,
                0.0, 0.0, 1.0, 1.0, 0, 0.0,
                0.0, 0.0, 2.0, 2.0, 0, 1.0);

// dissolve into air (reverse of materialize from air)
              cuiPlayTransition(firstPage, secondPage, true, false, 0, 0, 0.33,
                 0.0, 0.0, 2.0, 2.0, 0, 0.0,
                 0.0, 0.0, 1.0, 1.0, 0, 1.0);

// wipe from left to right (reverse of wipe from right to left)
              cuiPlayTransition(firstPage, secondPage, true, true, 1, 1, 0.25,
                2.0, 0.0, 1.0, 1.0, 0, 1.0,
                0.0, 0.0, 1.0, 1.0, 0, 1.0);

// wipe from right to left (reverse of wipe from left to right)
              cuiPlayTransition(firstPage, secondPage, true, true, 1, 1, 0.25,
                -2.0, 0.0, 1.0, 1.0, 0, 1.0,
                0.0, 0.0, 1.0, 1.0, 0, 1.0);

// wipe from top to bottom (reverse of wipe from bottom to top)
              cuiPlayTransition(firstPage, secondPage, true, true, 1, 1, 0.25,
                0.0, 2.0, 1.0, 1.0, 0, 1.0,
                0.0, 0.0, 1.0, 1.0, 0, 1.0);

// wipe from bottom to top (reverse of wipe from top to bottom)
              cuiPlayTransition(firstPage, secondPage, true, true, 1, 1, 0.25,
                0.0, -2.0, 1.0, 1.0, 0, 1.0,
                0.0, 0.0, 1.0, 1.0, 0, 1.0);

The maximize and minimize transition should use the method transformPageToTransitionCoordinates() of cuiPage to compute suitable coordinates for cuiPlayTransition() from the (page) coordinates that are used to position buttons etc.

Many of the transitions and their names might be familiar to you from software for slide presentations. Actually, the included transitions tend to be the more subtle transitions offered by this kind of software. Most apps on mobile devices, on the other hand, rely almost exclusively on this kind of transitions. Some of the reasons for this are discussed in the next section about guidelines for choosing and designing transitions.

Choosing and Designing Transitions

edit

It is easy to annoy users with transitions by deterring, disorienting, and/or distracting users. Make sure you don't. At the very least, users should prefer your transition compared to instantly cutting from one page to another. (This is not as easy as it might sound.) Here is a check list to avoid annoying transitions:

  1. make them quick: don't deter users from whatever they want to do on the next page (there has to be a good reason if a transition takes longer than about 1/4 second; there has to be a very good reason (e.g., you are sure the user wants it) if a transition takes longer than about 1/3 second; make sure to test your transition on the smallest applicable display because the smaller size will result in lower physical velocities; thus, the transition will appear slower)
  2. make them consistent: don't disorient users by
    • inconsistencies between transitions of the same type (e.g. transitions to the next (or previous) page in a list, transitions to a page on a lower (or higher) level of a hierarchical structure, transitions to (or from) dialog boxes, etc.)
    • inconsistencies between the transition from page A to page B and the transition from page B back to page A (they should be visually reversed unless a transition is its own reverse transition, e.g. dissolve)
    • inconsistencies between a transition and the position of the user action that activated it (e.g. if a click or touch on a graphical element at the right edge of the screen activated the transition, then the main direction of the transition should be from right to left — whatever kind of transition is used)
    • inconsistencies between a transition and the gesture that activated it (e.g. a flick in one direction should result in a transition in the same direction without ease-in but with ease-out)
  3. make them subtle: don't distract users from the content of the pages (which should be more important than any transition); remember that users see the same transitions over and over again (at least if you use them consistently); thus, transitions can grow old very quickly even if they were fun to watch the first 40 times.

This is just to avoid annoying transitions that deter, disorient, and/or distract users. In addition, good transitions should be meaningful and communicate information:

4. use transitions to communicate as much as possible:
  • communicate the relation between the pair of pages (e.g. use push transitions in appropriate directions in order to communicate that two pages are neighbors in a list)
  • communicate the type of one or both pages (e.g. reserve cover from top to bottom for notifications or dialog boxes)
  • communicate the structure in which the pages are organised (e.g. use push up/down to move on the same level in a hierarchy and push left/right to move up or down in the hierarchy)
  • communicate the function of pages (e.g. use transitions from film (in particular dissolve and wipe) for telling a story; or use maximize to provide detail information about a certain location)
  • communicate the type and position of the user action that has activated the transition (e.g. use a push transition in a similar direction as the flick gesture that activated it)

And as always with design guidelines: break them if you have a good reason.

Implementing Transitions

edit

This section discusses the implementation of the two functions cuiPlayTransition() and cuiDrawTransition() in cui2d.js. cuiDrawTransition() is called by the process function of a special page in the global variable cuiPageForTransitions. The function cuiPlayTransition() performs four main tasks:

  1. If necessary, it creates a canvas for the previous page and a canvas for the next page.
  2. Then it stores a snapshot of the previous page in one canvas and a snapshot of the next page in the other canvas.
  3. Furthermore, it sets the properties of the global variable cuiAnimationForTransitions for the animated transition.
  4. Lastly, it starts the animation, specifies to ignore events until its end, and requests a repaint of the canvas.

To understand the code, you should know that the 12 animated values in the 2 keyframes are: previousPositionX, previousPositionY, previousScaleX, previousScaleY, previousRotation, previousOpacity, nextPositionX, nextPositionY, nextScaleX, nextScaleY, nextRotation, nextOpacity. The values of the previous page of the 1st keyframe and the values of the next page of the 0th keyframe are specified by the user while the rest are default values, which specify that the previous page starts without any changes in the 0th keyframe and the next page ends without any changes in the 1st keyframe. The code is:

/**
 * Play a transition between two pages.
 * @param {cuiPage} previousPage - The initial page for the transition.
 * @param {cuiPage} nextPage - The final page for the transition.
 * @param {boolean} isPreviousOverNext - Whether to draw previousPage over nextPage.
 * @param {boolean} isFrontMaskAnimated - Whether to animate only an opacity mask of the page in front.
 * @param {number} animationInitialSpeed - 0 for zero initial speed, 1 for linear interpolation, other values scale the speed.
 * @param {number} animationFinalSpeed - 0 for zero final speed, 1 for linear interpolation, other values scale the speed.
 * @param {number} animationLength - Length of the transition in seconds. 
 * @param {number} previousFinalPositionX - Final x position of the previous page (-1/+1: centered on left/right edge). 
 * @param {number} previousFinalPositionY - Final y position of the previous page (-1/+1: centered on top/bottom edge). 
 * @param {number} previousFinalScaleX - Final x scale of the previous page (1: no scaling). 
 * @param {number} previousFinalScaleY - Final y scale of the previous page (1: no scaling). 
 * @param {number} previousFinalRotation - Final rotation in degrees of the previous page (0: no rotation). 
 * @param {number} previousFinalOpacity - Final opacity of the previous page (0: transparent, 1: opaque). 
 * @param {number} nextInitialPositionX - Initial x position of the next page (-1/+1: centered on left/right edge). 
 * @param {number} nextInitialPositionY - Initial y position of the next page (-1/+1: centered on top/bottom edge). 
 * @param {number} nextInitialScaleX - Initial x scale of the next page (1: no scaling). 
 * @param {number} nextInitialScaleY - Initial y scale of the next page (1: no scaling). 
 * @param {number} nextInitialRotation - Initial rotation in degrees of the next page (0: no rotation). 
 * @param {number} nextInitialOpacity - Initial opacity of the next page (0: transparent, 1: opaque). 
 */
function cuiPlayTransition(
  previousPage, nextPage, isPreviousOverNext, isFrontMaskAnimated,
  animationInitialSpeed, animationFinalSpeed, animationLength,
  previousFinalPositionX, previousFinalPositionY,
  previousFinalScaleX, previousFinalScaleY,
  previousFinalRotation, previousFinalOpacity,
  nextInitialPositionX, nextInitialPositionY,
  nextInitialScaleX, nextInitialScaleY,
  nextInitialRotation, nextInitialOpacity)
{ 
  // if necessary, create previousCanvas and nextCanvas
  if (null == cuiAnimationForTransitions.previousCanvas) {
    cuiAnimationForTransitions.previousCanvas = document.createElement("canvas");
  }
  if (null == cuiAnimationForTransitions.nextCanvas) {
    cuiAnimationForTransitions.nextCanvas = document.createElement("canvas");
  }

  // draw previousCanvas and nextCanvas

  // save current animations state and make sure the render loop doesn't render now
  var tempCanvas = cuiCanvas;
  var tempAnimationsArePlaying = cuiAnimationsArePlaying;
  var tempAnimationsEnd = cuiAnimationsEnd;
  cuiAnimationsArePlaying = false;
  cuiAnimationsEnd = 0; 

  // draw previous page into previousCanvas
  var previousCanvas = cuiAnimationForTransitions.previousCanvas;
  cuiCurrentPage = previousPage;
  cuiCanvas = previousCanvas;
  cuiContext = previousCanvas.getContext("2d");
  cuiProcess(null);

  // draw next page into nextCanvas
  var nextCanvas = cuiAnimationForTransitions.nextCanvas;
  cuiCurrentPage = nextPage;
  cuiCanvas = nextCanvas;
  cuiContext = nextCanvas.getContext("2d");
  cuiProcess(null);

  // restore cui state
  cuiCanvas = tempCanvas;
  cuiContext = cuiCanvas.getContext("2d");
  cuiCurrentPage = cuiPageForTransitions;
  cuiAnimationsArePlaying = tempAnimationsArePlaying; // restore animations state
  cuiAnimationsEnd = tempAnimationsEnd; // restore animations state

  // set cuiAnimationForTransitions
  var transitionKeyframes = [
    {time : 0.00, out : -animationInitialSpeed,
     values : [
      0.0, 0.0, 1.0, 1.0, 0.0, 1.0, // previous page initial values
      nextInitialPositionX, nextInitialPositionY,
      nextInitialScaleX, nextInitialScaleY,
      nextInitialRotation, nextInitialOpacity
    ]}, 
    {time : 1.00, in : -animationFinalSpeed,
     values : [
      previousFinalPositionX, previousFinalPositionY,
      previousFinalScaleX, previousFinalScaleY,
      previousFinalRotation, previousFinalOpacity,
      0.0, 0.0, 1.0, 1.0, 0.0, 1.0 // next page final values
    ]}
  ];
  cuiAnimationForTransitions.nextPage = nextPage;
  cuiAnimationForTransitions.isPreviousOverNext = isPreviousOverNext;
  cuiAnimationForTransitions.isFrontMaskAnimated = isFrontMaskAnimated;
  cuiAnimationForTransitions.keyframes = transitionKeyframes;
  cuiAnimationForTransitions.stretch = animationLength;
  cuiAnimationForTransitions.play();
  cuiIgnoringEventsEnd = cuiAnimationForTransitions.end;
  cuiRepaint();
}

The actual rendering of the animation is performed by cuiDrawTransition. First, it computes the animated values with animateValues and extracts the 12 animated parameters. Depending on isPreviousOverNext it then either renders the canvas of the previous page over the canvas of the next page or vice versa. (Since we use the composite operation "destination-over" the front page has to be rendered first.) If isFrontMaskAnimated is false, the front page is rendered directly (with drawImage), otherwise an animated mask is rendered (with fillRect) and the static front page is rendered only in the region of the mask by using the composite operation "source-in". (Animated masks are useful for wipe transitions.) The animation of the canvases and the masks is mainly achieved by the animated value of the opacity and by geometric transformations which use the animated values as parameters. Lastly, cuiDrawTransition checks whether the transition is complete and in that case it sets cuiCurrentPage to nextPage and requests a repaint.

/** Draw a frame of the current transition; called by cuiProcess(). */
function cuiDrawTransition() {
  var previousCanvas = cuiAnimationForTransitions.previousCanvas;
  var nextCanvas = cuiAnimationForTransitions.nextCanvas;
  var width = cuiCanvas.width;
  var height = cuiCanvas.height;
  var values = cuiAnimationForTransitions.animateValues();
  var previousPositionX = values[0];
  var previousPositionY = values[1];
  var previousScaleX = values[2];
  var previousScaleY = values[3];
  var previousRotation = values[4];
  var previousOpacity = values[5];
  var nextPositionX = values[6];
  var nextPositionY = values[7];
  var nextScaleX = values[8];
  var nextScaleY = values[9];
  var nextRotation = values[10];
  var nextOpacity = values[11];
  
  if (cuiAnimationForTransitions.isPreviousOverNext) {  
    // first draw previous page then next page
    if (!cuiAnimationForTransitions.isFrontMaskAnimated) { 
      // draw without mask
      cuiContext.globalCompositeOperation = "destination-over";
      cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, previousOpacity));
      cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
      cuiContext.translate((0.5 * previousPositionX + 0.5) * width, 
        (0.5 * previousPositionY + 0.5) * height);
        // translate center as specified
      cuiContext.rotate(previousRotation * Math.PI / 180.0); 
        // rotate around center
      cuiContext.scale(previousScaleX, previousScaleY); 
        // scale image as specified
      cuiContext.translate(-0.5 * width, -0.5 * height); 
        // translate center to origin
      cuiContext.drawImage(previousCanvas, 0, 0, width, height); 
        // draw full size
    } else { // draw with transform mask for wipe transitions
      cuiContext.globalCompositeOperation = "copy";
      cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, previousOpacity));
      cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
      cuiContext.translate((0.5 * previousPositionX + 0.5) * width, 
        (0.5 * previousPositionY + 0.5) * height);
        // translate center as specified
      cuiContext.rotate(previousRotation * Math.PI / 180.0); 
        // rotate around center
      cuiContext.scale(previousScaleX, previousScaleY); 
        // scale image as specified
      cuiContext.translate(-0.5 * width, -0.5 * height); 
        // translate center to origin
      cuiContext.fillStyle = "#000000";
      cuiContext.fillRect(0, 0, width, height); 
        // draw black full-size mask with specified opacity
      cuiContext.globalCompositeOperation = "source-in";
      cuiContext.globalAlpha = 1.0;
      cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
      cuiContext.drawImage(previousCanvas, 0, 0, width, height); 
        // draw canvas without trafo
    }
    // now draw next page under previous page            
    cuiContext.globalCompositeOperation = "destination-over";
      cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, nextOpacity));
    cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
    cuiContext.translate((0.5 * nextPositionX + 0.5) * width, 
      (0.5 * nextPositionY + 0.5) * height);
      // translate center as specified
    cuiContext.rotate(nextRotation * Math.PI / 180.0); 
      // rotate around center
    cuiContext.scale(nextScaleX, nextScaleY); // scale image as specified
    cuiContext.translate(-0.5 * width, -0.5 * height); 
      // translate center to origin
    cuiContext.drawImage(nextCanvas, 0, 0, width, height); 
      // draw full size
  } else { 
    // first draw next page then previous page
    if (!cuiAnimationForTransitions.isFrontMaskAnimated) { 
      // draw without mask
      cuiContext.globalCompositeOperation = "destination-over";
      cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, nextOpacity));
      cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
      cuiContext.translate((0.5 * nextPositionX + 0.5) * width, 
        (0.5 * nextPositionY + 0.5) * height);
        // translate center as specified
      cuiContext.rotate(nextRotation * Math.PI / 180.0); 
        // rotate around center
      cuiContext.scale(nextScaleX, nextScaleY); 
        // scale image as specified
      cuiContext.translate(-0.5 * width, -0.5 * height); 
        // translate center to origin
      cuiContext.drawImage(nextCanvas, 0, 0, width, height); 
        // draw full size
    } else { 
      // draw with transform mask for wipe transitions
      cuiContext.globalCompositeOperation = "copy";
      cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, nextOpacity));
      cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
      cuiContext.translate((0.5 * nextPositionX + 0.5) * width, 
        (0.5 * nextPositionY + 0.5) * height);
        // translate center as specified
      cuiContext.rotate(nextRotation * Math.PI / 180.0); 
        // rotate around center
      cuiContext.scale(nextScaleX, nextScaleY); 
        // scale image as specified
      cuiContext.translate(-0.5 * width, -0.5 * height); 
        // translate center to origin
      cuiContext.fillStyle = "#000000";
      cuiContext.fillRect(0, 0, width, height); 
        // draw black full-size mask with specified opacity
      cuiContext.globalCompositeOperation = "source-in";
      cuiContext.globalAlpha = 1.0;
      cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
      cuiContext.drawImage(nextCanvas, 0, 0, width, height); 
        // draw canvas without trafo
    }
    // now draw previous page under next page            
    cuiContext.globalCompositeOperation = "destination-over";
    cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, previousOpacity));
    cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
    cuiContext.translate((0.5 * previousPositionX + 0.5) * width, 
      (0.5 * previousPositionY + 0.5) * height);
      // translate center as specified
    cuiContext.rotate(previousRotation * Math.PI / 180.0); 
      // rotate around center
    cuiContext.scale(previousScaleX, previousScaleY); 
      // scale image as specified
    cuiContext.translate(-0.5 * width, -0.5 * height); 
      // translate center to origin
    cuiContext.drawImage(previousCanvas, 0, 0, width, height); 
      // draw full size
  }       
  // draw opaque background to avoid any semitransparent colors in the canvas
  cuiContext.globalCompositeOperation = "destination-over";
  cuiContext.globalAlpha = 1.0;
  cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
  cuiContext.fillStyle = "#000000";
  cuiContext.fillRect(0, 0, width, height);

  if (!cuiAnimationForTransitions.isPlaying()) { 
    // transition has finished
    cuiCurrentPage = cuiAnimationForTransitions.nextPage;
    cuiRepaint();
  }
}

If you look at the code, you will notice that there is a lot of repetition, and in fact, it is possible to shorten the code significantly; however, it's left in this long form for the sake of readability.


< Canvas 2D Web Apps

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