Skip to main content

  | मौसम…

411 समाचार
Public archive edition · Attributed source reporting with original dates · AI drafts are excluded

411 REFERENCE · Wikibooks

Canvas 2D Web Apps/Transformable Objects

Reference material by Wikibooks contributors. This retains its source identity and is not 411 news reporting.

This chapter introduces “transformable objects.” Here, “transformable objects” denote images that can be moved (“translated”), rotated, and scaled (uniformly in both directions) by two-finger gestures. Transformable objects are used very much like draggable objects; however, their process functions requires another image for the case that they are touched by two fingers and it takes another boolean value to determine whether the object can be dragged with one finger or the mouse (instead of only with two fingers). As with draggable objects, any number of objects can be transformed simultaneously (as long as the touch device supports the required number of touch events). Moreover, application programmers don't have to worry about the handling of multiple simultaneous touch events: the process function receives only events with a single pair of coordinates and processes only one object at a time. The Example The example of this chapter (which is available online; also as downloadable version) shows two objects: both can be dragged with one finger (or a mouse) and transformed with two fingers but one cannot be scaled and the other cannot be rotated. The following sections discuss how to use transformable objects and how them. See the chapter on draggable objects for other parts. <!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <script src="cui2d.js"></script> <script> function init() { // get images imageNormalAlien.src = "alien_sleepy.png"; imageNormalAlien.onload = cuiRepaint; imageFocusedAlien.src = "alien_wow.png"; imageFocusedAlien.onload = cuiRepaint; imageGrabbedOnceAlien.src = "alien_lipbite.png"; imageGrabbedOnceAlien.onload = cuiRepaint; imageGrabbedTwiceAlien.src = "alien_smiley.png"; imageGrabbedTwiceAlien.onload = cuiRepaint; // set defaults for all pages cuiBackgroundFillStyle = "#00A000"; // initialize cui2d and start with myPage cuiInit(myPage); } // create images for the smiley var imageNormalAlien = new Image(); var imageFocusedAlien = new Image(); var imageGrabbedOnceAlien = new Image(); var imageGrabbedTwiceAlien = new Image(); // create draggable objects var transformable0 = new cuiTransformable(); var transformable1 = new cuiTransformable(); // create a page var myPage = new cuiPage(400, 300, myPageProcess); // a function to repaint the canvas and return false (if null == event) // or to process user events (if null != event) and return true // if the event has been processed function myPageProcess(event) { // draw and react to transformables if (transformable0.process(event, 50, 100, 200, 200, null, imageNormalAlien, imageFocusedAlien, imageGrabbedOnceAlien, imageGrabbedTwiceAlien, cuiConstants.isDraggableWithOneFinger | cuiConstants.isTransformableWithTwoFingers)) { transformable0.scale = 1.0; // always reset scale (i.e. don't scale) return true; } if (transformable1.process(event, 250, 100, 200, 200, null, imageNormalAlien, imageFocusedAlien, imageGrabbedOnceAlien, imageGrabbedTwiceAlien, cuiConstants.isDraggableWithOneFinger | cuiConstants.isTransformableWithTwoFingers)) { transformable1.rotation = 0.0; // always reset rotation (i.e. dont' rotate) return true; } // repaint this page? if (null == event) { // background cuiContext.fillStyle = "#F0F0F0"; cuiContext.fillRect(0, 0, this.width, this.height); } return false; // event has not been processed } </script> </head> <body bgcolor="#000000" onload="init()" style="-webkit-user-drag:none; -webkit-user-select:none; "> <span style="color:white;">A canvas element cannot be displayed.</span> </body> </html> Using Transformable Objects Similarly to draggable object, a global variable should be defined for each transformable object. In the example, this looks like this: // create draggable objects var transformable0 = new cuiTransformable(); var transformable1 = new cuiTransformable(); The transformable objects are then processed in the page's process functions: // draw and react to transformables if (transformable0.process(event, 50, 100, 200, 200, null, imageNormalAlien, imageFocusedAlien, imageGrabbedOnceAlien, imageGrabbedTwiceAlien, cuiConstants.isDraggableWithOneFinger | cuiConstants.isTransformableWithTwoFingers)) { transformable0.scale = 1.0; // always reset scale (i.e. don't scale) return true; } if (transformable1.process(event, 250, 100, 200, 200, null, imageNormalAlien, imageFocusedAlien, imageGrabbedOnceAlien, imageGrabbedTwiceAlien, cuiConstants.isDraggableWithOneFinger | cuiConstants.isTransformableWithTwoFingers)) { transformable1.rotation = 0.0; // always reset rotation (i.e. dont' rotate) return true; } The last argument of the process function (here it's cuiConstants.isDraggableWithOneFinger | cuiConstants.isTransformableWithTwoFingers) specifies the available forms of interaction. To support multiple forms of interaction, the constants have to be combined with a bitwise-or as in the example. Other useful constants are cuiConstants.isDraggableWithTwoFingers, cuiConstants.isRotatableWithTwoFingers and cuiConstants.isUniformlyScalableWithTwoFingers. Deactivating rotation and scaling with these flags is not as flexible as the method in the example; however, it avoids some translational movement that comes with rotation and scaling gestures. The function process returns true whenever the object processed the event. In these cases, additional actions should be taken. Here, the scaling of the first object is deactivated by always setting it to 1.0. Analogously, the rotation of the second object is deactivated by always setting the rotation angle to 0 degrees. Of course, there are many more possibilities of reacting to the transformation of objects; e.g., restricting translationX and translationY. Implementation of Transformable Objects Due to the different transformations and the use of two touch points for the gesture, transformable objects require considerably more properties than draggable objects: /** * @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; }