/////////////////////////////////////////////////////////////////////// // Stage /////////////////////////////////////////////////////////////////////// /** * Stage constructor. A stage is used to contain multiple layers * @constructor * @augments Kinetic.Container * @param {Object} config * @param {String|DomElement} config.container Container id or DOM element * @param {Number} config.width * @param {Number} config.height * @param {Number} [config.x] * @param {Number} [config.y] * @param {Boolean} [config.visible] * @param {Boolean} [config.listening] whether or not the node is listening for events * @param {String} [config.id] unique id * @param {String} [config.name] non-unique name * @param {Number} [config.alpha] determines node opacity. Can be any number between 0 and 1 * @param {Object} [config.scale] * @param {Number} [config.scale.x] * @param {Number} [config.scale.y] * @param {Number} [config.rotation] rotation in radians * @param {Number} [config.rotationDeg] rotation in degrees * @param {Object} [config.offset] offsets default position point and rotation point * @param {Number} [config.offset.x] * @param {Number} [config.offset.y] * @param {Boolean} [config.draggable] * @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default * is none * @param {Object} [config.dragBounds] * @param {Number} [config.dragBounds.top] * @param {Number} [config.dragBounds.right] * @param {Number} [config.dragBounds.bottom] * @param {Number} [config.dragBounds.left] */ Kinetic.Stage = Kinetic.Container.extend({ init: function(config) { this.setDefaultAttrs({ width: 400, height: 200 }); /* * if container is a string, assume it's an id for * a DOM element */ if( typeof config.container === 'string') { config.container = document.getElementById(config.container); } // call super constructor this._super(config); this._setStageDefaultProperties(); this._id = Kinetic.Global.idCounter++; this._buildDOM(); this._bindContentEvents(); //change events this.on('widthChange.kinetic', function() { this._resizeDOM(); }); this.on('heightChange.kinetic', function() { this._resizeDOM(); }); var go = Kinetic.Global; go.stages.push(this); this._addId(this); this._addName(this); }, /** * draw children * @name draw * @methodOf Kinetic.Stage.prototype */ draw: function(canvas) { this._draw(canvas); }, /** * set stage size * @name setSize * @methodOf Kinetic.Stage.prototype * @param {Number} width * @param {Number} height */ setSize: function() { // set stage dimensions var size = Kinetic.Type._getSize(Array.prototype.slice.call(arguments)); this.setAttrs(size); }, /** * get stage size * @name getSize * @methodOf Kinetic.Stage.prototype */ getSize: function() { return { width: this.attrs.width, height: this.attrs.height }; }, /** * clear all layers * @name clear * @methodOf Kinetic.Stage.prototype */ clear: function() { var layers = this.children; for(var n = 0; n < layers.length; n++) { layers[n].clear(); } }, /** * serialize stage and children as a JSON object and return * the result as a json string * @name toJSON * @methodOf Kinetic.Stage.prototype */ toJSON: function() { var type = Kinetic.Type; function addNode(node) { var obj = {}; obj.attrs = {}; // serialize only attributes that are not function, image, DOM, or objects with methods for(var key in node.attrs) { var val = node.attrs[key]; if(!type._isFunction(val) && !type._isElement(val) && !type._hasMethods(val)) { obj.attrs[key] = val; } } obj.nodeType = node.nodeType; obj.shapeType = node.shapeType; if(node.nodeType !== 'Shape') { obj.children = []; var children = node.getChildren(); for(var n = 0; n < children.length; n++) { var child = children[n]; obj.children.push(addNode(child)); } } return obj; } return JSON.stringify(addNode(this)); }, /** * reset stage to default state * @name reset * @methodOf Kinetic.Stage.prototype */ reset: function() { // remove children this.removeChildren(); // defaults this._setStageDefaultProperties(); this.setAttrs(this.defaultNodeAttrs); }, /** * load stage with JSON string. De-serializtion does not generate custom * shape drawing functions, images, or event handlers (this would make the * serialized object huge). If your app uses custom shapes, images, and * event handlers (it probably does), then you need to select the appropriate * shapes after loading the stage and set these properties via on(), setDrawFunc(), * and setImage() * @name load * @methodOf Kinetic.Stage.prototype * @param {String} JSON string */ load: function(json) { this.reset(); function loadNode(node, obj) { var children = obj.children; if(children !== undefined) { for(var n = 0; n < children.length; n++) { var child = children[n]; var type; // determine type if(child.nodeType === 'Shape') { // add custom shape if(child.shapeType === undefined) { type = 'Shape'; } // add standard shape else { type = child.shapeType; } } else { type = child.nodeType; } var no = new Kinetic[type](child.attrs); node.add(no); loadNode(no, child); } } } var obj = JSON.parse(json); // copy over stage properties this.attrs = obj.attrs; loadNode(this, obj); this.draw(); }, /** * get mouse position for desktop apps * @name getMousePosition * @methodOf Kinetic.Stage.prototype * @param {Event} evt */ getMousePosition: function(evt) { return this.mousePos; }, /** * get touch position for mobile apps * @name getTouchPosition * @methodOf Kinetic.Stage.prototype * @param {Event} evt */ getTouchPosition: function(evt) { return this.touchPos; }, /** * get user position (mouse position or touch position) * @name getUserPosition * @methodOf Kinetic.Stage.prototype * @param {Event} evt */ getUserPosition: function(evt) { return this.getTouchPosition() || this.getMousePosition(); }, /** * get container DOM element * @name getContainer * @methodOf Kinetic.Stage.prototype */ getContainer: function() { return this.attrs.container; }, /** * get stage * @name getStage * @methodOf Kinetic.Stage.prototype */ getStage: function() { return this; }, /** * get stage DOM node, which is a div element * with the class name "kineticjs-content" * @name getDOM * @methodOf Kinetic.Stage.prototype */ getDOM: function() { return this.content; }, /** * Creates a composite data URL and requires a callback because the stage * toDataURL method is asynchronous. If MIME type is not * specified, then "image/png" will result. For "image/jpeg", specify a quality * level as quality (range 0.0 - 1.0). Note that this method works * differently from toDataURL() for other nodes because it generates an absolute dataURL * based on what's draw onto the canvases for each layer, rather than drawing * the current state of each node * @name toDataURL * @methodOf Kinetic.Stage.prototype * @param {Object} config * @param {Function} config.callback since the stage toDataURL() method is asynchronous, * the data url string will be passed into the callback * @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg". * "image/png" is the default * @param {Number} [config.width] data url image width * @param {Number} [config.height] data url image height * @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType, * you can specify the quality from 0 to 1, where 0 is very poor quality and 1 * is very high quality */ toDataURL: function(config) { var mimeType = config && config.mimeType ? config.mimeType : null; var quality = config && config.quality ? config.quality : null; /* * need to create a canvas element rather than using the buffer canvas * because this method is asynchonous which means that other parts of the * code could modify the buffer canvas before it's finished */ var width = config && config.width ? config.width : this.attrs.width; var height = config && config.height ? config.height : this.attrs.height; var canvas = new Kinetic.Canvas(width, height); var context = canvas.getContext(); var layers = this.children; function drawLayer(n) { var layer = layers[n]; var layerUrl = layer.getCanvas().toDataURL(); var imageObj = new Image(); imageObj.onload = function() { context.drawImage(imageObj, 0, 0); if(n < layers.length - 1) { drawLayer(n + 1); } else { config.callback(canvas.toDataURL(mimeType, quality)); } }; imageObj.src = layerUrl; } drawLayer(0); }, /** * converts stage into an image. Since the stage toImage() method * is asynchronous, a callback function is required * @name toImage * @methodOf Kinetic.Stage.prototype * @param {Object} config * @param {Function} callback since the toImage() method is asynchonrous, the * resulting image object is passed into the callback function * @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg". * "image/png" is the default * @param {Number} [config.width] data url image width * @param {Number} [config.height] data url image height * @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType, * you can specify the quality from 0 to 1, where 0 is very poor quality and 1 * is very high quality */ toImage: function(config) { this.toDataURL({ callback: function(dataUrl) { Kinetic.Type._getImage(dataUrl, function(img) { config.callback(img); }); } }); }, _resizeDOM: function() { var width = this.attrs.width; var height = this.attrs.height; // set content dimensions this.content.style.width = width + 'px'; this.content.style.height = height + 'px'; // set buffer canvas and path canvas sizes this.bufferCanvas.setSize(width, height); this.pathCanvas.setSize(width, height); // set user defined layer dimensions var layers = this.children; for(var n = 0; n < layers.length; n++) { var layer = layers[n]; layer.getCanvas().setSize(width, height); layer.draw(); } }, /** * remove layer from stage * @param {Layer} layer */ _remove: function(layer) { /* * remove canvas DOM from the document if * it exists */ try { this.content.removeChild(layer.canvas.element); } catch(e) { } }, /** * add layer to stage * @param {Layer} layer */ _add: function(layer) { layer.canvas.setSize(this.attrs.width, this.attrs.height); // draw layer and append canvas to container layer.draw(); this.content.appendChild(layer.canvas.element); /* * set layer last draw time to zero * so that throttling doesn't take into account * the layer draws associated with adding a node */ layer.lastDrawTime = 0; }, /** * detect event * @param {Shape} shape */ _detectEvent: function(shape, evt) { var isDragging = Kinetic.Global.drag.moving; var go = Kinetic.Global; var pos = this.getUserPosition(); var el = shape.eventListeners; var that = this; if(this.targetShape && shape._id === this.targetShape._id) { this.targetFound = true; } // handle onmousedown if(!isDragging && this.mouseDown) { this.mouseDown = false; this.clickStart = true; shape._handleEvent('mousedown', evt); } // handle onmouseup & onclick else if(this.mouseUp) { this.mouseUp = false; shape._handleEvent('mouseup', evt); // detect if click or double click occurred if(this.clickStart) { /* * if dragging and dropping, don't fire click or dbl click * event */ if((!go.drag.moving) || !go.drag.node) { shape._handleEvent('click', evt); if(this.inDoubleClickWindow) { shape._handleEvent('dblclick', evt); } this.inDoubleClickWindow = true; setTimeout(function() { that.inDoubleClickWindow = false; }, this.dblClickWindow); } } } // handle touchstart else if(!isDragging && this.touchStart && !this.touchMove) { this.touchStart = false; this.tapStart = true; shape._handleEvent('touchstart', evt); } // handle touchend & tap else if(this.touchEnd) { this.touchEnd = false; shape._handleEvent('touchend', evt); // detect if tap or double tap occurred if(this.tapStart) { /* * if dragging and dropping, don't fire tap or dbltap * event */ if((!go.drag.moving) || !go.drag.node) { shape._handleEvent('tap', evt); if(this.inDoubleClickWindow) { shape._handleEvent('dbltap', evt); } this.inDoubleClickWindow = true; setTimeout(function() { that.inDoubleClickWindow = false; }, this.dblClickWindow); } } } else if(!isDragging && this.touchMove) { shape._handleEvent('touchmove', evt); } /* * NOTE: these event handlers require target shape * handling */ // handle onmouseover else if(!isDragging && this._isNewTarget(shape, evt)) { /* * check to see if there are stored mouseout events first. * if there are, run those before running the onmouseover * events */ if(this.mouseoutShape) { this.mouseoverShape = shape; this.mouseoutShape._handleEvent('mouseout', evt); this.mouseoverShape = undefined; } shape._handleEvent('mouseover', evt); this._setTarget(shape); } // handle mousemove and touchmove else { if(!isDragging && this.mouseMove) { shape._handleEvent('mousemove', evt); } } }, /** * set new target */ _setTarget: function(shape) { this.targetShape = shape; this.targetFound = true; }, /** * check if shape should be a new target */ _isNewTarget: function(shape, evt) { if(!this.targetShape || (!this.targetFound && shape._id !== this.targetShape._id)) { /* * check if old target has an onmouseout event listener */ if(this.targetShape) { var oldEl = this.targetShape.eventListeners; if(oldEl) { this.mouseoutShape = this.targetShape; } } return true; } else { return false; } }, /** * handle incoming event * @param {Event} evt */ _handleStageEvent: function(evt) { var go = Kinetic.Global; if(!evt) { evt = window.event; } this._setMousePosition(evt); this._setTouchPosition(evt); this.targetFound = false; var pos = this.getUserPosition(); var p = this.pathCanvas.context.getImageData(pos.x, pos.y, 1, 1).data; var colorKey = Kinetic.Type._rgbToHex(p[0], p[1], p[2]); var shape = Kinetic.Global.shapes[colorKey]; var isDragging = Kinetic.Global.drag.moving; if(shape) { this._detectEvent(shape, evt); } // handle mouseout condition /* else if(!isDragging && this.targetShape && this.targetShape._id === shape._id) { this._setTarget(undefined); this.mouseoutShape = shape; } */ /* * if no shape was detected and a mouseout shape has been stored, * then run the onmouseout event handlers */ if(!shape && this.mouseoutShape) { this.mouseoutShape._handleEvent('mouseout', evt); this.mouseoutShape = undefined; } }, /** * begin listening for events by adding event handlers * to the container */ _bindContentEvents: function() { var go = Kinetic.Global; var that = this; var events = ['mousedown', 'mousemove', 'mouseup', 'mouseover', 'mouseout', 'touchstart', 'touchmove', 'touchend']; for(var n = 0; n < events.length; n++) { var pubEvent = events[n]; // induce scope ( function() { var event = pubEvent; that.content.addEventListener(event, function(evt) { that['_' + event](evt); }, false); }()); } }, _mouseover: function(evt) { this._handleStageEvent(evt); }, _mouseout: function(evt) { // if there's a current target shape, run mouseout handlers var targetShape = this.targetShape; if(targetShape) { targetShape._handleEvent('mouseout', evt); this.targetShape = undefined; } this.mousePos = undefined; // end drag and drop this._endDrag(evt); }, _mousemove: function(evt) { this.mouseDown = false; this.mouseUp = false; this.mouseMove = true; this._handleStageEvent(evt); // start drag and drop this._startDrag(evt); }, _mousedown: function(evt) { this.mouseDown = true; this.mouseUp = false; this.mouseMove = false; this._handleStageEvent(evt); //init stage drag and drop if(this.attrs.draggable) { this._initDrag(); } }, _mouseup: function(evt) { this.mouseDown = false; this.mouseUp = true; this.mouseMove = false; this._handleStageEvent(evt); this.clickStart = false; // end drag and drop this._endDrag(evt); }, _touchstart: function(evt) { evt.preventDefault(); this.touchStart = true; this.touchEnd = false; this.touchMove = false; this._handleStageEvent(evt); /* * init stage drag and drop */ if(this.attrs.draggable) { this._initDrag(); } }, _touchend: function(evt) { this.touchStart = false; this.touchEnd = true; this.touchMove = false; this._handleStageEvent(evt); this.tapStart = false; // end drag and drop this._endDrag(evt); }, _touchmove: function(evt) { evt.preventDefault(); this.touchEnd = false; this.touchMove = true; this._handleStageEvent(evt); // start drag and drop this._startDrag(evt); }, /** * set mouse positon for desktop apps * @param {Event} evt */ _setMousePosition: function(evt) { var mouseX = evt.offsetX || (evt.clientX - this._getContentPosition().left + window.pageXOffset); var mouseY = evt.offsetY || (evt.clientY - this._getContentPosition().top + window.pageYOffset); this.mousePos = { x: mouseX, y: mouseY }; }, /** * set touch position for mobile apps * @param {Event} evt */ _setTouchPosition: function(evt) { if(evt.touches !== undefined && evt.touches.length === 1) {// Only deal with // one finger var touch = evt.touches[0]; // Get the information for finger #1 var touchX = touch.clientX - this._getContentPosition().left + window.pageXOffset; var touchY = touch.clientY - this._getContentPosition().top + window.pageYOffset; this.touchPos = { x: touchX, y: touchY }; } }, /** * get container position */ _getContentPosition: function() { var rect = this.content.getBoundingClientRect(), root = document.documentElement; return { top: rect.top + root.scrollTop, left: rect.left + root.scrollLeft }; }, /** * end drag and drop */ _endDrag: function(evt) { var go = Kinetic.Global; var node = go.drag.node; if(node) { if(node.nodeType === 'Stage') { node.draw(); } else { node.getLayer().draw(); } // handle dragend if(go.drag.moving) { go.drag.moving = false; node._handleEvent('dragend', evt); } } go.drag.node = null; this.dragAnim.stop(); }, /** * start drag and drop */ _startDrag: function(evt) { var that = this; var go = Kinetic.Global; var node = go.drag.node; if(node) { var pos = that.getUserPosition(); var dc = node.attrs.dragConstraint; var db = node.attrs.dragBounds; var lastNodePos = { x: node.attrs.x, y: node.attrs.y }; // default var newNodePos = { x: pos.x - go.drag.offset.x, y: pos.y - go.drag.offset.y }; // bounds overrides if(db.left !== undefined && newNodePos.x < db.left) { newNodePos.x = db.left; } if(db.right !== undefined && newNodePos.x > db.right) { newNodePos.x = db.right; } if(db.top !== undefined && newNodePos.y < db.top) { newNodePos.y = db.top; } if(db.bottom !== undefined && newNodePos.y > db.bottom) { newNodePos.y = db.bottom; } node.setAbsolutePosition(newNodePos); // constraint overrides if(dc === 'horizontal') { node.attrs.y = lastNodePos.y; } else if(dc === 'vertical') { node.attrs.x = lastNodePos.x; } if(!go.drag.moving) { go.drag.moving = true; // execute dragstart events if defined go.drag.node._handleEvent('dragstart', evt); } // execute user defined ondragmove if defined go.drag.node._handleEvent('dragmove', evt); } }, /** * build dom */ _buildDOM: function() { // content this.content = document.createElement('div'); this.content.style.position = 'relative'; this.content.style.display = 'inline-block'; this.content.className = 'kineticjs-content'; this.attrs.container.appendChild(this.content); this.bufferCanvas = new Kinetic.Canvas({ width: this.attrs.width, height: this.attrs.height }); this.pathCanvas = new Kinetic.Canvas({ width: this.attrs.width, height: this.attrs.height }); this.pathCanvas.name = 'pathCanvas'; this.pathCanvas.strip(); this._resizeDOM(); }, _addId: function(node) { if(node.attrs.id !== undefined) { this.ids[node.attrs.id] = node; } }, _removeId: function(id) { if(id !== undefined) { delete this.ids[id]; } }, _addName: function(node) { var name = node.attrs.name; if(name !== undefined) { if(this.names[name] === undefined) { this.names[name] = []; } this.names[name].push(node); } }, _removeName: function(name, _id) { if(name !== undefined) { var nodes = this.names[name]; if(nodes !== undefined) { for(var n = 0; n < nodes.length; n++) { var no = nodes[n]; if(no._id === _id) { nodes.splice(n, 1); } } if(nodes.length === 0) { delete this.names[name]; } } } }, /** * bind event listener to container DOM element * @param {String} typesStr * @param {function} handler */ _onContent: function(typesStr, handler) { var types = typesStr.split(' '); for(var n = 0; n < types.length; n++) { var baseEvent = types[n]; this.content.addEventListener(baseEvent, handler, false); } }, /** * set defaults */ _setStageDefaultProperties: function() { this.nodeType = 'Stage'; this.dblClickWindow = 400; this.targetShape = undefined; this.targetFound = false; this.mouseoverShape = undefined; this.mouseoutShape = undefined; // desktop flags this.mousePos = undefined; this.mouseDown = false; this.mouseUp = false; this.mouseMove = false; this.clickStart = false; // mobile flags this.touchPos = undefined; this.touchStart = false; this.touchEnd = false; this.touchMove = false; this.tapStart = false; this.ids = {}; this.names = {}; this.dragAnim = new Kinetic.Animation(); }, _draw: function(canvas) { this._drawChildren(canvas); } }); // add getters and setters Kinetic.Node.addGettersSetters(Kinetic.Stage, ['width', 'height']); /** * get width * @name getWidth * @methodOf Kinetic.Stage.prototype */ /** * get height * @name getHeight * @methodOf Kinetic.Stage.prototype */ /** * set width * @name setWidth * @methodOf Kinetic.Stage.prototype * @param {Number} width */ /** * set height * @name setHeight * @methodOf Kinetic.Stage.prototype * @param {Number} height */