(function() { // CONSTANTS var STAGE = 'Stage', STRING = 'string', PX = 'px', MOUSEOUT = 'mouseout', MOUSELEAVE = 'mouseleave', MOUSEOVER = 'mouseover', MOUSEENTER = 'mouseenter', MOUSEMOVE = 'mousemove', MOUSEDOWN = 'mousedown', MOUSEUP = 'mouseup', CLICK = 'click', DBL_CLICK = 'dblclick', TOUCHSTART = 'touchstart', TOUCHEND = 'touchend', TAP = 'tap', DBL_TAP = 'dbltap', TOUCHMOVE = 'touchmove', CONTENT_MOUSEOUT = 'contentMouseout', CONTENT_MOUSELEAVE = 'contentMouseleave', CONTENT_MOUSEOVER = 'contentMouseover', CONTENT_MOUSEENTER = 'contentMouseenter', CONTENT_MOUSEMOVE = 'contentMousemove', CONTENT_MOUSEDOWN = 'contentMousedown', CONTENT_MOUSEUP = 'contentMouseup', CONTENT_CLICK = 'contentClick', CONTENT_DBL_CLICK = 'contentDblclick', CONTENT_TOUCHSTART = 'contentTouchstart', CONTENT_TOUCHEND = 'contentTouchend', CONTENT_TAP = 'contentTap', CONTENT_DBL_TAP = 'contentDbltap', CONTENT_TOUCHMOVE = 'contentTouchmove', DIV = 'div', RELATIVE = 'relative', INLINE_BLOCK = 'inline-block', KINETICJS_CONTENT = 'kineticjs-content', SPACE = ' ', UNDERSCORE = '_', CONTAINER = 'container', EMPTY_STRING = '', EVENTS = [MOUSEDOWN, MOUSEMOVE, MOUSEUP, MOUSEOUT, TOUCHSTART, TOUCHMOVE, TOUCHEND, MOUSEOVER], // cached variables eventsLength = EVENTS.length; function addEvent(ctx, eventName) { ctx.content.addEventListener(eventName, function(evt) { ctx[UNDERSCORE + eventName](evt); }, false); } Kinetic.Util.addMethods(Kinetic.Stage, { ___init: function(config) { this.nodeType = STAGE; // call super constructor Kinetic.Container.call(this, config); this._id = Kinetic.idCounter++; this._buildDOM(); this._bindContentEvents(); this._enableNestedTransforms = false; Kinetic.stages.push(this); }, _validateAdd: function(child) { if (child.getType() !== 'Layer') { Kinetic.Util.error('You may only add layers to the stage.'); } }, /** * set container dom element which contains the stage wrapper div element * @method * @memberof Kinetic.Stage.prototype * @param {DomElement} container can pass in a dom element or id string */ setContainer: function(container) { if( typeof container === STRING) { container = document.getElementById(container); } this._setAttr(CONTAINER, container); return this; }, shouldDrawHit: function() { return true; }, draw: function() { Kinetic.Node.prototype.draw.call(this); return this; }, /** * draw layer scene graphs * @name draw * @method * @memberof Kinetic.Stage.prototype */ /** * draw layer hit graphs * @name drawHit * @method * @memberof Kinetic.Stage.prototype */ /** * set height * @method * @memberof Kinetic.Stage.prototype * @param {Number} height */ setHeight: function(height) { Kinetic.Node.prototype.setHeight.call(this, height); this._resizeDOM(); return this; }, /** * set width * @method * @memberof Kinetic.Stage.prototype * @param {Number} width */ setWidth: function(width) { Kinetic.Node.prototype.setWidth.call(this, width); this._resizeDOM(); return this; }, /** * clear all layers * @method * @memberof Kinetic.Stage.prototype */ clear: function() { var layers = this.children, len = layers.length, n; for(n = 0; n < len; n++) { layers[n].clear(); } return this; }, /** * remove stage * @method * @memberof Kinetic.Stage.prototype */ destroy: function() { var content = this.content; Kinetic.Container.prototype.destroy.call(this); if(content && Kinetic.Util._isInDocument(content)) { this.getContainer().removeChild(content); } }, /** * get pointer position which can be a touch position or mouse position * @method * @memberof Kinetic.Stage.prototype */ getPointerPosition: function() { return this.pointerPos; }, getStage: function() { return this; }, /** * get stage content div element which has the * the class name "kineticjs-content" * @method * @memberof Kinetic.Stage.prototype */ getContent: function() { return this.content; }, /** * Creates a composite data URL and requires a callback because the composite is generated asynchronously. * @method * @memberof Kinetic.Stage.prototype * @param {Object} config * @param {Function} config.callback function executed when the composite has completed * @param {String} [config.mimeType] can be "image/png" or "image/jpeg". * "image/png" is the default * @param {Number} [config.x] x position of canvas section * @param {Number} [config.y] y position of canvas section * @param {Number} [config.width] width of canvas section * @param {Number} [config.height] height of canvas section * @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) { config = config || {}; var mimeType = config.mimeType || null, quality = config.quality || null, x = config.x || 0, y = config.y || 0, canvas = new Kinetic.SceneCanvas({ width: config.width || this.getWidth(), height: config.height || this.getHeight(), pixelRatio: 1 }), _context = canvas.getContext()._context, layers = this.children; if(x || y) { _context.translate(-1 * x, -1 * y); } function drawLayer(n) { var layer = layers[n], layerUrl = layer.toDataURL(), 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. * @method * @memberof Kinetic.Stage.prototype * @param {Object} config * @param {Function} config.callback function executed when the composite has completed * @param {String} [config.mimeType] can be "image/png" or "image/jpeg". * "image/png" is the default * @param {Number} [config.x] x position of canvas section * @param {Number} [config.y] y position of canvas section * @param {Number} [config.width] width of canvas section * @param {Number} [config.height] height of canvas section * @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) { var cb = config.callback; config.callback = function(dataUrl) { Kinetic.Util._getImage(dataUrl, function(img) { cb(img); }); }; this.toDataURL(config); }, /** * get visible intersection shape. This is the preferred * method for determining if a point intersects a shape or not * @method * @memberof Kinetic.Stage.prototype * @param {Object} pos * @param {Number} pos.x * @param {Number} pos.y * @returns {Kinetic.Shape} */ getIntersection: function(pos) { var layers = this.getChildren(), len = layers.length, end = len - 1, n, shape; for(n = end; n >= 0; n--) { shape = layers[n].getIntersection(pos); if (shape) { return shape; } } return null; }, _resizeDOM: function() { if(this.content) { var width = this.getWidth(), height = this.getHeight(), layers = this.getChildren(), len = layers.length, n, layer; // set content dimensions this.content.style.width = width + PX; this.content.style.height = height + PX; this.bufferCanvas.setSize(width, height); this.bufferHitCanvas.setSize(width, height); // set layer dimensions for(n = 0; n < len; n++) { layer = layers[n]; layer.getCanvas().setSize(width, height); layer.hitCanvas.setSize(width, height); layer.draw(); } } }, /** * add layer to stage * @method * @memberof Kinetic.Stage.prototype * @param {Kinetic.Layer} layer */ add: function(layer) { Kinetic.Container.prototype.add.call(this, layer); layer.canvas.setSize(this.attrs.width, this.attrs.height); layer.hitCanvas.setSize(this.attrs.width, this.attrs.height); // draw layer and append canvas to container layer.draw(); this.content.appendChild(layer.canvas._canvas); // chainable return this; }, getParent: function() { return null; }, getLayer: function() { return null; }, /** * returns a {@link Kinetic.Collection} of layers * @method * @memberof Kinetic.Stage.prototype */ getLayers: function() { return this.getChildren(); }, _bindContentEvents: function() { var that = this, n; for (n = 0; n < eventsLength; n++) { addEvent(this, EVENTS[n]); } }, _mouseover: function(evt) { this._fire(CONTENT_MOUSEOVER, evt); }, _mouseout: function(evt) { this._setPointerPosition(evt); var targetShape = this.targetShape; if(targetShape && !Kinetic.isDragging()) { targetShape._fireAndBubble(MOUSEOUT, evt); targetShape._fireAndBubble(MOUSELEAVE, evt); this.targetShape = null; } this.pointerPos = undefined; this._fire(CONTENT_MOUSEOUT, evt); }, _mousemove: function(evt) { this._setPointerPosition(evt); var dd = Kinetic.DD, shape = this.getIntersection(this.getPointerPosition()); if(shape && shape.isListening()) { if(!Kinetic.isDragging() && (!this.targetShape || this.targetShape._id !== shape._id)) { if(this.targetShape) { this.targetShape._fireAndBubble(MOUSEOUT, evt, shape); this.targetShape._fireAndBubble(MOUSELEAVE, evt, shape); } shape._fireAndBubble(MOUSEOVER, evt, this.targetShape); shape._fireAndBubble(MOUSEENTER, evt, this.targetShape); this.targetShape = shape; } else { shape._fireAndBubble(MOUSEMOVE, evt); } } /* * if no shape was detected, clear target shape and try * to run mouseout from previous target shape */ else { if(this.targetShape && !Kinetic.isDragging()) { this.targetShape._fireAndBubble(MOUSEOUT, evt); this.targetShape._fireAndBubble(MOUSELEAVE, evt); this.targetShape = null; } } // content event this._fire(CONTENT_MOUSEMOVE, evt); if(dd) { dd._drag(evt); } // always call preventDefault for desktop events because some browsers // try to drag and drop the canvas element if (evt.preventDefault) { evt.preventDefault(); } }, _mousedown: function(evt) { this._setPointerPosition(evt); var shape = this.getIntersection(this.getPointerPosition()); Kinetic.listenClickTap = true; if (shape && shape.isListening()) { this.clickStartShape = shape; shape._fireAndBubble(MOUSEDOWN, evt); } // content event this._fire(CONTENT_MOUSEDOWN, evt); // always call preventDefault for desktop events because some browsers // try to drag and drop the canvas element if (evt.preventDefault) { evt.preventDefault(); } }, _mouseup: function(evt) { this._setPointerPosition(evt); var that = this, shape = this.getIntersection(this.getPointerPosition()), clickStartShape = this.clickStartShape, fireDblClick = false; if(Kinetic.inDblClickWindow) { fireDblClick = true; Kinetic.inDblClickWindow = false; } else { Kinetic.inDblClickWindow = true; } setTimeout(function() { Kinetic.inDblClickWindow = false; }, Kinetic.dblClickWindow); if (shape && shape.isListening()) { shape._fireAndBubble(MOUSEUP, evt); // detect if click or double click occurred if(Kinetic.listenClickTap && clickStartShape && clickStartShape._id === shape._id) { shape._fireAndBubble(CLICK, evt); if(fireDblClick) { shape._fireAndBubble(DBL_CLICK, evt); } } } // content events this._fire(CONTENT_MOUSEUP, evt); if (Kinetic.listenClickTap) { this._fire(CONTENT_CLICK, evt); if(fireDblClick) { this._fire(CONTENT_DBL_CLICK, evt); } } Kinetic.listenClickTap = false; // always call preventDefault for desktop events because some browsers // try to drag and drop the canvas element if (evt.preventDefault) { evt.preventDefault(); } }, _touchstart: function(evt) { this._setPointerPosition(evt); var shape = this.getIntersection(this.getPointerPosition()); Kinetic.listenClickTap = true; if (shape && shape.isListening()) { this.tapStartShape = shape; shape._fireAndBubble(TOUCHSTART, evt); // only call preventDefault if the shape is listening for events if (shape.isListening() && evt.preventDefault) { evt.preventDefault(); } } // content event this._fire(CONTENT_TOUCHSTART, evt); }, _touchend: function(evt) { this._setPointerPosition(evt); var that = this, shape = this.getIntersection(this.getPointerPosition()); fireDblClick = false; if(Kinetic.inDblClickWindow) { fireDblClick = true; Kinetic.inDblClickWindow = false; } else { Kinetic.inDblClickWindow = true; } setTimeout(function() { Kinetic.inDblClickWindow = false; }, Kinetic.dblClickWindow); if (shape && shape.isListening()) { shape._fireAndBubble(TOUCHEND, evt); // detect if tap or double tap occurred if(Kinetic.listenClickTap && shape._id === this.tapStartShape._id) { shape._fireAndBubble(TAP, evt); if(fireDblClick) { shape._fireAndBubble(DBL_TAP, evt); } } // only call preventDefault if the shape is listening for events if (shape.isListening() && evt.preventDefault) { evt.preventDefault(); } } // content events if (Kinetic.listenClickTap) { this._fire(CONTENT_TOUCHEND, evt); if(fireDblClick) { this._fire(CONTENT_DBL_TAP, evt); } } Kinetic.listenClickTap = false; }, _touchmove: function(evt) { this._setPointerPosition(evt); var dd = Kinetic.DD, shape = this.getIntersection(this.getPointerPosition()); if (shape && shape.isListening()) { shape._fireAndBubble(TOUCHMOVE, evt); // only call preventDefault if the shape is listening for events if (shape.isListening() && evt.preventDefault) { evt.preventDefault(); } } this._fire(CONTENT_TOUCHMOVE, evt); // start drag and drop if(dd) { dd._drag(evt); } }, _setPointerPosition: function(evt) { var evt = evt ? evt : window.event, contentPosition = this._getContentPosition(), offsetX = evt.offsetX, clientX = evt.clientX, x = null, y = null, touch; // touch events if(evt.touches !== undefined) { // currently, only handle one finger if (evt.touches.length === 1) { touch = evt.touches[0]; // get the information for finger #1 x = touch.clientX - contentPosition.left; y = touch.clientY - contentPosition.top; } } // mouse events else { // if offsetX is defined, assume that offsetY is defined as well if (offsetX !== undefined) { x = offsetX; y = evt.offsetY; } // we unforunately have to use UA detection here because accessing // the layerX or layerY properties in newer veresions of Chrome // throws a JS warning. layerX and layerY are required for FF // when the container is transformed via CSS. else if (Kinetic.UA.browser === 'mozilla') { x = evt.layerX; y = evt.layerY; } // if clientX is defined, assume that clientY is defined as well else if (clientX !== undefined && contentPosition) { x = clientX - contentPosition.left; y = evt.clientY - contentPosition.top; } } if (x !== null && y !== null) { this.pointerPos = { x: x, y: y }; } }, _getContentPosition: function() { var rect = this.content.getBoundingClientRect ? this.content.getBoundingClientRect() : { top: 0, left: 0 }; return { top: rect.top, left: rect.left }; }, _buildDOM: function() { var container = this.getContainer(); // clear content inside container container.innerHTML = EMPTY_STRING; // content this.content = document.createElement(DIV); this.content.style.position = RELATIVE; this.content.style.display = INLINE_BLOCK; this.content.className = KINETICJS_CONTENT; this.content.setAttribute('role', 'presentation'); container.appendChild(this.content); // the buffer canvas pixel ratio must be 1 because it is used as an // intermediate canvas before copying the result onto a scene canvas. // not setting it to 1 will result in an over compensation this.bufferCanvas = new Kinetic.SceneCanvas({ pixelRatio: 1 }); this.bufferHitCanvas = new Kinetic.HitCanvas(); this._resizeDOM(); }, _onContent: function(typesStr, handler) { var types = typesStr.split(SPACE), len = types.length, n, baseEvent; for(n = 0; n < len; n++) { baseEvent = types[n]; this.content.addEventListener(baseEvent, handler, false); } } }); Kinetic.Util.extend(Kinetic.Stage, Kinetic.Container); // add getters and setters Kinetic.Factory.addGetter(Kinetic.Stage, 'container'); /** * get container DOM element * @name getContainer * @method * @memberof Kinetic.Stage.prototype */ })();