(function() { // CONSTANTS var ABSOLUTE_OPACITY = 'absoluteOpacity', ABSOLUTE_TRANSFORM = 'absoluteTransform', BEFORE = 'before', CHANGE = 'Change', CHILDREN = 'children', DOT = '.', EMPTY_STRING = '', GET = 'get', ID = 'id', KINETIC = 'kinetic', LISTENING = 'listening', MOUSEENTER = 'mouseenter', MOUSELEAVE = 'mouseleave', NAME = 'name', SET = 'set', SHAPE = 'Shape', SPACE = ' ', STAGE = 'stage', TRANSFORM = 'transform', UPPER_STAGE = 'Stage', VISIBLE = 'visible', CLONE_BLACK_LIST = ['id'], TRANSFORM_CHANGE_STR = [ 'xChange.kinetic', 'yChange.kinetic', 'scaleXChange.kinetic', 'scaleYChange.kinetic', 'skewXChange.kinetic', 'skewYChange.kinetic', 'rotationChange.kinetic', 'offsetXChange.kinetic', 'offsetYChange.kinetic', 'transformsEnabledChange.kinetic' ].join(SPACE); Kinetic.Util.addMethods(Kinetic.Node, { _init: function(config) { var that = this; this._id = Kinetic.idCounter++; this.eventListeners = {}; this.attrs = {}; this._cache = {}; this._filterUpToDate = false; this.setAttrs(config); // event bindings for cache handling this.on(TRANSFORM_CHANGE_STR, function() { this._clearCache(TRANSFORM); that._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); }); this.on('visibleChange.kinetic', function() { that._clearSelfAndDescendantCache(VISIBLE); }); this.on('listeningChange.kinetic', function() { that._clearSelfAndDescendantCache(LISTENING); }); this.on('opacityChange.kinetic', function() { that._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); }); }, _clearCache: function(attr){ if (attr) { delete this._cache[attr]; } else { this._cache = {}; } }, _getCache: function(attr, privateGetter){ var cache = this._cache[attr]; // if not cached, we need to set it using the private getter method. if (cache === undefined) { this._cache[attr] = privateGetter.call(this); } return this._cache[attr]; }, /* * when the logic for a cached result depends on ancestor propagation, use this * method to clear self and children cache */ _clearSelfAndDescendantCache: function(attr) { this._clearCache(attr); if (this.children) { this.getChildren().each(function(node) { node._clearSelfAndDescendantCache(attr); }); } }, /** * clear cached canvas * @method * @memberof Kinetic.Node.prototype * @returns {Kinetic.Node} * @example * node.clearCache(); */ clearCache: function() { delete this._cache.canvas; this._filterUpToDate = false; return this; }, /** * cache node to improve drawing performance, apply filters, or create more accurate * hit regions * @method * @memberof Kinetic.Node.prototype * @param {Object} config * @param {Number} [config.x] * @param {Number} [config.y] * @param {Number} [config.width] * @param {Number} [config.height] * @param {Boolean} [config.drawBorder] when set to true, a red border will be drawn around the cached * region for debugging purposes * @returns {Kinetic.Node} * @example * // cache a shape with the x,y position of the bounding box at the center and * // the width and height of the bounding box equal to the width and height of * // the shape obtained from shape.width() and shape.height() * image.cache(); * * // cache a node and define the bounding box position and size * node.cache({ * x: -30, * y: -30, * width: 100, * height: 200 * }); * * // cache a node and draw a red border around the bounding box * // for debugging purposes * node.cache({ * x: -30, * y: -30, * width: 100, * height: 200, * drawBorder: true * }); */ cache: function(config) { var conf = config || {}, x = conf.x || 0, y = conf.y || 0, width = conf.width || this.width(), height = conf.height || this.height(), drawBorder = conf.drawBorder || false, layer = this.getLayer(); if (width === 0 || height === 0) { Kinetic.Util.warn('Width or height of caching configuration equals 0. Cache is ignored.'); return; } var cachedSceneCanvas = new Kinetic.SceneCanvas({ pixelRatio: 1, width: width, height: height }), cachedFilterCanvas = new Kinetic.SceneCanvas({ pixelRatio: 1, width: width, height: height }), cachedHitCanvas = new Kinetic.HitCanvas({ width: width, height: height }), origTransEnabled = this.transformsEnabled(), origX = this.x(), origY = this.y(), sceneContext = cachedSceneCanvas.getContext(), hitContext = cachedHitCanvas.getContext(); this.clearCache(); sceneContext.save(); hitContext.save(); // this will draw a red border around the cached box for // debugging purposes if (drawBorder) { sceneContext.save(); sceneContext.beginPath(); sceneContext.rect(0, 0, width, height); sceneContext.closePath(); sceneContext.setAttr('strokeStyle', 'red'); sceneContext.setAttr('lineWidth', 5); sceneContext.stroke(); sceneContext.restore(); } sceneContext.translate(x * -1, y * -1); hitContext.translate(x * -1, y * -1); if (this.nodeType === 'Shape') { sceneContext.translate(this.x() * -1, this.y() * -1); hitContext.translate(this.x() * -1, this.y() * -1); } this.drawScene(cachedSceneCanvas, this); this.drawHit(cachedHitCanvas, this); sceneContext.restore(); hitContext.restore(); this._cache.canvas = { scene: cachedSceneCanvas, filter: cachedFilterCanvas, hit: cachedHitCanvas }; return this; }, _drawCachedSceneCanvas: function(context) { context.save(); this.getLayer()._applyTransform(this, context); context.drawImage(this._getCachedSceneCanvas()._canvas, 0, 0); context.restore(); }, _getCachedSceneCanvas: function() { var filters = this.filters(), cachedCanvas = this._cache.canvas, sceneCanvas = cachedCanvas.scene, filterCanvas = cachedCanvas.filter, filterContext = filterCanvas.getContext(), len, imageData, n, filter; if (filters) { if (!this._filterUpToDate) { try { len = filters.length; filterContext.clear(); // copy cached canvas onto filter context filterContext.drawImage(sceneCanvas._canvas, 0, 0); imageData = filterContext.getImageData(0, 0, filterCanvas.getWidth(), filterCanvas.getHeight()); // apply filters to filter context for (n=0; n