Show:

File: src\class.scene.js

/*
 * Copyright (c) 2012  Capgemini Technology Services (hereinafter “Capgemini”)
 *
 * License/Terms of Use
 *
 * Permission is hereby granted, free of charge and for the term of intellectual property rights on the Software, to any
 * person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify
 * and propagate free of charge, anywhere in the world, all or part of the Software subject to the following mandatory conditions:
 *
 *   •    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 *  Any failure to comply with the above shall automatically terminate the license and be construed as a breach of these
 *  Terms of Use causing significant harm to Capgemini.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 *  WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
 *  OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 *  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 *  Except as contained in this notice, the name of Capgemini shall not be used in advertising or otherwise to promote
 *  the use or other dealings in this Software without prior written authorization from Capgemini.
 *
 *  These Terms of Use are subject to French law.
 * */

"use strict";

/**
 * Provides requestAnimationFrame in a cross browser way.
 * @property cgsgGlobalRenderingTimer
 * @private
 * @type {Number}
 */
var cgsgGlobalRenderingTimer = null;
//var cgsgGlobalFramerate = CGSG_DEFAULT_FRAMERATE;
(function () {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
        window.cancelAnimationFrame =
            window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
    }

    if (!window.requestAnimationFrame) {
        window.requestAnimationFrame = function (callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 17 - (currTime - lastTime)); //1000/60 = 16.667
            cgsgGlobalRenderingTimer = window.setTimeout(function () {
                callback(currTime + timeToCall);
            }, timeToCall);
            lastTime = currTime + timeToCall;
            //return id;
        };
    }

    if (!window.cancelAnimationFrame) {
        window.cancelAnimationFrame = function (id) {
            clearTimeout(id);
        };
    }
}());

/**
 * Represent the scene of the application.
 * It encapsulates the scene graph itself and several methods to track mouse and touch events, ...
 *
 * @class CGSGScene
 * @constructor
 * @module Scene
 * @main Scene
 * @extends {Object}
 * @param {HTMLElement} canvas a handler to the canvas HTML element
 * @type {CGSGScene}
 * @author Gwennael Buchet (gwennael.buchet@gmail.com)
 */
