/**
* @file cui2d is a light-weight collection of JavaScript functions for creating
* graphical user interfaces in an HTML5 canvas 2d context.
* Examples are available in the wikibook "Canvas 2D Web Apps"
* ({@link http://en.wikibooks.org/wiki/Canvas_2D_Web_Apps}).
* Comments are formatted for JSDoc 3 (see {@link http://usejsdoc.org}).
* The version number specifies major version, last digit of year, month and day. Quicklinks:
* <ul><li><b>globals:</b>
* <ul><li>methods: {@link cuiInit}, {@link cuiRepaint},
* {@link cuiPlayTransition}, {@link cuiIsInsideRectangle}, {@link cuiFillMultiLineText}
* <li>members: {@link cuiCurrentPage}, {@link cuiContext}, {@link cuiCanvas},
* {@link cuiBackgroundFillStyle}, {@link cuiDefaultFillStyle}, {@link cuiDefaultFont},
* {@link cuiDefaultTextAlign}, {@link cuiDefaultTextBaseline}, {@link cuiIgnoringEventsEnd},
* {@link cuiAnimationStep}, {@link cuiAnimationsArePlaying}, {@link cuiTimeUntilHold}
* </ul>
* <li><b>{@link cuiPage} class:</b>
* <ul><li>methods: {@link cuiPage.process|process}, {@link cuiPage#processOverlay|processOverlay},
* {@link cuiPage#transformPageToTransitionCoordinates|transformPageToTransitionCoordinates},
* {@link cuiPage#computeTranslationToCenterPoint|computeTranslationToCenterPoint}
* <li>members: {@link cuiPage.width|width}, {@link cuiPage.height|height},
* {@link cuiPage.interactionBits|interactionBits},
* {@link cuiPage.isAdjustingWidth|isAdjustingWidth},
* {@link cuiPage.isAdjustingHeight|isAdjustingHeight},
* {@link cuiPage.horizontalAlignment|horizontalAlignment},
* {@link cuiPage.verticalAlignment|verticalAlignment}, {@link cuiPage.view|view}
* </ul>
* <li><b>{@link cuiConstants} class:</b>
* <ul><li>members: {@link cuiConstants.none|none},
* {@link cuiConstants.isDraggableWithOneFinger|isDraggableWithOneFinger},
* {@link cuiConstants.isDraggableWithTwoFingers|isDraggableWithTwoFingers},
* {@link cuiConstants.isRotatableWithTwoFingers|isRotatableWithTwoFingers},
* {@link cuiConstants.isUniformlyScalableWithTwoFingers|isUniformlyScalableWithTwoFingers},
* {@link cuiConstants.isTransformableWithTwoFingers|isTransformableWithTwoFingers},
* {@link cuiConstants.isDraggableWithThreeFingers|isDraggableWithThreeFingers},
* {@link cuiConstants.isRotatableWithThreeFingers|isRotatableWithThreeFingers},
* {@link cuiConstants.isUniformlyScalableWithThreeFingers|isUniformlyScalableWithThreeFingers},
* {@link cuiConstants.isNonUniformlyScalableWithThreeFingers|isNonUniformlyScalableWithThreeFingers},
* {@link cuiConstants.isShearableWithThreeFingers|isShearableWithThreeFingers},
* {@link cuiConstants.isDeformableWithThreeFingers|isDeformableWithThreeFingers},
* {@link cuiConstants.isLimitedToHorizontalDragging|isLimitedToHorizontalDragging},
* {@link cuiConstants.isLimitedToVerticalDragging|isLimitedToVerticalDragging}
* </ul>
* <li><b>{@link cuiButton} class: </b>
* <ul><li>methods: {@link cuiButton#process|process}, {@link cuiButton#isClicked|isClicked},
* {@link cuiButton#isDoubleClicked|isDoubleClicked}, {@link cuiButton#isHeldDown|isHeldDown}
* <li>members: {@link cuiButton.isPointerDown|isPointerDown}, {@link cuiButton.isPointerInside|isPointerInside}
* </ul>
* <li><b>{@link cuiDraggable} class:</b>
* <ul><li>methods: {@link cuiDraggable#process|process}, {@link cuiDraggable#isClicked|isClicked},
* {@link cuiDraggable#isDoubleClicked|isDoubleClicked}, {@link cuiDraggable#isHeldDown|isHeldDown}
* <li>members: {@link cuiDraggable.translationX|translationX}, {@link cuiDraggable.translationY|translationY},
* {@link cuiDraggable.isPointerDown|isPointerDown}, {@link cuiDraggable.isPointerInside|isPointerInside}
* </ul>
* <li><b>{@link cuiTransformable} class: </b>
* <ul><li>methods: {@link cuiTransformable#process|process},
* {@link cuiTransformable#isClicked|isClicked}, {@link cuiTransformable#isDoubleClicked|isDoubleClicked},
* {@link cuiTransformable#isHeldDown0|isHeldDown0}, {@link cuiTransformable#isHeldDown1|isHeldDown1}
* <li>members: {@link cuiTransformable.translationX|translationX}, {@link cuiTransformable.translationY|translationY},
* {@link cuiTransformable.rotation|rotation}, {@link cuiTransformable.scale|scale},
* {@link cuiTransformable.isPointerDown0|isPointerDown0}, {@link cuiTransformable.isPointerDown1|isPointerDown1},
* {@link cuiTransformable.isPointerInside0|isPointerInside0},
* {@link cuiTransformable.isProcessingOuterEvents|isProcessingOuterEvents}
* </ul>
* <li><b>{@link cuiAnimation} class: </b>
* <ul><li>methods: {@link cuiAnimation#play|play}, {@link cuiAnimation#animateValues|animateValues},
* {@link cuiAnimation#isPlaying|isPlaying}, {@link cuiAnimation#stopLooping|stopLooping}
* </ul>
* <li><b>{@link cuiKeyframe} class:</b>
* <ul><li>members: {@link cuiKeyframe.time|time}, {@link cuiKeyframe.in|in}, {@link cuiKeyframe.out|out},
* {@link cuiKeyframe.values|values}
* </ul>
* </ul>
* @version 0.30902
* @license public domain
* @author Martin Kraus <martin@create.aau.dk>
*/
/**
* The canvas element. Set by {@link cuiInit}.
* @type {Object}
*/
var cuiCanvas;
/**
* The 2d context of the canvas element. Set by {@link cuiInit}.
* @type {Object}
*/
var cuiContext;
/**
* Currently displayed page. Can be set to change the page.
* @see cuiIgnoringEventsEnd
* @type {cuiPage}
*/
var cuiCurrentPage;
/**
* Time (in milliseconds after January 1, 1970) when events are no longer ignored.
* Typically set to "(new Date()).getTime() + x"
* where "x" is the number of milliseconds that events are being ignored.
* (This is useful after changing {@link cuiCurrentPage}.)
* @type {number}
*/
var cuiIgnoringEventsEnd;
/**
* Minimum time between frames in milliseconds.
* @type {number}
*/
var cuiAnimationStep = 15;
/**
* Flag indicating whether any animation is playing. Set by {@link cuiAnimation#play}.
* @type {boolean}
*/
var cuiAnimationsArePlaying;
/**
* Time in milliseconds that a pointer has to be held down until a hold event is triggered.
* @see cuiButton#isHeldDown
* @see cuiDraggable#isHeldDown
* @see cuiTransformable#isHeldDown0
* @see cuiTransformable#isHeldDown1
*/
var cuiTimeUntilHold = 500;
/**
* Background color (behind all pages).
* @type {string}
*/
var cuiBackgroundFillStyle = "#000000";
/**
* Default font for all pages.
* @type {string}
*/
var cuiDefaultFont = "bold 20px Helvetica, sans-serif";
/**
* Default horizontal text alignment for all pages.
* @type {string}
*/
var cuiDefaultTextAlign = "center";
/**
* Default vertical text alignment for all pages.
* @type {string}
*/
var cuiDefaultTextBaseline = "middle";
/** Default fill style (e.g. for text color) for all pages. */
var cuiDefaultFillStyle = "#000000";
// Boolean flag for requesting a repaint of the canvas. Set by cuiRepaint; cleared by cuiProcess.
var cuiCanvasNeedsRepaint;
// Time (in milliseconds after January 1, 1970) when the last animation should stop.
// Set by cuiAnimation.play.
var cuiAnimationsEnd;
// The cuiAnimation for all transition effects. Set by cuiInit.
var cuiAnimationForTransitions;
// The cuiPage for all transition effects. Set by cuiInit.
var cuiPageForTransitions;
/**
* Initialize cui2d.
* @param {cuiPage} startPage - The page to display first.
*/
function cuiInit(startPage) {
// IE-specific polyfill which enables the passage of arbitrary arguments to the
// callback functions of javascript timers (HTML5 standard syntax).
// https://developer.mozilla.org/en-US/docs/DOM/window.setInterval
//
if (document.all && !window.setTimeout.isPolyfill) {
var __nativeST__ = window.setTimeout;
window.setTimeout = function (vCallback, nDelay /*, argumentToPass1, argumentToPass2, etc. */) {
var aArgs = Array.prototype.slice.call(arguments, 2);
return __nativeST__(vCallback instanceof Function ? function () {
vCallback.apply(null, aArgs);
} : vCallback, nDelay);
};
window.setTimeout.isPolyfill = true;
}
// actual initialization
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("gesturestart", cuiIgnoreEvent);
document.body.addEventListener("gesturechange", cuiIgnoreEvent);
document.body.addEventListener("gestureend", cuiIgnoreEvent);
document.body.addEventListener("mousedown", cuiMouse);
document.body.addEventListener("mouseup", cuiMouse);
document.body.addEventListener("mousemove", cuiMouse);
document.body.addEventListener("mousewheel", cuiMouse);
document.body.addEventListener("wheel", cuiMouse);
document.body.addEventListener("dblclick", 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();
}
return false;
}
cuiRepaint();
cuiRenderLoop();
}
// Resize handler. Used by cuiInit.
function cuiResize() {
cuiRepaint();
}
// Event handler for ignoring events. Used by cuiInit.
function cuiIgnoreEvent(event) {
event.preventDefault();
}
// Event handler for mouse events. Used by cuiInit.
function cuiMouse(event) {
event.preventDefault();
cuiProcess(event);
}
// Event handler for touch events. Used by cuiInit.
function cuiTouch(event) {
event.preventDefault();
// process all changed touches for the current event individually
var touches = event.changedTouches;
for (var i = 0; i < touches.length; i++) {
event.clientX = touches[i].clientX;
event.clientY = touches[i].clientY;
event.identifier = touches[i].identifier;
cuiProcess(event);
}
}
// Send an internal event to check for a mouse or touch hold condition.
function cuiSendHoldEvent(x, y, id, time) {
var event = { clientX : x, clientY : y, identifier : id, type : "mousehold", timeDown : time };
cuiProcess(event);
}
/** 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)
}
// 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
}
// Either process the event (if event != null) or repaint the current page specified by cuiCurrentPage (if event == null).
// (Is called by cuiRenderLoop.)
function cuiProcess(event) {
// ignore events if necessary
if (null != event && cuiIgnoringEventsEnd > 0) {
if ((new Date()).getTime() < cuiIgnoringEventsEnd) {
return;
}
}
// clear repaint flag
if (null == event) {
cuiCanvasNeedsRepaint = false;
}
var transform = {scale : 1.0, x : 0.0, y : 0.0};
cuiCurrentPage.computeInitialTransformation(transform);
cuiCurrentPage.computeEventCoordinates(event, transform);
// initialize drawing
if (null == event) {
cuiCanvas.width = window.innerWidth;
cuiCanvas.height = window.innerHeight;
// The following line is not necessary because we set the canvas size:
// cuiContext.clearRect(0, 0, cuiCanvas.width, cuiCanvas.height);
// Some people recommend to avoid setting the canvas size every frame,
// but I had trouble with rendering a transition effect on Firefox without it.
cuiCurrentPage.setPageTransformation(transform);
cuiContext.globalCompositeOperation = "destination-over";
cuiContext.globalAlpha = 1.0;
cuiContext.font = cuiDefaultFont;
cuiContext.textAlign = cuiDefaultTextAlign;
cuiContext.textBaseline = cuiDefaultTextBaseline;
cuiContext.fillStyle = cuiDefaultFillStyle;
}
if (!cuiCurrentPage.process(event) && cuiCurrentPage != cuiPageForTransitions && null != event) {
// event hasn't been processed, not a transition, and we have an event?
event.eventX = event.clientX; // we don't need any transformation here because the initial ...
event.eventY = event.clientY; // ... transformation is applied to the arguments of ...
// ... view.process() and the transformation in view is applied internally in view.process()
var oldTranslationX = cuiCurrentPage.view.translationX;
var oldTranslationY = cuiCurrentPage.view.translationY;
if (cuiCurrentPage.view.process(event, transform.x, transform.y, cuiCurrentPage.width * transform.scale,
cuiCurrentPage.height * transform.scale, null, null, null, null, null, cuiCurrentPage.interactionBits)) {
// enforce dragging constraints with page
if (cuiCurrentPage.interactionBits & cuiConstants.isLimitedToVerticalDragging) {
cuiCurrentPage.view.translationX = oldTranslationX;
}
if (cuiCurrentPage.interactionBits & cuiConstants.isLimitedToHorizontalDragging) {
cuiCurrentPage.view.translationY = oldTranslationY;
}
}
}
// draw background behind page
if (null == event) {
cuiContext.globalCompositeOperation = "destination-over";
cuiContext.globalAlpha = 1.0;
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.fillStyle = cuiBackgroundFillStyle;
cuiContext.fillRect(0, 0, cuiCanvas.width, cuiCanvas.height);
}
}
/**
* Write multi-line text where a new line is started with a newline character and a carriage return
* character draws the following line over the previous.
* @param {string} text - The multi-line text.
* @param {number} x - The x coordinate for all lines.
* @param {number} y - The y coordinate for the first line. Further lines use y + n * baselineSkip.
* @param {number} maxWidth - The maximum width of all lines. (If a line is too wide, it is scaled.)
* @param {number} maxHeight - The maximum height. Lines at y coordinates larger than
* maxHeight - baselineSkip are clipped.
* @param {number} baselineSkip - The increment between y coordinates of successive lines.
*/
function cuiFillMultiLineText(text, x, y, maxWidth, maxHeight, baselineSkip) {
var indexLineBreak = -1;
var lineString;
var indexNextLineBreak;
var indexNextNewLine;
var indexNextReturn;
do {
indexNextNewLine = text.indexOf('\n', indexLineBreak + 1);
indexNextReturn = text.indexOf('\r', indexLineBreak + 1);
if (indexNextNewLine >= 0 && (indexNextReturn < 0 || indexNextNewLine < indexNextReturn)) {
indexNextLineBreak = indexNextNewLine;
}
else {
indexNextLineBreak = indexNextReturn;
}
if (indexNextLineBreak >= 0) {
lineString = text.substring(indexLineBreak + 1, indexNextLineBreak);
}
else {
lineString = text.substring(indexLineBreak + 1);
}
cuiContext.fillText(lineString, x, y, maxWidth);
if (indexNextNewLine == indexNextLineBreak) {
y = y + baselineSkip;
}
indexLineBreak = indexNextLineBreak;
} while (indexLineBreak >= 0 && y + baselineSkip < maxHeight);
}
/**
* Either determine whether the event's position is inside a rectangle (if event != null)
* or draw an image in the rectangle with a text string on top of it (if event == null).
* @param {Object} event - An object describing a user event by its "type", coordinates in
* page coordinates ("eventX" and "eventY"), an "identifier" for touch events, and optionally
* "buttons" to specify which mouse buttons are depressed. If null, the function should
* redraw the button.
* @param {number} x - The x coordinate of the top, left corner of the rectangle.
* @param {number} y - The y coordinate of the top, left corner of the rectangle.
* @param {number} width - The width of the rectangle.
* @param {number} height - The height of the rectangle.
* @param {string} text - A text that is written at the center of the rectangle. (May be null).
* @param {Object} image - An image to be drawn inside the rectangle. (May be null.)
* @returns {boolean} True if event != null and the coordinates of the event are
* inside the rectangle, false otherwise.
*/
function cuiIsInsideRectangle(event, x, y, width, height, text, image) {
if (null == event) { // draw button
if (null != text) {
cuiContext.fillText(text, x + width / 2, y + height / 2);
}
if (null != image) {
cuiContext.drawImage(image, x, y, width, height);
}
return false;
}
else { // if (null != event)
if (event.eventX >= x && event.eventX < x + width &&
event.eventY >= y && event.eventY < y + height) {
return true;
}
return false;
}
}
/**
* @class cuiConstants
* @classdesc This class is a collection of constants used in cui2d.
*/
function cuiConstants() {
}
/** Constant for 0, e.g. no bits. */
cuiConstants.none = 0;
/** Bit constant to specify dragging with the mouse or one finger. */
cuiConstants.isDraggableWithOneFinger = 1;
/** Bit constant to specify dragging with two fingers. */
cuiConstants.isDraggableWithTwoFingers = 2;
/** Bit constant to specify rotation with two fingers. */
cuiConstants.isRotatableWithTwoFingers = 4;
/** Bit constant to specify uniform scaling with two fingers */
cuiConstants.isUniformlyScalableWithTwoFingers = 8;
/** Bit constant to specify dragging, rotation and scaling with two fingers. */
cuiConstants.isTransformableWithTwoFingers = 2 + 4 + 8;
/** Currently unused bit constant. */
cuiConstants.isDraggableWithThreeFingers = 16;
/** Currently unused bit constant. */
cuiConstants.isRotatableWithThreeFingers = 32;
/** Currently unused bit constant. */
cuiConstants.isUniformlyScalableWithThreeFingers = 64;
/** Currently unused bit constant. */
cuiConstants.isNonUniformlyScalableWithThreeFingers = 128;
/** Currently unused bit constant. */
cuiConstants.isShearableWithThreeFingers = 256;
/** Currently unused bit constant. */
cuiConstants.isDeformableWithThreeFingers = 16 + 32 + 64 + 129 + 256;
/** Bit constant to specify a limitation to horizontal dragging. */
cuiConstants.isLimitedToHorizontalDragging = 512;
/** Bit constant to specify a limitation to vertical dragging. */
cuiConstants.isLimitedToVerticalDragging = 1024;
/**
* @class cuiPage
* @classdesc Pages are the top-level structure of a cui2d user interface.
* There is always exactly one (proper) page visible but the {@link cuiPage.process} function
* of that page might include multiple overlay pages with {@link cuiPage#processOverlay}.
* {@link cuiPage.process} usually calls other process functions (e.g. {@link cuiButton#process},
* {@link cuiDraggable#process}, {@link cuiTransformable#process}) to include GUI elements in
* the layout of a page.
* Each page has a coordinate system with the origin in the top, left corner and
* x coordinates between 0 and width, and y coordinates between 0 and height.
*
* @desc Create a new cuiPage of specified width and height with the specified process function.
* @param {number} width - The width of the page.
* @param {number} height - The height of the page.
* @param {function} process - The page's process function to process an event
* (with process(event) which should return true to indicate that the event has
* been processed and therefore to prevent the default gestures for manipulating
* pages) and to repaint the page (with process(null) which should always return
* false).
*/
function cuiPage(width, height, process) {
/**
* The width of the page.
* @member {number} cuiPage.width
*/
this.width = width;
/**
* The height of the page.
* @member {number} cuiPage.height
*/
this.height = height;
/**
* The page's process function to process an event
* (with process(event) which should return true to indicate that the event has
* been processed and therefore to prevent the default gestures for manipulating
* pages) and to repaint the page (with process(null) which should always return
* false).
* @member {function} cuiPage.process
*/
this.process = process;
/**
* Flag to specify whether to adjust the width of the page by scaling.
* (By default set to true.)
* @member {boolean} cuiPage.isAdjustingWidth
*/
this.isAdjustingWidth = true;
/**
* Flag to specify whether to adjust the height of the page by scaling.
* (By default set to true.)
* @member {boolean} cuiPage.isAdjustingHeight
*/
this.isAdjustingHeight = true;
/**
* Number to specify the horizontal alignment: -1 for left, 0 for center, +1 for right.
* (By default set to 0.)
* @member {number} cuiPage.horizontalAlignment
*/
this.horizontalAlignment = 0;
/**
* Number to specify the vertical alignment: -1 for top, 0 for center, +1 for bottom.
* (By default set to 0.)
* @member {number} cuiPage.verticalAlignment
*/
this.verticalAlignment = 0;
/**
* Bits to specify the available forms of default interaction with a page.
* Either {@link cuiConstants.none} or a bitwise-or of other constants in {@link cuiConstants}, e.g.
* cuiConstants.isDraggableWithOneFinger | cuiConstants.isTransformableWithTwoFingers.
* @member {number} cuiPage.interactionBits
*/
this.interactionBits = (cuiConstants.isDraggableWithOneFinger | cuiConstants.isTransformableWithTwoFingers);
/**
* The transformable object representing the page's transformation relative to the initial transformation.
* This transformable object usually has its {@link cuiTransformable.isProcessingOuterEvents} property
* set to true.
* @member {cuiTransformable} cuiPage.view
*/
this.view = new cuiTransformable();
this.view.isProcessingOuterEvents = true; // consume events even if outside rectangle
}
// Compute the initial transformation from page coordinates to window coordinates.
// parameter transform - The result is written to this object, which should
// have scale, x and y properties representing a transformation consisting of a
// scaling and a translation.
cuiPage.prototype.computeInitialTransformation = function(transform) {
if ((this.isAdjustingWidth && this.isAdjustingHeight &&
window.innerWidth / this.width < window.innerHeight / this.height) ||
(this.isAdjustingWidth && !this.isAdjustingHeight)) {
// required X scaling is smaller: use it
transform.scale = window.innerWidth / this.width;
transform.x = 0.0; // X is scaled for full window
transform.y = 0.5 * (this.verticalAlignment + 1.0) *
(window.innerHeight - this.height * transform.scale);
// scaling is too small for Y: offset to align page
}
else if (this.isAdjustingHeight) { // required Y scaling is smaller: use it
transform.scale = window.innerHeight / this.height;
transform.x = 0.5 * (this.horizontalAlignment + 1.0) *
(window.innerWidth - this.width * transform.scale);
// scaling is too small for X: offset to align page
transform.y = 0.0;
}
else { // no adjusting of width nor height: just align
transform.scale = 1.0;
transform.x = 0.5 * (this.horizontalAlignment + 1.0) *
(window.innerWidth - this.width * transform.scale);
transform.y = 0.5 * (this.horizontalAlignment + 1.0) *
(window.innerHeight - this.height * transform.scale);
}
}
// Compute event coordinates in page coordinates and write them to event.eventX and event.eventY.
// parameter event - The user invent with coordinates clientX and clientY.
// parameter transform - The transformation computed with cuiPage#computeInitialTransformation.
cuiPage.prototype.computeEventCoordinates = function(event, transform) {
if (null != event) {
// transformation in this.view
var mappedX = event.clientX - this.view.translationX;
var mappedY = event.clientY - this.view.translationY;
mappedX = mappedX - transform.x - 0.5 * this.width * transform.scale;
mappedY = mappedY - transform.y - 0.5 * this.height * transform.scale;
var angle = -this.view.rotation * Math.PI / 180.0;
var tempX = Math.cos(angle) * mappedX - Math.sin(angle) * mappedY;
mappedY = Math.sin(angle) * mappedX + Math.cos(angle) * mappedY;
mappedX = tempX / this.view.scale;
mappedY = mappedY / this.view.scale;
mappedX = mappedX + transform.x + 0.5 * this.width * transform.scale;
mappedY = mappedY + transform.y + 0.5 * this.height * transform.scale;
// initial transformation for fitting the page into the window
event.eventX = (mappedX - transform.x) / transform.scale;
event.eventY = (mappedY - transform.y) / transform.scale;
}
}
// Set the transformation from page coordinates to window coordinates in cuiContext.
// parameter transform - The transformation computed with cuiPage.computeInitialTransformation.
cuiPage.prototype.setPageTransformation = function(transform) {
// transformation in this.view
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.translate(this.view.translationX, this.view.translationY);
cuiContext.translate(transform.x + 0.5 * this.width * transform.scale,
transform.y + 0.5 * this.height * transform.scale);
cuiContext.rotate(this.view.rotation * Math.PI / 180.0);
cuiContext.scale(this.view.scale, this.view.scale);
cuiContext.translate(-transform.x - 0.5 * this.width * transform.scale,
-transform.y - 0.5 * this.height * transform.scale);
// initial transformation for fitting the page into the window
cuiContext.translate(transform.x, transform.y);
cuiContext.scale(transform.scale, transform.scale);
}
/**
* Transforms the coordinates point.x and point.y from page coordinates to
* the transition coordinate system (with coordinates between -1 and +1).
* @see cuiPlayTransition
* @param {Object} point - An object with an x and y property representing a 2D point.
*/
cuiPage.prototype.transformPageToTransitionCoordinates = function(point) {
var transform = {scale : 1.0, x : 0.0, y : 0.0};
this.computeInitialTransformation(transform);
// initial transformation for fitting the page into the window
var x = point.x * transform.scale + transform.x;
var y = point.y * transform.scale + transform.y;
// transformation in this.view
x = (x - transform.x - 0.5 * this.width * transform.scale) * this.view.scale;
y = (y - transform.y - 0.5 * this.height * transform.scale) * this.view.scale;
var angle = this.view.rotation * Math.PI / 180.0;
var tempX = Math.cos(angle) * x - Math.sin(angle) * y;
y = Math.sin(angle) * x + Math.cos(angle) * y;
x = tempX + transform.x + 0.5 * this.width * transform.scale + this.view.translationX;
y = y + transform.y + 0.5 * this.height * transform.scale + this.view.translationY;
// transformation from window coordinates to transition coordinates
point.x = 2.0 * x / window.innerWidth - 1.0;
point.y = 2.0 * y / window.innerHeight - 1.0;
}
/**
* Compute the values for {@link cuiTransformable.translationX} and
* {@link cuiTransformable.translationY} of {@link cuiPage.view}
* to center the point (point.x, point.y) on the screen and
* write these values to point.x and point.y.
* @param {Object} point - An object with an x and y property representing a 2D point.
*/
cuiPage.prototype.computeTranslationToCenterPoint = function(point) {
var transform = {scale : 1.0, x : 0.0, y : 0.0};
this.computeInitialTransformation(transform);
// compute where point (in page coordinates) is mapped to in window coordinates
// with the current transformation
var mappedX = point.x * transform.scale + transform.x;
var mappedY = point.y * transform.scale + transform.y;
mappedX = mappedX - transform.x - 0.5 * this.width * transform.scale;
mappedY = mappedY - transform.y - 0.5 * this.height * transform.scale;
mappedX = mappedX * this.view.scale;
mappedY = mappedY * this.view.scale;
var angle = this.view.rotation * Math.PI / 180.0;
var tempX = Math.cos(angle) * mappedX - Math.sin(angle) * mappedY;
mappedY = Math.sin(angle) * mappedX + Math.cos(angle) * mappedY;
mappedX = tempX + transform.x + 0.5 * this.width * transform.scale;
mappedY = mappedY + transform.y + 0.5 * this.height * transform.scale;
mappedX = mappedX + this.view.translationX;
mappedY = mappedY + this.view.translationY;
// (x,y) should be mapped to (0.5 * window.innerWidth, 0.5 * window.innerHeight)
// we compute the require translation such that it ends up there
point.x = this.view.translationX + 0.5 * window.innerWidth - mappedX;
point.y = this.view.translationY + 0.5 * window.innerHeight - mappedY;
}
/**
* 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;
}
/**
* @class cuiButton
* @classdesc Buttons are clickable rectangular regions.
*
* @desc Create a new cuiButton.
*/
function cuiButton() {
/**
* Flag specifying whether a mouse button or finger is inside the button's rectangle.
* @member {boolean} cuiButton.isPointerInside
*/
this.isPointerInside = false;
/**
* Flag specifying whether a mouse button or finger is pushing the button or has been
* pushing the button and is still held down (but may have moved outside the button's
* rectangle).
* @member {boolean} cuiButton.isPointerDown
*/
this.isPointerDown = false;
this.identifier = -1; // the identifier of the touch point
this.hasTriggeredClick = false; // click event has been triggered?
this.hasTriggeredDoubleClick = false; // double click event has been triggered?
this.hasTriggeredHold = false; // hold event has been triggered?
}
/**
* Determine whether the button has just been clicked.
* @returns {boolean} True if the button has been clicked, false otherwise.
*/
cuiButton.prototype.isClicked = function() {
return this.hasTriggeredClick;
}
/**
* Determine whether the button has just been double clicked.
* @returns {boolean} True if the button has been double clicked, false otherwise.
*/
cuiButton.prototype.isDoubleClicked = function() {
return this.hasTriggeredDoubleClick;
}
/**
* Determine whether the button has just been held longer than {@link cuiTimeUntilHold}.
* @returns {boolean} True if the button has just been held down long enough, false otherwise.
*/
cuiButton.prototype.isHeldDown = function() {
return this.hasTriggeredHold;
}
/**
* Either process the event (if event != null) and return true if the event has been processed,
* or draw the appropriate image for the button state in the rectangle
* with a text string on top of it (if event == null) and return false.
* This function is usually called by {@link cuiPage.process} of a {@link cuiPage}.
* @param {Object} event - An object describing a user event by its "type", coordinates in
* page coordinates ("eventX" and "eventY"), an "identifier" for touch events, and optionally
* "buttons" to specify which mouse buttons are depressed. If null, the function should
* redraw the button.
* @param {number} x - The x coordinate of the top, left corner of the button's rectangle.
* @param {number} y - The y coordinate of the top, left corner of the button's rectangle.
* @param {number} width - The width of the button's rectangle.
* @param {number} height - The height of the button's rectangle.
* @param {string} text - A text that is written at the center of the rectangle. (May be null).
* @param {Object} imageNormal - An image to be drawn inside the button's rectangle if there
* are no user interactions. (May be null.)
* @param {Object} imageFocused - An image to be drawn inside the button's rectangle if the
* mouse hovers over the button's rectangle or a touch point moves into it. (May be null.)
* @param {Object} imagePressed - An image to be drawn inside the button's rectangle if a
* mouse button is pushed or the button is touched. (May be null.)
* @returns {boolean} True if event != null and the event has been processed (implying that
* no other GUI elements should process it). False otherwise.
*/
cuiButton.prototype.process = function(event, x, y, width, height,
text, imageNormal, imageFocused, imagePressed) {
// choose appropriate image
var image = imageNormal;
if (this.isPointerDown && this.isPointerInside) {
image = imagePressed;
}
else if (this.isPointerDown || this.isPointerInside) {
image = imageFocused;
}
// check or repaint button
var isIn = cuiIsInsideRectangle(event, x, y, width, height, text, image);
// note that the event might be inside the rectangle (isIn == true)
// but the state might still be isPointerDown == false (e.g. for touchend or
// touchcancel or if the pointer went down outside of the button)
// react to event
if (null == event) {
return false; // no event to process
}
// clear trigger flags (they are set only once after the event and have to be cleared afterwards)
this.hasTriggeredClick = false;
this.hasTriggeredDoubleClick = false;
this.hasTriggeredHold = false;
// process double click events
if ("dblclick" == event.type) {
this.hasTriggeredDoubleClick = isIn;
return isIn;
}
// process our hold events
if ("mousehold" == event.type) {
if (event.timeDown == this.timeDown && event.identifier == this.identifier &&
this.isPointerInside && this.isPointerDown) {
this.hasTriggeredHold = true;
return true;
}
return false;
}
// process wheel events
if ("wheel" == event.type || "mousewheel" == event.type) {
return isIn; // give directly to caller
}
// ignore mouse or touch points that are not the tracked point (apart from mousedown and touchstart)
if (this.isPointerInside || this.isPointerDown) {
if ("touchend" == event.type || "touchmove" == event.type || "touchcancel" == event.type) {
if (event.identifier != this.identifier) {
return false; // ignore all other touch points except "touchstart" events
}
}
else if (("mousemove" == event.type || "mouseup" == event.type) && event.identifier >= 0) {
return false; // ignore mouse (except mousedown) if we are tracking a touch point
}
}
// state changes
if (!this.isPointerInside && !this.isPointerDown) { // passive button state
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
this.isPointerDown = true;
this.isPointerInside = true;
if ("touchstart" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
this.timeDown = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier, this.timeDown);
cuiRepaint();
return true;
}
else if (isIn && ("mousemove" == event.type || "mouseup" == event.type ||
"touchmove" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = true;
if ("touchmove" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
cuiRepaint();
return true;
}
else {
return false; // none of our business
}
}
else if (this.isPointerInside && !this.isPointerDown) { // focused button state (not pushed yet)
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
this.isPointerDown = true;
this.isPointerInside = true;
if ("touchstart" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
this.timeDown = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier, this.timeDown);
cuiRepaint();
return true;
}
else if (isIn && ("touchend" == event.type || "touchcancel" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (!isIn && ("touchmove" == event.type || "touchend" == event.type ||
"touchcancel" == event.type || "mousemove" == event.type || "mouseup" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return false; // none of our business
}
else {
return false; // none of our business
}
}
else if (!this.isPointerInside && this.isPointerDown) { // focused button state (pushed previously)
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
this.isPointerDown = true;
this.isPointerInside = true;
if ("touchstart" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
this.timeDown = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier, this.timeDown);
cuiRepaint();
return true;
}
else if (isIn && ("mouseup" == event.type || ("mousemove" == event.type && 0 == event.buttons))) {
this.isPointerDown = false;
this.isPointerInside = true;
this.identifier = -1; // mouse
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if (isIn && ("touchend" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if (isIn && ("touchcancel" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (isIn && ("touchmove" == event.type || ("mousemove" == event.type))) {
this.isPointerInside = true;
cuiRepaint();
return true;
}
else if (!isIn && ("mouseup" == event.type || ("mousemove" == event.type && 0 == event.buttons) ||
"touchend" == event.type || "touchcancel" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (!isIn && (("mousedown" == event.type && this.identifier < 0) ||
("touchstart" == event.type && this.identifier == event.identifier))) {
this.isPointerDown = false;
this.isPointerInside = false;
return false; // none of our business
}
else if ("touchmove" == event.type || "mousemove" == event.type) {
return true; // this is our event, we feel responsible for it
}
else {
return false; // none of our business
}
}
else if (this.isPointerInside && this.isPointerDown) { // depressed button state
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
this.isPointerDown = true;
this.isPointerInside = true;
if ("touchstart" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
this.timeDown = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier, this.timeDown);
cuiRepaint();
return true;
}
else if (isIn && ("mouseup" == event.type || ("mousemove" == event.type && 0 == event.buttons))) {
this.isPointerDown = false;
this.isPointerInside = true;
this.identifier = -1; // mouse
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if (isIn && ("touchend" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if (isIn && ("touchcancel" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (!isIn && ("mouseup" == event.type || ("mousemove" == event.type && 0 == event.buttons) ||
"touchend" == event.type || "touchcancel" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (!isIn && ("touchmove" == event.type || ("mousemove" == event.type))) {
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (!isIn && (("mousedown" == event.type && this.identifier < 0) ||
("touchstart" == event.type && this.identifier == event.identifier))) {
this.isPointerDown = false;
this.isPointerInside = false;
return false; // none of our business
}
else if ("touchmove" == event.type || "mousemove" == event.type) {
return true; // this is our event, we feel responsible for it
}
else {
return false; // none of our business
}
}
// unreachable code
return false;
}
/**
* @class cuiDraggable
* @classdesc Draggables can be translated by dragging.
*
* @desc Create a new cuiDraggable.
*/
function cuiDraggable() {
/**
* Difference in x coordinate by which the centre of the draggable has been moved relative to its
* initial position (specified by x + 0.5 * width with the arguments of {@link cuiDraggable#process}).
* @member {number} cuiDraggable.translationX
*/
this.translationX = 0;
/**
* Difference in y coordinate by which the centre of the draggable has been moved relative to its
* initial position (specified by y + 0.5 * height with the arguments of {@link cuiDraggable#process}).
* @member {number} cuiDraggable.translationY
*/
this.translationY = 0;
/**
* Flag specifying whether a mouse button or finger is inside the object's rectangle.
* @member {boolean} cuiDraggable.isPointerInside
*/
this.isPointerInside = false;
/**
* Flag specifying whether a mouse button or finger is pushing the object or has been
* pushing the object and is still held down (but may have moved outside the object's
* rectangle).
* @member {boolean} cuiDraggable.isPointerDown
*/
this.isPointerDown = false;
this.hasTriggeredClick = false; // click event has been triggered?
this.hasTriggeredDoubleClick = false; // double click event has been triggered?
this.hasTriggeredHold = false; // hold event has been triggered?
this.timeDown = 0; // time in milliseconds after January 1, 1970 when the pointer went down
this.eventXDown = 0; // x coordinate of the event when the pointer went down
this.eventYDown = 0; // y coordinate of the event when the pointer went down
this.identifier = -1; // identifier of the touch point
this.translationXDown = 0; // value of translationX when the pointer went down
this.translationYDown = 0; // value of translationY when the pointer went down
}
/**
* Determine whether the draggable has just been clicked.
* @returns {boolean} True if the draggable has been clicked, false otherwise.
*/
cuiDraggable.prototype.isClicked = function() {
return this.hasTriggeredClick;
}
/**
* Determine whether the draggable has just been double clicked.
* @returns {boolean} True if the button has been double clicked, false otherwise.
*/
cuiDraggable.prototype.isDoubleClicked = function() {
return this.hasTriggeredDoubleClick;
}
/**
* Determine whether the pointer has just been held down longer than {@link cuiTimeUntilHold}.
* @returns {boolean} True if the pointer has just been held down long enough, false otherwise.
*/
cuiDraggable.prototype.isHeldDown = function() {
return this.hasTriggeredHold;
}
/**
* Either process the event (if event != null) and return true if the event has been processed,
* or draw the appropriate image for the object state in the rectangle
* with a text string on top of it (if event == null) and return false.
* This function is usually called by {@link cuiPage.process} of a {@link cuiPage}.
* @param {Object} event - An object describing a user event by its "type", coordinates in
* page coordinates ("eventX" and "eventY"), an "identifier" for touch events, and optionally
* "buttons" to specify which mouse buttons are depressed. If null, the function should
* redraw the object.
* @param {number} x - The x coordinate of the top, left corner of the object's rectangle.
* @param {number} y - The y coordinate of the top, left corner of the object's rectangle.
* @param {number} width - The width of the object's rectangle.
* @param {number} height - The height of the object's rectangle.
* @param {string} text - A text that is written at the center of the rectangle. (May be null).
* @param {Object} imageNormal - An image to be drawn inside the object's rectangle if there
* are no user interactions. (May be null.)
* @param {Object} imageFocused - An image to be drawn inside the object's rectangle if the
* mouse hovers over the object's rectangle or a touch point moves into it. (May be null.)
* @param {Object} imagePressed - An image to be drawn inside the object's rectangle if a
* mouse button is pushed or the object is touched. (May be null.)
* @param {number} interactionBits - The forms of interaction, either {@link cuiConstants.none} or a bitwise-or
* of other constants in {@link cuiConstants}, e.g.
* cuiConstants.isDraggableWithOneFinger | cuiConstants.isLimitedToHorizontalDragging.
* @returns {boolean} True if event != null and the event has been processed (implying that
* no other GUI elements should process it). False otherwise.
*/
cuiDraggable.prototype.process = function(event, x, y, width, height,
text, imageNormal, imageFocused, imagePressed, interactionBits) {
// choose appropriate image
var image = imageNormal;
if (this.isPointerDown) {
image = imagePressed;
}
else if (this.isPointerInside) {
image = imageFocused;
}
// check or repaint button
var isIn = cuiIsInsideRectangle(event, x + this.translationX, y + this.translationY,
width, height, text, image);
// note that the event might be inside the rectangle (isIn == true)
// but the state might still be isPointerDown == false (e.g. for touchend or
// touchcancel or if the pointer went down outside of the button)
// react to event
if (null == event) {
return false; // no event to process
}
// clear trigger events (these are set only once after the event and have to be cleared afterwards)
this.hasTriggeredClick = false;
this.hasTriggeredDoubleClick = false;
this.hasTriggeredHold = false;
// process double click events
if ("dblclick" == event.type) {
this.hasTriggeredDoubleClick = isIn;
return isIn;
}
// process our hold events
if ("mousehold" == event.type) {
if (event.timeDown == this.timeDown && event.identifier == this.identifier &&
this.isPointerDown) {
this.hasTriggeredHold = true;
return true;
}
return false;
}
// process other events
if ("wheel" == event.type || "mousewheel" == event.type) {
return isIn; // give directly to caller
}
// ignore mouse or touch points that are not the tracked point (apart from mousedown and touchstart)
if (this.isPointerInside || this.isPointerDown) {
if ("touchend" == event.type || "touchmove" == event.type || "touchcancel" == event.type) {
if (event.identifier != this.identifier) {
return false; // ignore all other touch points except "touchstart" events
}
}
else if (("mousemove" == event.type || "mouseup" == event.type) && this.identifier >= 0) {
return false; // ignore mouse (except mousedown) if we are tracking a touch point
}
}
// state changes
if (!this.isPointerInside && !this.isPointerDown) { // passive object state
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
this.isPointerDown = true;
this.isPointerInside = true;
if ("touchstart" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
this.timeDown = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier, this.timeDown);
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.eventXDown = event.eventX;
this.eventYDown = event.eventY;
cuiRepaint();
return true;
}
else if (isIn && ("mousemove" == event.type || "mouseup" == event.type ||
"touchmove" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = true;
if ("touchmove" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
cuiRepaint();
return true;
}
else {
return false; // none of our business
}
}
else if (this.isPointerInside && !this.isPointerDown) { // focused object state (not pushed yet)
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
this.isPointerDown = true;
this.isPointerInside = true;
if ("touchstart" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
this.timeDown = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier, this.timeDown);
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.eventXDown = event.eventX;
this.eventYDown = event.eventY;
cuiRepaint();
return true;
}
else if (isIn && ("touchend" == event.type || "touchcancel" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if (!isIn && ("touchmove" == event.type || "touchend" == event.type ||
"touchcancel" == event.type || "mousemove" == event.type || "mouseup" == event.type)) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return false; // none of our business
}
else {
return false; // none of our business
}
}
else if (this.isPointerDown) { // grabbed object state
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
this.isPointerDown = true;
this.isPointerInside = true;
if ("touchstart" == event.type) {
this.identifier = event.identifier;
}
else {
this.identifier = -1; // mouse
}
this.timeDown = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier, this.timeDown);
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.eventXDown = event.eventX;
this.eventYDown = event.eventY;
cuiRepaint();
return true;
}
else if ("mouseup" == event.type || ("mousemove" == event.type && 0 == event.buttons)) {
this.isPointerDown = false;
this.isPointerInside = isIn;
this.identifier = -1; // mouse
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if ("touchend" == event.type) {
this.isPointerDown = false;
this.isPointerInside = false;
this.hasTriggeredClick = true;
cuiRepaint();
return true;
}
else if ("touchcancel" == event.type) {
this.isPointerDown = false;
this.isPointerInside = false;
cuiRepaint();
return true;
}
else if ("touchmove" == event.type || ("mousemove" == event.type)) {
this.isPointerInside = isIn;
if (cuiConstants.isDraggableWithOneFinger & interactionBits) {
if (!(cuiConstants.isLimitedToVerticalDragging & interactionBits)) {
this.translationX = this.translationXDown + (event.eventX - this.eventXDown);
}
if (!(cuiConstants.isLimitedToHorizontalDragging & interactionBits)) {
this.translationY = this.translationYDown + (event.eventY - this.eventYDown);
}
}
cuiRepaint();
return true;
}
else if (!isIn && (("mousedown" == event.type && this.identifier < 0) ||
("touchstart" == event.type && this.identifier == event.identifier))) {
this.isPointerDown = false;
this.isPointerInside = false;
return false; // none of our business
}
else {
return false; // none of our business
}
}
// unreachable code
return false;
}
/**
* @class cuiTransformable
* @classdesc Transformable objects can be translated, rotated, and scaled with one- and
* two-finger gestures.
*
* @desc Create a new cuiTransformable.
*/
function cuiTransformable() {
/**
* Clockwise rotation angle in degrees by which the object has been rotated.
* @member {number} cuiTransformable.rotation
*/
this.rotation = 0;
/**
* Scaling factor by which the object has been magnified.
* @member {number} cuiTransformable.scale
*/
this.scale = 1;
/**
* Difference in x coordinate by which the centre of the transformable has been moved relative to its
* initial position (specified by x + 0.5 * width with the arguments of {@link cuiTransformable#process}).
* @member {number} cuiTransformable.translationX
*/
this.translationX = 0;
/**
* Difference in y coordinate by which the centre of the transformable has been moved relative to its
* initial position (specified by y + 0.5 * height with the arguments of {@link cuiTransformable#process}).
* @member {number} cuiTransformable.translationY
*/
this.translationY = 0;
/**
* Flag specifying whether a mouse button or first finger is inside the object's rectangle.
* @member {boolean} cuiTransformable.isPointerInside0
*/
this.isPointerInside0 = false;
/**
* Flag specifying whether a mouse button or first finger is pushing the object or has been
* pushing the object and is still held down (but may have moved outside the object's
* rectangle).
* @member {boolean} cuiTransformable.isPointerDown0
*/
this.isPointerDown0 = false;
/**
* Flag specifying whether a second finger is pushing the object or has been
* pushing the object and is still held down (but may have moved outside the object's
* rectangle).
* @member {boolean} cuiTransformable.isPointerDown1
*/
this.isPointerDown1 = false;
this.hasTriggeredClick = false; // click event has been triggered?
this.hasTriggeredDoubleClick = false; // double click event has been triggered?
this.hasTriggeredHold0 = false; // hold event has been triggered for first pointer?
this.hasTriggeredHold1 = false; // hold event has been triggered for second pointer?
/**
* Flag to specify whether to process events even if they are outside of the rectangle.
* If true, it will consume many more events and therefore should only be used for background objects.
* @member {boolean} cuiTransformable.isProcessingOuterEvents
*/
this.isProcessingOuterEvents = false;
this.timeDown0 = 0; // time in milliseconds after January 1, 1970 when the first pointer went down
this.timeDown1 = 0; // time in milliseconds after January 1, 1970 when the second pointer went down
this.identifier0 = -1; // identifier of the first touch point (-1 for mouse)
this.identifier1 = -1; // identifier of the second touch point (-1 for mouse)
this.translationXDown = 0; // value of translationX when the pointer went down
this.translationYDown = 0; // value of translationX when the pointer went down
this.rotationDown = 0; // value of rotation when the pointer went down
this.scaleDown = 0; // value of scale when the pointer went down
this.eventXDown0 = 0; // x coordinate of the event when the first pointer went down
this.eventYDown0 = 0; // y coordinate of the event when the first pointer went down
this.eventXDown1 = 0; // x coordinate of the event when the second pointer went down
this.eventYDown1 = 0; // y coordinate of the event when the second pointer went down
this.eventX0 = 0; // current x coordinate of the first pointer
this.eventY0 = 0; // current Y coordinate of the first pointer
this.eventX1 = 0; // current x coordinate of the second pointer
this.eventY1 = 0; // current y coordinate of the second pointer
};
/**
* Returns whether the transformable has just been clicked.
* @returns {boolean} True if the draggable has been clicked, false otherwise.
*/
cuiTransformable.prototype.isClicked = function() {
return this.hasTriggeredClick;
}
/**
* Determine whether the button has just been double clicked.
* @returns {boolean} True if the button has been double clicked, false otherwise.
*/
cuiTransformable.prototype.isDoubleClicked = function() {
return this.hasTriggeredDoubleClick;
}
/**
* Determine whether first pointer has just been held down longer than {@link cuiTimeUntilHold}.
* @returns {boolean} True if the first pointer has just been held down long enough, false otherwise.
*/
cuiTransformable.prototype.isHeldDown0 = function() {
return this.hasTriggeredHold0;
}
/**
* Determine whether second pointer has just been held down longer than {@link cuiTimeUntilHold}.
* @returns {boolean} True if the second pointer has just been held down long enough, false otherwise.
*/
cuiTransformable.prototype.isHeldDown1 = function() {
return this.hasTriggeredHold1;
}
/**
* Either process the event (if event != null) and return true if the event has been processed,
* or draw the appropriate image for the object state in the rectangle
* with a text string on top of it (if event == null) and return false.
* This function is usually called by {@link cuiPage.process} of a {@link cuiPage}.
* @param {Object} event - An object describing a user event by its "type", coordinates in
* page coordinates ("eventX" and "eventY"), an "identifier" for touch events, and optionally
* "buttons" to specify which mouse buttons are depressed. If null, the function should
* redraw the object.
* @param {number} x - The x coordinate of the top, left corner of the object's rectangle.
* @param {number} y - The y coordinate of the top, left corner of the object's rectangle.
* @param {number} width - The width of the object's rectangle.
* @param {number} height - The height of the object's rectangle.
* @param {string} text - A text that is written at the center of the rectangle. (May be null).
* @param {Object} imageNormal - An image to be drawn inside the object's rectangle if there
* are no user interactions. (May be null.)
* @param {Object} imageFocused - An image to be drawn inside the object's rectangle if the
* mouse hovers over the object's rectangle or a touch point moves into it. (May be null.)
* @param {Object} imagePressed0 - An image to be drawn inside the object's rectangle if a
* mouse button is pushed or the object is touched once. (May be null.)
* @param {Object} imagePressed1 - An image to be drawn inside the object's rectangle if a
* mouse button is pushed or the object is touched twice. (May be null.)
* @param {number} interactionBits - The forms of interaction, either {@link cuiConstants.none}
* or a bitwise-or of other constants in {@link cuiConstants}, e.g.
* cuiConstants.isDraggableWithOneFinger | cuiConstants.isTransformableWithTwoFingers.
* @returns {boolean} True if event != null and the event has been processed (implying that
* no other GUI elements should process it). False otherwise.
*/
cuiTransformable.prototype.process = function (event, x, y, width, height,
text, imageNormal, imageFocused, imagePressed0, imagePressed1, interactionBits) {
if (null == event) {
// choose appropriate image
var image = imageNormal;
if (this.isPointerDown1) {
image = imagePressed1;
}
else if (this.isPointerDown0) {
image = imagePressed0;
}
else if (this.isPointerInside0) {
image = imageFocused;
}
// transform and draw object
cuiContext.save();
cuiContext.translate(this.translationX, this.translationY);
cuiContext.translate(x + 0.5 * width, y + 0.5 * height);
cuiContext.rotate(this.rotation * Math.PI / 180.0);
cuiContext.scale(this.scale, this.scale);
cuiContext.translate(-x - 0.5 * width, -y - 0.5 * height);
if (null != text) {
cuiContext.fillText(text, x, y, width, height);
}
if (null != image) {
cuiContext.drawImage(image, x, y, width, height);
}
cuiContext.restore();
return false;
}
// check point of event
var isIn = false;
var mappedX = event.eventX - this.translationX;
var mappedY = event.eventY - this.translationY;
mappedX = mappedX - x - 0.5 * width;
mappedY = mappedY - y - 0.5 * height;
var angle = -this.rotation * Math.PI / 180.0;
var tempX = Math.cos(angle) * mappedX - Math.sin(angle) * mappedY;
mappedY = Math.sin(angle) * mappedX + Math.cos(angle) * mappedY;
mappedX = tempX / this.scale;
mappedY = mappedY / this.scale;
mappedX = mappedX + x + 0.5 * width;
mappedY = mappedY + y + 0.5 * height;
if ((x <= mappedX && mappedX < x + width && y <= mappedY && mappedY < y + height) ||
this.isProcessingOuterEvents) {
isIn = true;
}
// clear event notifications (they are only set once and need to be cleared afterwards)
this.hasTriggeredClick = false;
this.hasTriggeredDoubleClick = false;
this.hasTriggeredHold0 = false;
this.hasTriggeredHold1 = false;
// process double click events
if ("dblclick" == event.type) {
this.hasTriggeredDoubleClick = isIn;
return isIn;
}
// process our hold events
if ("mousehold" == event.type) {
if (event.timeDown == this.timeDown0 && event.identifier == this.identifier0 &&
this.isPointerDown0) {
this.hasTriggeredHold0 = true;
return true;
}
return false;
}
if ("mousehold" == event.type) {
if (event.timeDown == this.timeDown1 && event.identifier == this.identifier1 &&
this.isPointerDown1) {
this.hasTriggeredHold1 = true;
return true;
}
return false;
}
// process wheel events
if ("wheel" == event.type || "mousewheel" == event.type) {
if (!(cuiConstants.isUniformlyScalableWithTwoFingers & interactionBits)) {
return false;
}
if (isIn) {
// compute new x and y based on the motion of the point under the mouse
// first compute the point that is mapped to the point under the mouse
var fixpointX = event.eventX;
var fixpointY = event.eventY;
var mappedX = fixpointX - this.translationX - x - 0.5 * width;
var mappedY = fixpointY - this.translationY - y - 0.5 * height;
var angle = -this.rotation * Math.PI / 180.0;
fixpointX = (Math.cos(angle) * mappedX - Math.sin(angle) * mappedY) / this.scale
+ x + 0.5 * width;
fixpointY = (Math.sin(angle) * mappedX + Math.cos(angle) * mappedY) / this.scale
+ y + 0.5 * height;
// change scale
var delta;
if ("mousewheel" == event.type) {
delta = -event.wheelDelta / 30.0;
}
else {
delta = event.deltaY;
}
this.scale = this.scale * Math.pow(2.0, -0.025 * delta);
// now see where this fixpoint is mapped to with the current transformation
mappedX = fixpointX - x - 0.5 * width;
mappedY = fixpointY - y - 0.5 * height;
angle = this.rotation * Math.PI / 180.0;
var tempX = this.scale * (Math.cos(angle) * mappedX - Math.sin(angle) * mappedY);
mappedY = this.scale * (Math.sin(angle) * mappedX + Math.cos(angle) * mappedY);
mappedX = tempX + x + 0.5 * width + this.translationX;
mappedY = mappedY + y + 0.5 * height + this.translationY;
// (x,y) should be at the position of the mouse;
// we change the transformation such that it ends up there
this.translationX = this.translationX + event.eventX - mappedX;
this.translationY = this.translationY + event.eventY - mappedY;
cuiRepaint();
}
return isIn;
}
// ignore mouse or touch points that are not the tracked point (apart from mousedown and touchstart)
if ((this.isPointerInside0 || this.isPointerDown0) && !this.isPointerDown1) {
if ("touchend" == event.type || "touchmove" == event.type || "touchcancel" == event.type) {
if (event.identifier != this.identifier0) {
return false; // ignore all other touch points except "touchstart" events
}
}
else if (("mousemove" == event.type || "mouseup" == event.type) && this.identifier0 >= 0) {
return false; // ignore mouse (except mousedown) if we are tracking a touch point
}
}
if (this.isPointerDown0 && this.isPointerDown1) {
if ("touchend" == event.type || "touchmove" == event.type || "touchcancel" == event.type) {
if (event.identifier != this.identifier0 &&
event.identifier != this.identifier1) {
return false; // ignore all other touch points except "touchstart" events
}
}
else if (("mousemove" == event.type || "mouseup" == event.type) &&
(this.identifier0 >= 0 && this.identifier1 >= 0)) {
return false; // ignore mouse (except mousedown) if we are tracking a touch point
}
}
// state changes
if (!this.isPointerInside0 && !this.isPointerDown0 && !this.isPointerDown1) { // passive object state
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) { // add 0th point
this.isPointerDown0 = true;
this.isPointerInside0 = true;
if ("touchstart" == event.type) {
this.identifier0 = event.identifier;
}
else {
this.identifier0 = -1; // mouse
}
this.timeDown0 = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier0, this.timeDown0);
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.rotationDown = this.rotation;
this.scaleDown = this.scale;
this.eventXDown0 = event.eventX;
this.eventYDown0 = event.eventY;
this.eventX0 = event.eventX;
this.eventY0 = event.eventY;
cuiRepaint();
return true;
}
else if (isIn && ("mousemove" == event.type || "mouseup" == event.type ||
"touchmove" == event.type)) {
this.isPointerDown0 = false;
this.isPointerInside0 = true;
if ("touchmove" == event.type) {
this.identifier0 = event.identifier;
}
else {
this.identifier0 = -1; // mouse
}
cuiRepaint();
return true;
}
else {
return false; // none of our business
}
}
else if (this.isPointerInside0 && !this.isPointerDown0 && !this.isPointerDown1) { // focused object state (not pushed yet)
if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) { // add 0th point
this.isPointerDown0 = true;
this.isPointerInside0 = true;
if ("touchstart" == event.type) {
this.identifier0 = event.identifier;
}
else {
this.identifier0 = -1; // mouse
}
this.timeDown0 = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier0, this.timeDown0);
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.rotationDown = this.rotation;
this.scaleDown = this.scale;
this.eventXDown0 = event.eventX;
this.eventYDown0 = event.eventY;
this.eventX0 = event.eventX;
this.eventY0 = event.eventY;
cuiRepaint();
return true;
}
else if (isIn && ("touchend" == event.type || "touchcancel" == event.type)) {
this.isPointerDown0 = false;
this.isPointerInside0 = false;
cuiRepaint();
return true;
}
else if (!isIn && ("touchmove" == event.type || "touchend" == event.type ||
"touchcancel" == event.type || "mousemove" == event.type || "mouseup" == event.type)) {
this.isPointerDown0 = false;
this.isPointerInside0 = false;
cuiRepaint();
return false; // none of our business
}
else {
return false; // none of our business
}
}
else if (this.isPointerDown0 && !this.isPointerDown1) { // object grabbed once
if (isIn && this.identifier0 < 0 && "mousedown" == event.type) {
// replace 0th mouse point
this.identifier0 = -1; // mouse down
this.isPointerDown0 = true;
this.timeDown0 = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier0, this.timeDown0);
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.rotationDown = this.rotation;
this.scaleDown = this.scale;
this.eventXDown0 = event.eventX;
this.eventYDown0 = event.eventY;
this.eventX0 = event.eventX;
this.eventY0 = event.eventY;
cuiRepaint();
return true;
}
else if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
// add 1st touch point
this.isPointerDown1 = true;
if ("touchstart" == event.type) {
this.identifier1 = event.identifier;
}
else {
this.identifier1 = -1; // mouse
}
this.timeDown1 = (new Date()).getTime();
setTimeout(cuiSendHoldEvent, cuiTimeUntilHold, event.clientX, event.clientY, this.identifier1, this.timeDown1);
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.rotationDown = this.rotation;
this.scaleDown = this.scale;
this.eventXDown0 = this.eventX0;
this.eventYDown0 = this.eventY0;
this.eventXDown1 = event.eventX;
this.eventYDown1 = event.eventY;
this.eventX1 = event.eventX;
this.eventY1 = event.eventY;
cuiRepaint();
return true;
}
else if ("mouseup" == event.type || ("mousemove" == event.type && 0 == event.buttons)) {
this.isPointerDown0 = false;
this.isPointerInside0 = isIn;
this.identifier0 = -1; // mouse
if (isIn) {
this.hasTriggeredClick = true;
}
cuiRepaint();
return true;
}
else if ("touchend" == event.type) {
this.isPointerDown0 = false;
this.isPointerInside0 = false;
if (isIn) {
this.hasTriggeredClick = true;
}
cuiRepaint();
return true;
}
else if ("touchcancel" == event.type) {
this.isPointerDown0 = false;
this.isPointerInside0 = false;
cuiRepaint();
return true;
}
else if ("touchmove" == event.type || ("mousemove" == event.type)) {
this.isPointerInside0 = isIn;
this.eventX0 = event.eventX;
this.eventY0 = event.eventY;
if (cuiConstants.isDraggableWithOneFinger & interactionBits) {
this.translationX = this.translationXDown + (this.eventX0 - this.eventXDown0);
this.translationY = this.translationYDown + (this.eventY0 - this.eventYDown0);
}
cuiRepaint();
return true;
}
else if (!isIn && (("mousedown" == event.type && this.identifier0 < 0) ||
("touchstart" == event.type && this.identifier0 == event.identifier))) {
this.isPointerDown0 = false;
this.isPointerInside0 = false;
cuiRepaint();
return false; // none of our business
}
else {
return false; // none of our business
}
}
else if (this.isPointerDown1) { // two pointers down
if (("mouseup" == event.type && this.identifier0 < 0) ||
(("touchend" == event.type || "touchcancel" == event.type) &&
event.identifier == this.identifier0)) { // 0th point goes up
// remove 0th point, replace by 1st
this.isPointerDown1 = false;
this.isPointerDown0 = true;
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.rotationDown = this.rotation;
this.scaleDown = this.scale;
this.eventXDown0 = this.eventX1;
this.eventYDown0 = this.eventY1;
this.eventX0 = this.eventX1;
this.eventY0 = this.eventY1;
this.identifier0 = this.identifier1;
if (isIn) {
this.hasTriggeredClick = true;
}
cuiRepaint();
return true;
}
else if (("mouseup" == event.type && this.identifier1 < 0) ||
(("touchend" == event.type || "touchcancel" == event.type) &&
event.identifier == this.identifier1)) { // 1st point goes up
// just remove 1st point
this.isPointerDown1 = false;
this.isPointerDown0 = true;
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.rotationDown = this.rotation;
this.scaleDown = this.scale;
this.eventXDown0 = this.eventX0;
this.eventYDown0 = this.eventY0;
if (isIn) {
this.hasTriggeredClick = true;
}
cuiRepaint();
return true;
}
else if (isIn && ("mousedown" == event.type || "touchstart" == event.type)) {
// remove 0th point, replace by 1st, add new as 1stby removing the 0th point,
// the user has a way of getting rid of ghost points
// which are no longer tracked (but which we still assume to be active)
this.translationXDown = this.translationX;
this.translationYDown = this.translationY;
this.rotationDown = this.rotation;
this.scaleDown = this.scale;
this.eventXDown0 = this.eventX1;
this.eventYDown0 = this.eventY1;
this.eventX0 = this.eventX1;
this.eventY0 = this.eventY1;
this.identifier0 = this.identifier1;
this.eventXDown1 = event.eventX;
this.eventYDown1 = event.eventY;
this.eventX1 = event.eventX;
this.eventY1 = event.eventY;
if ("touchstart" == event.type) {
this.identifier1 = event.identifier;
}
else {
this.identifier1 = -1; // mouse
}
cuiRepaint();
return true;
}
else if ("touchmove" == event.type || ("mousemove" == event.type)) {
// update dragging
if (("mousemove" == event.type && this.identifier0 < 0) ||
("touchmove" == event.type && event.identifier == this.identifier0)) {
this.eventX0 = event.eventX;
this.eventY0 = event.eventY;
}
else if (("mousemove" == event.type && this.identifier1 < 0) ||
("touchmove" == event.type && event.identifier == this.identifier1)) {
this.eventX1 = event.eventX;
this.eventY1 = event.eventY;
}
else {
return false; // we should not have gotten this event (see above for the filtering)
}
if (cuiConstants.isRotatableWithTwoFingers & interactionBits) {
// compute new rotation
this.rotation = this.rotationDown +
(Math.atan2(this.eventY1 - this.eventY0,
this.eventX1 - this.eventX0) -
Math.atan2(this.eventYDown1 - this.eventYDown0,
this.eventXDown1 - this.eventXDown0)
) * 180.0 / Math.PI;
while (this.rotation >= 360.0) {
this.rotation -= 360.0;
}
while (this.rotation < 0.0) {
this.rotation += 360.0;
}
}
if (cuiConstants.isScalableWithTwoFingers & interactionBits) {
// compute new scale
var diffPointsLength =
Math.sqrt((this.eventX0 - this.eventX1) *
(this.eventX0 - this.eventX1) +
(this.eventY0 - this.eventY1) *
(this.eventY0 - this.eventY1));
var diffPointsLengthDown =
Math.sqrt((this.eventXDown0 - this.eventXDown1) *
(this.eventXDown0 - this.eventXDown1) +
(this.eventYDown0 - this.eventYDown1) *
(this.eventYDown0 - this.eventYDown1));
this.scale = this.scaleDown * diffPointsLength /
diffPointsLengthDown;
}
if (cuiConstants.isDraggableWithTwoFingers & interactionBits) {
// compute new x and y based on the motion of the center between the two points
// first compute the point that was mapped to the center between the two fingers when grabbed
var fixpointX = 0.5 * (this.eventXDown0 + this.eventXDown1);
var fixpointY = 0.5 * (this.eventYDown0 + this.eventYDown1);
var mappedX = fixpointX - this.translationXDown - x - 0.5 * width;
var mappedY = fixpointY - this.translationYDown - y - 0.5 * height;
var angle = -this.rotationDown * Math.PI / 180.0;
fixpointX = (Math.cos(angle) * mappedX - Math.sin(angle) * mappedY) / this.scaleDown
+ x + 0.5 * width;
fixpointY = (Math.sin(angle) * mappedX + Math.cos(angle) * mappedY) / this.scaleDown
+ y + 0.5 * height;
// now see where this fixpoint is mapped to with the current transformation
mappedX = fixpointX - x - 0.5 * width;
mappedY = fixpointY - y - 0.5 * height;
angle = this.rotation * Math.PI / 180.0;
var tempX = this.scale * (Math.cos(angle) * mappedX - Math.sin(angle) * mappedY);
mappedY = this.scale * (Math.sin(angle) * mappedX + Math.cos(angle) * mappedY);
mappedX = tempX + x + 0.5 * width + this.translationX;
mappedY = mappedY + y + 0.5 * height + this.translationY;
// (x,y) should be at the center between the two fingers;
// we change the transformation such that it ends up there
this.translationX = this.translationX +
0.5 * (this.eventX0 + this.eventX1) - mappedX;
this.translationY = this.translationY +
0.5 * (this.eventY0 + this.eventY1) - mappedY;
}
cuiRepaint();
return true;
}
else {
return false;
}
}
// unreachable code
return false;
}
/**
* @class cuiKeyframe
* @classdesc A keyframe defines an array of numeric values at a certain time with tangents for the interpolation
* right before and right after that time. Instead of using the constructor, objects can also be initialized
* with "{time : ..., in : ..., out : ..., values : [..., ...]}"
* (See {@link cuiAnimation}.)
*
* @desc Create a new cuiKeyframe.
* @param {number} time - The time of the keyframe (in seconds relative to the start of the animation).
* @param {number} inTangent - Number specifying the tangent before the keyframe; -1: linear interpolation,
* 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases.
* @param {number} outTangent - Number specifying the tangent after the keyframe; -1: linear interpolation,
* 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases.
* @param {number[]} values - An array of numbers; all keyframes of one animation should have
* values arrays of the same size.
*/
function cuiKeyframe(time, inTangent, outTangent, values) {
/**
* The time of the keyframe (in seconds relative to the start of the animation).
* @member {number} cuiKeyframe.time
*/
this.time = time;
/**
* Number specifying the tangent before the keyframe; -1: linear interpolation,
* 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases.
* @member {number} cuiKeyframe.in
*/
this.in = inTangent;
/**
* Number specifying the tangent after the keyframe; -1: linear interpolation,
* 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases.
* @member {number} cuiKeyframe.out
*/
this.out = outTangent;
/**
* An array of numbers; all keyframes of one animation should have
* values arrays of the same size.
* @member {number[]} cuiKeyframe.values
*/
this.values = values;
}
/**
* @class cuiAnimation
* @classdesc Animations allow to animate (i.e. interpolate) numbers specified by keyframes.
* (See {@link cuiKeyframe}.)
*
* @desc Create a new cuiAnimation.
*/
function cuiAnimation() {
this.keyframes = null;
this.stretch = 1.0;
this.start = 0;
this.end = 0;
this.isLooping = false;
}
/**
* Play an animation.
* @param {cuiKeyframe[]} keyframes - An array of keyframe objects. (Object initialization with
* something like var keys = [{time : ..., in : ..., out : ..., values : [..., ...]}, {...}, ...];
* is encouraged.) (See {@link cuiKeyframe}.)
* @param {number} stretch - A scale factor for the times in the keyframes;
* one way of usage: start designing keyframe times with stretch = 1 and
* adjust the overall timing at the end by adjusting stretch;
* another way of usage: define all times of keyframes between 0 and 1 (as in CSS transitions)
* and then set stretch to the length of the animation in seconds.
* @param {boolean} isLooping - Whether to repeat the animation endlessly.
*/
cuiAnimation.prototype.play = function(keyframes, stretch, isLooping) {
this.keyframes = keyframes;
this.stretch = stretch;
this.isLooping = isLooping;
this.start = (new Date()).getTime();
this.end = this.start +
1000.0 * this.keyframes[this.keyframes.length - 1].time * this.stretch;
if (this.end > cuiAnimationsEnd) { // new maximum end?
cuiAnimationsEnd = this.end;
}
cuiRepaint();
}
/**
* Stop looping the animation.
*/
cuiAnimation.prototype.stopLooping = function() {
this.isLooping = false;
}
/**
* Determine whether the animation is currently playing.
* @returns {boolean} True if the animation is currently playing, false otherwise.
*/
cuiAnimation.prototype.isPlaying = function() {
if (!this.isLooping) {
return ((new Date()).getTime() < this.end);
}
else {
return (this.end > 0);
}
}
/**
* Compute an array of interpolated values based on the keyframes and the current time.
* Returns the values array of the 0th keyframe if the animation hasn't started yet
* and the values array of the last keyframe if it has finished playing.
* This makes it possible to use animateValues even after the animation has stopped.
* (See {@link cuiKeyframe}.)
* @returns {number[]} An array of interpolated values.
*/
cuiAnimation.prototype.animateValues = function() {
var now = (new Date()).getTime();
if (now < this.start || this.end <= this.start) { // animation not started?
return this.keyframes[0].values;
}
if (now > this.end) { // current loop of animation already over?
if (!this.isLooping) {
return this.keyframes[this.keyframes.length - 1].values;
}
// restart the animation
var length = 1000.0 * this.keyframes[this.keyframes.length - 1].time * this.stretch;
this.start = this.start + Math.floor((now - this.start) / length) * length;
this.end = this.start + length;
if (this.end > cuiAnimationsEnd) { // new maximum end?
cuiAnimationsEnd = this.end;
}
}
// determine index iTo of keyframe after(!) current time t
var iTo = 0;
var ut = 0.001 * (now - this.start) / this.stretch;
// unstretched time relative to animation start in seconds
while (iTo < this.keyframes.length &&
this.keyframes[iTo].time < ut) {
iTo = iTo + 1;
}
var iFrom = iTo - 1; // index of keyframe before t
if (iTo == 0) {
return this.keyframes[0].values;
}
if (iTo >= this.keyframes.length) {
return this.keyframes[this.keyframes.length - 1].values;
}
// interpolate each value
var newValues = this.keyframes[iFrom].values.slice(0);
var t0 = this.keyframes[iFrom].time;
var t1 = this.keyframes[iTo].time;
var t = (ut - t0) / (t1 - t0)
var tt = t * t;
var ttt = tt * t;
for (var iValue = 0; iValue < newValues.length; iValue++) {
// compute values for cubic Hermite spline with out/in determining
// the velocity: out/in = -1: linear, out/in = 0: slow (i.e. 0),
// out/in = 1: smooth (Catmull-Rom spline).
// The magnitude of in/out changes the velocity accordingly.
var p0, p1, m0, m1;
p0 = this.keyframes[iFrom].values[iValue];
p1 = this.keyframes[iTo].values[iValue];
// compute out slope m0 at iFrom
if (this.keyframes[iFrom].out < 0.0) { // linear
m0 = (p1 - p0) / (t1 - t0) * (-this.keyframes[iFrom].out);
}
else if (iFrom > 0) { // smooth, not in first interval
m0 = (p1 - this.keyframes[iFrom - 1].values[iValue]) /
(t1 - this.keyframes[iFrom - 1].time) *
this.keyframes[iFrom].out;
}
else { // smooth, in first interval
m0 = (p1 - p0) / (t1 - t0) * this.keyframes[iFrom].out;
}
// compute in slope m1 at iTo
if (this.keyframes[iTo].in < 0.0) { // linear
m1 = (p1 - p0) / (t1 - t0) * (-this.keyframes[iTo].in);
}
else if (iTo < this.keyframes.length - 1) { // smooth, not last interval
m1 = (this.keyframes[iTo + 1].values[iValue] - p0) /
(this.keyframes[iTo + 1].time - t0) *
this.keyframes[iTo].in;
}
else { // smooth, in last interval
m1 = (p1 - p0) / (t1 - t0) * this.keyframes[iTo].in;
}
// cubic Hermite curve interpolation
newValues[iValue] = (2.0*ttt-3.0*tt+1.0) * p0 +
(ttt-2.0*tt+t)*(t1-t0) * m0 +
(-2.0*ttt+3.0*tt) * p1 +
(ttt-tt) * (t1-t0) * m1;
}
return newValues;
}
/**
* Play a transition between two pages.
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // maximize (reverse of minimize)
* var startPoint = {x: 300 + 40, y: 50 + 25}; // start point
* 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);
* @example
* // minimize (reverse of maximize)
* var targetPoint = {x: 300 + 40, y: 50 + 25}; // target point
* 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @example
* // 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);
* @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.play(transitionKeyframes, animationLength);
cuiIgnoringEventsEnd = cuiAnimationForTransitions.end;
cuiRepaint();
}
// 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();
}
}