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 | edit source]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 | edit source]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 | edit source]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
: thecuiPage
object of the previous page; the transition is from the previous page to the next pagenextPage
: thecuiPage
object of the next pageisPreviousOverNext
: whether thepreviousPage
is rendered over thenextPage
or vice versa; one possibility has to be chosen for the whole transitionisFrontMaskAnimated
: whether an opacity mask of the page in front is animated instead of the page itself (ifisPreviousOverNext
istrue
thenpreviousPage
is the page in front, otherwise it isnextPage
); animation of an opacity mask is useful mainly for wipe transitionsanimationInitialSpeed
: 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 overshootsanimationFinalSpeed
: final speed of change of the animated values at the end of the transition (seeanimationInitialSpeed
)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
andpreviousFinalPositionY
: final position of the center ofpreviousPage
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
andpreviousFinalScaleY
: final scale factors for width and height ofpreviousPage
. (The initial scaling is always 1; i.e., initially there is no scaling.)previousFinalRotation
: final clockwise rotation ofpreviousPage
in degrees. (The initial rotation is always 0; i.e., there is no rotation.)previousFinalOpacity
: final opacity ofpreviousPage
. (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
andnextInitialPositionY
: initial position of the center ofnextPage
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
andnextInitialScaleY
: initial scale factors for width and height ofnextPage
. (The final scaling is always 1; i.e., initially there is no scaling.)nextInitialRotation
: initial clockwise rotation ofnextPage
in degrees. (The final rotation is always 0; i.e., there is no rotation.)nextInitialOpacity
: initial opacity ofnextPage
. (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 | edit source]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:
- 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)
- 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)
- 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 | edit source]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:
- If necessary, it creates a canvas for the previous page and a canvas for the next page.
- Then it stores a snapshot of the previous page in one canvas and a snapshot of the next page in the other canvas.
- Furthermore, it sets the properties of the global variable
cuiAnimationForTransitions
for the animated transition. - 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.