var CGSGScene = CGSGObject.extend(
    {
        initialize: function (canvas) {

            //detect the current explorer to apply correct parameters
            cgsgDetectCurrentExplorer();

            //for IE10 on Win8 : disable the default touch actions
            if (typeof canvas.style.msTouchAction != 'undefined') {
                canvas.style.msTouchAction = "none";
            }

            //noinspection JSUndeclaredVariable
            cgsgCanvas = canvas;
            /**
             * @property context
             * @type {CanvasRenderingContext2D}
             */
            this.context = cgsgCanvas.getContext("2d");

            /**
             * Multiselection boolean.
             * @property allowMultiSelect
             * @default true
             * @type {Boolean}
             */
            this.allowMultiSelect = true;

            /**
             * Fill color for the drag selection selection rectangle
             * @property dragSelectFillColor
             * @default "#C0C0C0"
             * @type {String}
             */
            this.dragSelectFillColor = CGSG_DEFAULT_DRAG_SELECT_FILL_COLOR;

            /**
             * Stroke color for the drag selection selection rectangle
             * @property dragSelectStrokeColor
             * @default "#808080"
             * @type {String}
             */
            this.dragSelectStrokeColor = CGSG_DEFAULT_DRAG_SELECT_STROKE_COLOR;

            /**
             * Alpha value for the drag selection rectangle
             * @property dragSelectAlpha
             * @default 0.6
             * @type {Number}
             */
            this.dragSelectAlpha = CGSG_DEFAULT_DRAG_SELECT_ALPHA;

            /**
             * The scene graph itself
             * @property sceneGraph
             * @type {CGSGSceneGraph}
             */
            this.sceneGraph = new CGSGSceneGraph(cgsgCanvas, this.context);

            /**
             * List of the current selected nodes in the scenegraph.
             * @property selectedNodes
             * @type {Array}
             */
            this.selectedNodes = this.sceneGraph.selectedNodes;

            /**
             * Current framerate of the application
             * @property fps
             * @type {Number}
             */
            this.fps = 0;

            /*
             * If true, framework will take care of multi-touch : NOT EFFECTIVE YET
             * @property multitouch
             * @default false
             * @type {Boolean}
             */
            //this.multitouch = false;

            ////// @private /////////
            /**
             * @property _isRunning
             * @type {Boolean}
             * @private
             */
            this._isRunning = false;
            // when set to true, the canvas will redraw everything
            // invalidate() just sets this to false right now
            // we want to call invalidate() whenever we make a change
            this._needRedraw = true;

            /**
             * @property _frameContainer Handler to the HTML Element displaying the FPS
             * @type {HTMLElement}
             * @private
             */
            this._frameContainer = null;

            /**
             * True if the [CTRL} key is being pressed
             * @property _keyDownedCtrl
             * @default false
             * @type {Boolean}
             * @private
             */
            this._keyDownedCtrl = false;

            /**
             * @property _timerDblTouch
             * @default null
             * @type {Number}
             * @private
             */
            this._timerDblTouch = null;

            /**
             * The delay between 2 touches to be considered as a dbl touch event.
             * To remove the double touch, just set it to 0
             * @property dblTouchDelay
             * @default CGSG_DEFAULT_DBLTOUCH_DELAY
             * @type {Number}
             */
            this.dblTouchDelay = CGSG_DEFAULT_DBLTOUCH_DELAY;

            /**
             * Current positions of the mouse or touch (Array of CGSGPosition)
             * @property _mousePosition
             * @type {Array}
             * @private
             */
            this._mousePosition = [];
            this._mouseOldPosition = [];
            this._dragSelectStartMousePosition = [];
            this._dragSelectEndMousePosition = [];
            this._isDrag = false;
            this._isResizeDrag = false;
            this._isDragSelect = false;
            this._resizingDirection = -1;
            /**
             * @property _listCursors List of the names for the cursor when overring a handlebox
             * @type {Array}
             * @private
             */
            this._listCursors =
                ['nw-resize', 'n-resize', 'ne-resize', 'w-resize', 'e-resize', 'sw-resize',
                    's-resize', 'se-resize'];
            this._offsetX = 0;
            this._offsetY = 0;
            /**
             * @property _selectedNode The current last selected node
             * @type {null}
             * @private
             */
            this._selectedNode = null;

            ////// INITIALIZATION /////////

            //use an external variable to define the scope of the processes
            var scope = this;
            cgsgCanvas.onmousedown = function (event) {
                scope.onMouseDown(event);
            };
            cgsgCanvas.onmouseup = function (event) {
                scope.onMouseUp(event);
            };
            cgsgCanvas.ondblclick = function (event) {
                scope.onMouseDblClick(event);
            };
            cgsgCanvas.onmousemove = function (event) {
                scope.onMouseMove(event);
            };
            document.onkeydown = function (event) {
                scope.onKeyDownHandler(event);
            };
            document.onkeyup = function (event) {
                scope.onKeyUpHandler(event);
            };
            cgsgCanvas.addEventListener('touchstart', function (event) {
                scope.onTouchStart(event);
            }, false);
            cgsgCanvas.addEventListener('touchmove', function (event) {
                scope.onTouchMove(event);
            }, false);
            cgsgCanvas.addEventListener('touchend', function (event) {
                scope.onTouchEnd(event);
            }, false);

            cgsgCanvas.addEventListener('MSPointerDown', function (event) {
                scope.onTouchStart(event);
            }, false);
            cgsgCanvas.addEventListener("MSPointerMove", function (event) {
                scope.onTouchMove(event);
            }, false);
            cgsgCanvas.addEventListener('MSPointerUp', function (event) {
                scope.onTouchEnd(event);
            }, false);

            this._lastUpdate = new Date().getTime();

            this._nodeMouseOver = null;

            /**
             * Callback on click down on scene event.
             * @property onSceneClickStart
             * @default null
             * @type {Function}
             * @example
             *  this.onSceneClickStart = function (event) {
			 *      event.position; //Array of CGSGPosition
			 *      event.event; //Event
			 *  }
             */
            this.onSceneClickStart = null;
            /**
             * Callback on click up on scene event
             * @property onSceneClickEnd
             * @default null
             * @type {Function}
             * @example
             *  this.onSceneClickEnd = function (event) {
			 *      event.position; //Array of CGSGPosition
			 *      event.event; //Event
			 *  }
             */
            this.onSceneClickEnd = null;
            /**
             * Callback on double click start on scene event
             * @property onSceneDblClickStart
             * @default null
             * @type {Function}
             * @example
             *  this.onSceneDblClickStart = function (event) {
			 *      event.position; //Array of CGSGPosition
			 *      event.event; //Event
			 *  }
             */
            this.onSceneDblClickStart = null;
            /**
             * Callback on double click up on scene event
             * @property onSceneDblClickEnd
             * @default null
             * @type {Function}
             * @example
             *  this.onSceneDblClickEnd = function (event) {
			 *      event.position; //Array of CGSGPosition
			 *      event.event; //Event
			 *  }
             */
            this.onSceneDblClickEnd = null;
            /**
             * Callback on start rendering event
             * @property onRenderStart
             * @default null
             * @type {Function}
             * @example
             *  this.onSceneClickStart = function () {
			 *      //...
			 *  }
             */
            this.onRenderStart = null;
            /**
             * Callback on end rendering event
             * @property onRenderEnd
             * @default null
             * @type {Function}
             * @example
             *  this.onRenderEnd = function () {
			 *      //...
			 *  }
             */
            this.onRenderEnd = null;
        },

        /**
         * Change the dimension of the canvas.
         * Does not really change the dimension of the rendering canvas container,
         *  but is used by the different computations
         * @method setCanvasDimension
         * @param {CGSGDimension} newDimension
         * */
        setCanvasDimension: function (newDimension) {
            cgsgCanvas.width = newDimension.width;
            cgsgCanvas.height = newDimension.height;
            this.sceneGraph.setCanvasDimension(newDimension);

            //Experimental
            /*this._dblCanvas.width = newDimension.x;
             this._dblCanvas.height = newDimension.y;
             this._dblContext = this._dblCanvas.getContext('2d');*/
        },

        /**
         * Remove the nodes selected in the scene graph
         * @method deleteSelected
         */
        deleteSelected: function () {
            if (this.sceneGraph.selectedNodes.length > 0) {
                for (var i = this.sceneGraph.selectedNodes.length - 1; i >= 0; i--) {
                    this._selectedNode = this.sceneGraph.selectedNodes[i];
                    this.sceneGraph.removeNode(this._selectedNode, true);
                }
            }
        },

        /**
         * Deselect all nodes
         * @public
         * @method deselectAll
         * @param {Array} excludedArray CGSGNodes not to deselect
         */
        deselectAll: function (excludedArray) {
            this._isDrag = false;
            this._isResizeDrag = false;
            this._resizingDirection = -1;

            this.sceneGraph.deselectAll(excludedArray);

            this.invalidate();
        },

        /**
         * the main rendering loop
         * @protected
         * @method render
         */
        render: function () {
            if (this._isRunning && this._needRedraw) {
                if (this.onRenderStart !== null) {
                    this.onRenderStart();
                }

                this.sceneGraph.render();

                //render the drag selection box directly onto the scene graph ontop of everything else
                if (this._dragSelectStartMousePosition.length > 0 && this._dragSelectEndMousePosition.length > 0) {

                    var p1 = this._dragSelectStartMousePosition[0];
                    var p2 = this._dragSelectEndMousePosition[0];

                    var dx = p2.x - p1.x, dy = p2.y - p1.y;

                    this.sceneGraph.context.save();

                    this.sceneGraph.context.scale(cgsgDisplayRatio.x, cgsgDisplayRatio.y);
                    this.sceneGraph.context.strokeStyle = this.dragSelectStrokeColor;
                    this.sceneGraph.context.fillStyle = this.dragSelectFillColor;
                    this.sceneGraph.context.globalAlpha = this.dragSelectAlpha;
                    this.sceneGraph.context.fillRect(p1.x, p1.y, dx, dy);
                    this.sceneGraph.context.strokeRect(p1.x, p1.y, dx, dy);

                    this.sceneGraph.context.restore();
                }

                if (this.onRenderEnd !== null) {
                    this.onRenderEnd();
                }

            }

            //if (!this.sceneGraph.stillHaveAnimation()) {
            //	this._needRedraw = false;
            //}

            this._updateFramerate();
            this._updateFramerateContainer();
        },

        /**
         * Call this to start the update of the scene
         * @public
         * @method startPlaying
         */
        startPlaying: function () {
            //we want the callback of the requestAnimationFrame function to be this one.
            //however, the scope of 'this' won't be the same on the requestAnimationFrame function (scope = window)
            // and this one (scope = this). So we bind this function to this scope
            var bindStartPlaying = this.startPlaying.bind(this);
            window.requestAnimationFrame(bindStartPlaying);
            this._isRunning = true;

            this.render();
        },

        /**
         * Call this to stop the rendering (and so animation) update
         * @public
         * @method stopPlaying
         */
        stopPlaying: function () {
            window.cancelAnimationFrame(cgsgGlobalRenderingTimer);
            this._isRunning = false;
        },

        /**
         * Inform the SceneGraph that a new render is needed
         * @public
         * @method invalidate
         */
        invalidate: function () {
            this._needRedraw = true;
        },

        /**
         * Update the current framerate
         * @method _updateFramerate
         * @private
         */
        _updateFramerate: function () {
            if (!cgsgExist(this._fpss)) {
                this._fpss = [];
                this.currentFps = 0;
            }

            var now = new Date().getTime();
            var delta = (now - this._lastUpdate);

            if (!isNaN(cgsgMaxFramerate)) {
                while ((1000.0 / delta) > cgsgMaxFramerate) {
                    now = new Date().getTime();
                    delta = (now - this._lastUpdate);
                }
            }

            this._fpss[this.currentFps++] = 1000.0 / delta;

            if (this.currentFps == cgsgFramerateDelay) {
                this.currentFps = 0;
                this.fps = this._fpss.average();
            }

            this._lastUpdate = now;
        },

        /**
         * Update the innerHTML of the HTMLElement passed as parameter of the "showFPS" function
         * @method _updateFramerateContainer
         * @private
         */
        _updateFramerateContainer: function () {
            if (this._frameContainer !== null) {
                this._frameContainer.innerHTML = Math.round(this.fps).toString();
            }
        },

        /**
         * @public
         * @method showFPS
         * @param {HTMLElement} elt an HTML element to receive the FPS. Can be null if you want to remove the framerate
         */
        showFPS: function (elt) {
            this._frameContainer = elt;
        },

        /**
         * Set the new value for the display ratio.
         * The display ratio is used to resize all the elements on the graph to be adapted to the screen,
         * depending on the reference screen size.
         * You can compute the ratio like this: x = canvas.width/reference.width ; y = canvas.height/reference.height
         * @public
         * @method setDisplayRatio
         * @param {CGSGScale} newRatio a CGSGScale value
         */
        setDisplayRatio: function (newRatio) {
            //noinspection JSUndeclaredVariable
            cgsgDisplayRatio = newRatio;
            this.sceneGraph.initializeGhost(cgsgCanvas.width / cgsgDisplayRatio.x,
                cgsgCanvas.height / cgsgDisplayRatio.y);
        },

        /**
         * @public
         * @method getDisplayRatio
         * @return {CGSGScale} the current display ratio
         */
        getDisplayRatio: function () {
            return cgsgDisplayRatio;
        },

        /**
         * click mouse Event handler function
         * @protected
         * @method onMouseDown
         * @param {MouseEvent} event
         */
        onMouseDown: function (event) {
            //this._clickOnScene(event, true);
            this.onTouchStart(event);
        },

        /**
         * touch down Event handler function
         * @protected
         * @method onTouchStart
         * @param {Event} event
         */
        onTouchStart: function (event) {
            if (event.preventManipulation)
                event.preventManipulation();
            event.preventDefault();
            event.stopPropagation();

            if (!cgsgExist(this._timerDblTouch)) {
                this._mousePosition = cgsgGetCursorPositions(event, cgsgCanvas);
                this._selectedNode = this.sceneGraph.pickNode(this._mousePosition[0], function (node) {
                    return cgsgExist(node.onDblClick);
                });
                this._tmpSelectedNode = this._selectedNode;
            }
            else {
                this._selectedNode = this._tmpSelectedNode;
            }

            //if the touch was over a node with the onDblClick method defined, check whether it's a dbl touch or not
            if (cgsgExist(this._selectedNode)) {
                //if the timer exists, then it's a dbl touch
                if (cgsgExist(this._timerDblTouch)) {
                    clearTimeout(this._timerDblTouch);
                    this._timerDblTouch = null;
                    this._dblClickOnScene(event);
                }
                else {
                    var that = this;
                    var evt = event;
                    this._timerDblTouch = setTimeout(function () {
                        clearTimeout(that._timerDblTouch);
                        that._timerDblTouch = null;
                        that._clickOnScene(evt, true);
                    }, this.dblTouchDelay);
                }
            }
            else {
                //not a touch on a node with the onDblClick event defined,
                // so it's a single touch, just call the _clickOnScene method usually
                this._clickOnScene(event, true);
            }
        },

        /**
         * @private
         * @method _clickOnScene
         * @param {Event} event MouseEvent or TouchEvent
         * @param {Boolean} mustPickNode
         */
        _clickOnScene: function (event, mustPickNode) {
            this._mousePosition = cgsgGetCursorPositions(event, cgsgCanvas);

            if (this.onSceneClickStart !== null) {
                this.onSceneClickStart({position: this._mousePosition.copy(), event: event});
            }

            //if the mouse cursor is over a handle box (ie: a resize marker)
            if (this._resizingDirection !== -1) {
                this._mouseOldPosition = this._mousePosition.copy();
                this._isResizeDrag = true;
                if (this.onSceneClickEnd !== null) {
                    this.onSceneClickEnd({position: this._mousePosition.copy(), event: event});
                }
                return;
            }

            //try to pick up the nodes under the cursor
            if (mustPickNode) {
                this._selectedNode = this.sceneGraph.pickNode(this._mousePosition[0], function (node) {
                    return (node.isTraversable === true && (node.isClickable === true || node.isDraggable === true
                        || node.isResizable === true));
                });
            }
            //if a node is under the cursor, select it if it is (clickable || reiszable || draggable)
            if (this._selectedNode !== null && this._selectedNode !== undefined &&
                (this._selectedNode.isClickable || this._selectedNode.isDraggable
                    || this._selectedNode.isResizable)) {
                if (this._selectedNode.isDraggable || this._selectedNode.isResizable) {

                    //if multiselection is activated
                    if (this.allowMultiSelect && this._keyDownedCtrl) {
                        if (!this._selectedNode.isSelected) {
                            this.sceneGraph.selectNode(this._selectedNode);
                        }
                        else {
                            this.sceneGraph.deselectNode(this._selectedNode);
                        }
                    }
                    //no multiselection
                    else {
                        //if node not already selected
                        if (!this._selectedNode.isSelected) {
                            this.deselectAll(null);
                            this.sceneGraph.selectNode(this._selectedNode);
                        }
                    }

                    this._isDrag = true;
                    this._isResizeDrag = false;

                    //ask for redraw
                    this.invalidate();
                }

                //execute the action bound with the click event
                if (this._selectedNode.isClickable) {
                    if (cgsgExist(this._selectedNode.onClick)) {
                        this._selectedNode.onClick({node: this._selectedNode, position: this._mousePosition.copy(), event: event});
                    }
                    //deselect all node except the new _selectedNode
                    if (this._selectedNode.isDraggable === false && this._selectedNode.isResizable === false) {
                        this.deselectAll([this._selectedNode]);
                    }
                }

            }
            //if no nodes were hit (that were clickable,reizeable or draggable) lets start a drag selection if we are allowed
            else if (this.allowMultiSelect) {

                this._isDragSelect = true;
                this._dragSelectStartMousePosition = cgsgGetCursorPositions(event, cgsgCanvas);
                this._dragSelectEndMousePosition = cgsgGetCursorPositions(event, cgsgCanvas);
                this.deselectAll(null);
            }
            //else if no nodes was clicked
            else {
                this.deselectAll(null);
            }

            if (this.onSceneClickEnd !== null) {
                this.onSceneClickEnd({position: this._mousePosition.copy(), event: event});
            }

            this._mouseOldPosition = this._mousePosition.copy();
        },

        /**
         * mouse move Event handler function
         * @protected
         * @method onMouseMove
         * @param {MouseEvent} event
         */
        onMouseMove: function (event) {
            this._moveOnScene(event);
        },

        /**
         * touch move Event handler function
         * @protected
         * @method onTouchMove
         * @param {Event} event
         */
        onTouchMove: function (event) {
            if (event.preventManipulation)
                event.preventManipulation();
            event.preventDefault();
            event.stopPropagation();
            this._moveOnScene(event);
        },

        /**
         * @private
         * @method _moveOnScene
         * @param {Event} event MouseEvent or TouchEvent
         */
        _moveOnScene: function (event) {
            var i, nodeOffsetX, nodeOffsetY;
            this._mousePosition = cgsgGetCursorPositions(event, cgsgCanvas);
            this._selectedNode = null;

            if (this._isDrag) {
                if (this.sceneGraph.selectedNodes.length > 0) {
                    this._offsetX = this._mousePosition[0].x - this._mouseOldPosition[0].x;
                    this._offsetY = this._mousePosition[0].y - this._mouseOldPosition[0].y;
                    var canMove = true;
                    for (i = this.sceneGraph.selectedNodes.length - 1; i >= 0; i--) {
                        this._selectedNode = this.sceneGraph.selectedNodes[i];
                        if (this._selectedNode !== null && this._selectedNode.isDraggable) {
                            this._selectedNode.isMoving = true;
                            //TODO : appliquer aussi l'opposée de la rotation
                            nodeOffsetX = this._offsetX /
                                (this._selectedNode._absoluteScale.x / this._selectedNode.scale.x);
                            nodeOffsetY = this._offsetY /
                                (this._selectedNode._absoluteScale.y / this._selectedNode.scale.y);
                            //check for the region constraint
                            if (this._selectedNode.regionConstraint !== null) {
                                var reg = this._selectedNode.getRegion().copy();
                                reg.position.x += nodeOffsetX;
                                reg.position.y += nodeOffsetY;
                                if (!cgsgRegionIsInRegion(reg, this._selectedNode.regionConstraint, 0)) {
                                    canMove = false;
                                }
                            }

                            if (canMove) {
                                this._selectedNode.translateWith(nodeOffsetX, nodeOffsetY, true);
                                //this._selectedNode.computeAbsoluteMatrix(true);
                                if (this._selectedNode.onDrag !== null) {
                                    this._selectedNode.onDrag({node: this._selectedNode, position: this._mousePosition.copy(), event: event});
                                }
                            }
                        }
                    }

                    this._mouseOldPosition = this._mousePosition.copy();

                    // something is changing position so we better invalidate the canvas!
                    this.invalidate();
                }
            }
            else if (this._isResizeDrag) {
                if (this.sceneGraph.selectedNodes.length > 0) {
                    this._offsetX = this._mousePosition[0].x - this._mouseOldPosition[0].x;
                    this._offsetY = this._mousePosition[0].y - this._mouseOldPosition[0].y;
                    for (i = this.sceneGraph.selectedNodes.length - 1; i >= 0; i--) {
                        this._selectedNode = this.sceneGraph.selectedNodes[i];
                        if (this._selectedNode.isResizable) {
                            this._selectedNode.isResizing = true;
                            //TODO : appliquer aussi l'opposée de la rotation
                            nodeOffsetX = this._offsetX / this._selectedNode._absoluteScale.x;
                            nodeOffsetY = this._offsetY / this._selectedNode._absoluteScale.y;

                            var delta = Math.max(nodeOffsetX, nodeOffsetY);
                            if (delta == 0) {
                                delta = Math.min(nodeOffsetX, nodeOffsetY);
                            }
                            var realDimX = this._selectedNode.dimension.width *
                                this._selectedNode._absoluteScale.x;
                            var realDimY = this._selectedNode.dimension.height *
                                this._selectedNode._absoluteScale.y;
                            var d = {dW: 0, dH: 0};
                            // 0  1  2
                            // 3     4
                            // 5  6  7
                            switch (this._resizingDirection) {
                                case 0:
                                    if (this._selectedNode.isProportionalResize) {
                                        d = this._getDeltaOnMove(delta, nodeOffsetX, nodeOffsetY, realDimX,
                                            realDimY,
                                            -1, -1);
                                        this._selectedNode.translateWith(-d.dW, -d.dH, false);
                                        this._selectedNode.resizeWith(d.dW, d.dH, false);
                                    }
                                    else {
                                        this._selectedNode.translateWith(nodeOffsetX * this._selectedNode.scale.x,
                                            nodeOffsetY * this._selectedNode.scale.y,
                                            false);
                                        this._selectedNode.resizeWith(-nodeOffsetX, -nodeOffsetY, false);
                                    }
                                    break;
                                case 1:
                                    this._selectedNode.translateWith(0, nodeOffsetY * this._selectedNode.scale.y,
                                        false);
                                    this._selectedNode.resizeWith(0, -nodeOffsetY, false);
                                    break;
                                case 2:
                                    if (this._selectedNode.isProportionalResize) {
                                        d = this._getDeltaOnMove(delta, nodeOffsetX, nodeOffsetY, realDimX,
                                            realDimY,
                                            1, -1);
                                        this._selectedNode.translateWith(0, -d.dH, false);
                                        this._selectedNode.resizeWith(d.dW, d.dH, false);
                                    }
                                    else {
                                        this._selectedNode.translateWith(0,
                                            nodeOffsetY * this._selectedNode.scale.y,
                                            false);
                                        this._selectedNode.resizeWith(nodeOffsetX, -nodeOffsetY, false);
                                    }
                                    break;
                                case 3:
                                    this._selectedNode.translateWith(nodeOffsetX * this._selectedNode.scale.x, 0,
                                        false);
                                    this._selectedNode.resizeWith(-nodeOffsetX, 0, false);
                                    break;
                                case 4:
                                    this._selectedNode.resizeWith(nodeOffsetX, 0, false);
                                    break;
                                case 5:
                                    if (this._selectedNode.isProportionalResize) {
                                        d = this._getDeltaOnMove(delta, nodeOffsetX, nodeOffsetY, realDimX,
                                            realDimY,
                                            1, -1);
                                        this._selectedNode.translateWith(d.dW, 0, false);
                                        this._selectedNode.resizeWith(-d.dW, -d.dH, false);
                                    }
                                    else {
                                        this._selectedNode.translateWith(nodeOffsetX * this._selectedNode.scale.x,
                                            0,
                                            false);
                                        this._selectedNode.resizeWith(-nodeOffsetX, nodeOffsetY, false);
                                    }
                                    break;
                                case 6:
                                    this._selectedNode.resizeWith(0, nodeOffsetY, false);
                                    break;
                                case 7:
                                    if (this._selectedNode.isProportionalResize) {
                                        d = this._getDeltaOnMove(delta, nodeOffsetX, nodeOffsetY, realDimX,
                                            realDimY,
                                            1, 1);
                                        this._selectedNode.resizeWith(d.dW, d.dH, false);
                                    }
                                    else {
                                        this._selectedNode.resizeWith(nodeOffsetX, nodeOffsetY, false);
                                    }
                                    break;
                            }
                            this._selectedNode.computeAbsoluteMatrix(true);
                            if (this._selectedNode.onResize !== null) {
                                this._selectedNode.onResize({node: this._selectedNode, position: this._mousePosition.copy(), event: event});
                            }
                        }
                    }
                }
                this._mouseOldPosition = this._mousePosition.copy();

                this.invalidate();
            }
            // if there's a selection, see if we grabbed one of the resize handles
            else if (this.sceneGraph.selectedNodes.length > 0 && this._isResizeDrag == false) {
                for (i = this.sceneGraph.selectedNodes.length - 1; i >= 0; i--) {
                    this._selectedNode = this.sceneGraph.selectedNodes[i];
                    if (this._selectedNode.isResizable) {
                        for (var h = 0; h < 8; h++) {
                            var selectionHandle = this._selectedNode.resizeHandles[h];

                            // resize handles will always be rectangles
                            if (selectionHandle.checkIfSelected(this._mousePosition[0],
                                cgsgResizeHandleThreshold)) {
                                // we found one!
                                this._resizingDirection = h;

                                //draw the correct cursor
                                cgsgCanvas.style.cursor = this._listCursors[h];

                                return;
                            }
                        }
                    }
                }

                // not over a selection box, return to normal
                this._isResizeDrag = false;
                this._resizingDirection = -1;
                cgsgCanvas.style.cursor = 'auto';

                //ask for redraw
                this.invalidate();
            }
            //if we are drag selecting
            else if (this._isDragSelect) {
                this._dragSelectEndMousePosition = this._mousePosition.copy();

                //ask to redraw for the selection box
                this.invalidate();
            }

            //mouse over a node ?
            if (!this._isDrag && !this._isResizeDrag) {
                var n = null;
                //first test the mouse over the current _nodeMouseOver. If it's ok, no need to traverse other
                if (cgsgExist(this._nodeMouseOver)) {
                    n = this._nodeMouseOver.pickNode(this._mousePosition[0], null, cgsgGhostContext, false, null);

                    if (n === null) {
                        this._nodeMouseOver.isMouseOver = false;
                        if (cgsgExist(this._nodeMouseOver.onMouseOut)) {
                            this._nodeMouseOver.onMouseOut({node: this._nodeMouseOver, position: this._mousePosition.copy(), event: event});
                        }
                        this._nodeMouseOver = null;
                    }
                    else if (n === this._nodeMouseOver) {
                        if (cgsgExist(this._nodeMouseOver.onMouseOver)) {
                            this._nodeMouseOver.onMouseOver({node: this._nodeMouseOver, position: this._mousePosition.copy(), event: event});
                        }
                    }
                }

                //if the previous node under the mouse is no more under the mouse, test the other nodes
                if (n === null) {
                    if ((n = this.sceneGraph.pickNode(this._mousePosition[0], function (node) {
                        return (node.onMouseEnter !== null || node.onMouseOver !== null)
                    })) !== null) {
                        n.isMouseOver = true;
                        this._nodeMouseOver = n;
                        this._nodeMouseOver.isMouseOver = true;
                        if (cgsgExist(this._nodeMouseOver.onMouseEnter)) {
                            this._nodeMouseOver.onMouseEnter({node: this._nodeMouseOver, position: this._mousePosition.copy(), event: event})
                        }
                    }
                }

            }
        },

        /**
         * @method _getDeltaOnMove
         * @param delta
         * @param nodeOffsetX
         * @param nodeOffsetY
         * @param w
         * @param h
         * @param signeX
         * @param signeY
         * @return {Object}
         * @private
         */
        _getDeltaOnMove: function (delta, nodeOffsetX, nodeOffsetY, w, h, signeX, signeY) {
            var dW = nodeOffsetX, dH = nodeOffsetY;
            var ratio = 1.0;
            if (delta == nodeOffsetX) {
                ratio = (w + signeX * delta) / w;
                dW = signeX * delta;
                dH = (ratio - 1.0) * h;
            }
            else {
                ratio = (h + signeY * delta) / h;
                dH = signeY * delta;
                dW = (ratio - 1.0) * w;
            }

            return {dW: dW, dH: dH};
        },

        /**
         * mouse up Event handler function
         * @protected
         * @method onMouseUp
         * @param {MouseEvent} event
         */
        onMouseUp: function (event) {
            this._upOnScene(event);
        },

        /**
         * touch up Event handler function
         * @protected
         * @method onTouchEnd
         * @param {Event} event
         */
        onTouchEnd: function (event) {
            if (event.preventManipulation)
                event.preventManipulation();
            event.preventDefault();
            event.stopPropagation();
            this._upOnScene(event);
        },

        /**
         * @method _upOnScene
         * @param {Event} event MouseEvent or TouchEvent
         * @private
         */
        _upOnScene: function (event) {
            var i = 0;

            //if current action was to drag nodes
            if (this._isDrag) {
                for (i = this.sceneGraph.selectedNodes.length - 1; i >= 0; i--) {
                    this._selectedNode = this.sceneGraph.selectedNodes[i];
                    if (this._selectedNode.isMoving) {
                        this._selectedNode.isMoving = false;
                        this._selectedNode.computeAbsoluteMatrix(true);
                        if (this._selectedNode.onDragEnd !== null) {
                            this._selectedNode.onDragEnd({node: this._selectedNode, position: this._mousePosition.copy(), event: event});
                        }
                    }
                }
                this._isDrag = false;
            }

            //else if current action was to resize nodes
            else if (this._isResizeDrag) {
                for (i = this.sceneGraph.selectedNodes.length - 1; i >= 0; i--) {
                    this._selectedNode = this.sceneGraph.selectedNodes[i];
                    if (this._selectedNode.isResizing) {
                        this._selectedNode.isResizing = false;
                        this._selectedNode.computeAbsoluteMatrix(true);
                        if (this._selectedNode.onResizeEnd !== null) {
                            this._selectedNode.onResizeEnd({node: this._selectedNode, position: this._mousePosition.copy(), event: event});
                        }
                    }
                }
                this._isResizeDrag = false;
            }
            //else if this is a drag select
            else if (this._isDragSelect) {
                this._isDragSelect = false;
                this._doDragSelect();
                this._dragSelectStartMousePosition = [];
                this._dragSelectEndMousePosition = [];

                //request a re-render for the drag select rect to be killed with
                this.invalidate();
            }

            //else if jst up the mice of nodes
            else {
                this._selectedNode = this.sceneGraph.selectedNodes[this.sceneGraph.selectedNodes.length - 1];
                if (cgsgExist(this._selectedNode) && this._selectedNode.onMouseUp !== null) {
                    this._selectedNode.onMouseUp({node: this._selectedNode, position: this._mousePosition.copy(), event: event});
                }
            }

            this._resizingDirection = -1;
        },

        /**
         * Select the nodes under the drag select rectangle
         * @protected
         * @method _doDragSelect
         */
        _doDragSelect: function () {

            var p1 = this._dragSelectStartMousePosition[0];
            var p2 = this._dragSelectEndMousePosition[0];

            var dx = p2.x - p1.x, dy = p2.y - p1.y;

            if (dx < 0) {
                dx = Math.abs(dx);
                p1.x -= dx;
            }

            if (dy < 0) {
                dy = Math.abs(dy);
                p1.y -= dy;
            }

            var region = new CGSGRegion(p1.x, p1.y, dx, dy);
            var newSelections = this.sceneGraph.pickNodes(region, function (node) {
                return (node.isTraversable === true && (/*node.isClickable === true ||*/ node.isDraggable === true
                    || node.isResizable === true))
            });

            for (var i = 0, len = newSelections.length; i < len; ++i) {
                this.sceneGraph.selectNode(newSelections[i]);
            }
        },
        /**
         * mouse double click Event handler function
         * @protected
         * @method onMouseDblClick
         * @param {MouseEvent} event
         */
        onMouseDblClick: function (event) {
            //this._dblClickOnScene(event);
            event.preventDefault();
            event.stopPropagation();
        },

        /**
         * @protected
         * @method _dblClickOnScene
         * @param {Event} event
         * @return {CGSGNode} the node that was double-clicked
         * @private
         */
        _dblClickOnScene: function (event) {
            if (this.onSceneDblClickStart !== null) {
                this.onSceneDblClickStart(event);
            }
            this._mousePosition = cgsgGetCursorPositions(event, cgsgCanvas);
            this._selectedNode = this.sceneGraph.pickNode(this._mousePosition[0], function (node) {
                return true;
            });
            if (cgsgExist(this._selectedNode) && this._selectedNode.onDblClick !== null) {
                this._selectedNode.onDblClick({node: this._selectedNode, position: this._mousePosition.copy(), event: event});
            }
            else if (this.onSceneDblClickEnd !== null) {
                this.onSceneDblClickEnd({position: this._mousePosition.copy(), event: event});
            }
            return this._selectedNode;
        },

        /**
         * @method onKeyDownHandler
         * @protected
         * @param {KeyboardEvent} event
         * @return {Number}
         */
        onKeyDownHandler: function (event) {
            var keynum = (window.event) ? event.keyCode : event.which;

            switch (keynum) {
                case 17:
                    this._keyDownedCtrl = true;
                    break;
            }

            return keynum;
        },

        /**
         * @method onKeyUpHandler
         * @protected
         * @param {KeyboardEvent} event
         * @return {Number}
         */
        onKeyUpHandler: function (event) {
            var keynum = (window.event) ? event.keyCode : event.which;

            switch (keynum) {
                case 17:
                    this._keyDownedCtrl = false;
                    break;
            }

            return keynum;
        }
    }
);