UNPKG

konva

Version:

<p align="center"> <img src="https://raw.githubusercontent.com/konvajs/konvajs.github.io/master/apple-touch-icon-180x180.png" alt="Konva logo" height="180" /> </p>

1,692 lines (1,603 loc) 71.2 kB
(function(Konva) { 'use strict'; // CONSTANTS var ABSOLUTE_OPACITY = 'absoluteOpacity', ABSOLUTE_TRANSFORM = 'absoluteTransform', ABSOLUTE_SCALE = 'absoluteScale', CHANGE = 'Change', CHILDREN = 'children', DOT = '.', EMPTY_STRING = '', GET = 'get', ID = 'id', KONVA = 'konva', 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.konva', 'yChange.konva', 'scaleXChange.konva', 'scaleYChange.konva', 'skewXChange.konva', 'skewYChange.konva', 'rotationChange.konva', 'offsetXChange.konva', 'offsetYChange.konva', 'transformsEnabledChange.konva' ].join(SPACE), SCALE_CHANGE_STR = ['scaleXChange.konva', 'scaleYChange.konva'].join(SPACE); /** * Node constructor. Nodes are entities that can be transformed, layered, * and have bound events. The stage, layers, groups, and shapes all extend Node. * @constructor * @memberof Konva * @abstract * @param {Object} config * @@nodeParams */ Konva.Node = function(config) { this._init(config); }; Konva.Util.addMethods(Konva.Node, { _init: function(config) { var that = this; this._id = Konva.idCounter++; this.eventListeners = {}; this.attrs = {}; this._cache = {}; this._filterUpToDate = false; this._isUnderCache = false; this.setAttrs(config); // event bindings for cache handling this.on(TRANSFORM_CHANGE_STR, function() { this._clearCache(TRANSFORM); that._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); }); this.on(SCALE_CHANGE_STR, function() { that._clearSelfAndDescendantCache(ABSOLUTE_SCALE); }); this.on('visibleChange.konva', function() { that._clearSelfAndDescendantCache(VISIBLE); }); this.on('listeningChange.konva', function() { that._clearSelfAndDescendantCache(LISTENING); }); this.on('opacityChange.konva', 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 Konva.Node.prototype * @returns {Konva.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. For all basic shapes size of cache canvas will be automatically detected. * If you need to cache your custom `Konva.Shape` instance you have to pass shape's bounding box * properties. Look at [https://konvajs.github.io/docs/performance/Shape_Caching.html](link to demo page) for more information. * @method * @memberof Konva.Node.prototype * @param {Object} [config] * @param {Number} [config.x] * @param {Number} [config.y] * @param {Number} [config.width] * @param {Number} [config.height] * @param {Number} [config.offset] increase canvas size by `offset` pixel in all directions. * @param {Boolean} [config.drawBorder] when set to true, a red border will be drawn around the cached * region for debugging purposes * @param {Number} [config.pixelRatio] change quality (or pixel ratio) of cached image. pixelRatio = 2 will produce 2x sized cache. * @returns {Konva.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, * offset : 10, * drawBorder: true * }); */ cache: function(config) { var conf = config || {}, rect = this.getClientRect({ skipTransform: true, relativeTo: this.getParent() }), width = conf.width || rect.width, height = conf.height || rect.height, pixelRatio = conf.pixelRatio, x = conf.x || rect.x, y = conf.y || rect.y, offset = conf.offset || 0, drawBorder = conf.drawBorder || false; if (!width || !height) { // make throw async, because we don't need to stop funcion setTimeout(function() { Konva.Util.throw( 'Width or height of caching configuration equals 0. Caching is ignored.' ); }); return; } width += offset * 2; height += offset * 2; x -= offset; y -= offset; var cachedSceneCanvas = new Konva.SceneCanvas({ pixelRatio: pixelRatio, width: width, height: height }), cachedFilterCanvas = new Konva.SceneCanvas({ pixelRatio: pixelRatio, width: width, height: height }), cachedHitCanvas = new Konva.HitCanvas({ pixelRatio: 1, width: width, height: height }), sceneContext = cachedSceneCanvas.getContext(), hitContext = cachedHitCanvas.getContext(); cachedHitCanvas.isCache = true; this.clearCache(); sceneContext.save(); hitContext.save(); sceneContext.translate(-x, -y); hitContext.translate(-x, -y); // extra flag to skip on getAbsolute opacity calc this._isUnderCache = true; this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); this._clearSelfAndDescendantCache(ABSOLUTE_SCALE); this.drawScene(cachedSceneCanvas, this, true); this.drawHit(cachedHitCanvas, this, true); this._isUnderCache = false; sceneContext.restore(); hitContext.restore(); // 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(); } this._cache.canvas = { scene: cachedSceneCanvas, filter: cachedFilterCanvas, hit: cachedHitCanvas, x: x, y: y }; return this; }, /** * Return client rectangle {x, y, width, height} of node. This rectangle also include all styling (strokes, shadows, etc). * The rectangle position is relative to parent container. * @method * @memberof Konva.Node.prototype * @param {Object} config * @param {Boolean} [config.skipTransform] should we apply transform to node for calculating rect? * @param {Object} [config.relativeTo] calculate client rect relative to one of the parents * @returns {Object} rect with {x, y, width, height} properties * @example * var rect = new Konva.Rect({ * width : 100, * height : 100, * x : 50, * y : 50, * strokeWidth : 4, * stroke : 'black', * offsetX : 50, * scaleY : 2 * }); * * // get client rect without think off transformations (position, rotation, scale, offset, etc) * rect.getClientRect({ skipTransform: true}); * // returns { * // x : -2, // two pixels for stroke / 2 * // y : -2, * // width : 104, // increased by 4 for stroke * // height : 104 * //} * * // get client rect with transformation applied * rect.getClientRect(); * // returns Object {x: -2, y: 46, width: 104, height: 208} */ getClientRect: function() { // abstract method // redefine in Container and Shape throw new Error('abstract "getClientRect" method call'); }, _transformedRect: function(rect, top) { var points = [ { x: rect.x, y: rect.y }, { x: rect.x + rect.width, y: rect.y }, { x: rect.x + rect.width, y: rect.y + rect.height }, { x: rect.x, y: rect.y + rect.height } ]; var minX, minY, maxX, maxY; var trans = this.getAbsoluteTransform(top); points.forEach(function(point) { var transformed = trans.point(point); if (minX === undefined) { minX = maxX = transformed.x; minY = maxY = transformed.y; } minX = Math.min(minX, transformed.x); minY = Math.min(minY, transformed.y); maxX = Math.max(maxX, transformed.x); maxY = Math.max(maxY, transformed.y); }); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; }, _drawCachedSceneCanvas: function(context) { context.save(); context._applyOpacity(this); context._applyGlobalCompositeOperation(this); context.translate(this._cache.canvas.x, this._cache.canvas.y); var cacheCanvas = this._getCachedSceneCanvas(); var ratio = cacheCanvas.pixelRatio; context.drawImage( cacheCanvas._canvas, 0, 0, cacheCanvas.width / ratio, cacheCanvas.height / ratio ); context.restore(); }, _drawCachedHitCanvas: function(context) { var cachedCanvas = this._cache.canvas, hitCanvas = cachedCanvas.hit; context.save(); context.translate(this._cache.canvas.x, this._cache.canvas.y); context.drawImage(hitCanvas._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) { var ratio = sceneCanvas.pixelRatio; try { len = filters.length; filterContext.clear(); // copy cached canvas onto filter context filterContext.drawImage( sceneCanvas._canvas, 0, 0, sceneCanvas.getWidth() / ratio, sceneCanvas.getHeight() / ratio ); imageData = filterContext.getImageData( 0, 0, filterCanvas.getWidth(), filterCanvas.getHeight() ); // apply filters to filter context for (n = 0; n < len; n++) { filter = filters[n]; if (typeof filter !== 'function') { Konva.Util.error( 'Filter should be type of function, but got ' + typeof filter + ' insted. Please check correct filters' ); continue; } filter.call(this, imageData); filterContext.putImageData(imageData, 0, 0); } } catch (e) { Konva.Util.error('Unable to apply filter. ' + e.message); } this._filterUpToDate = true; } return filterCanvas; } return sceneCanvas; }, /** * bind events to the node. KonvaJS supports mouseover, mousemove, * mouseout, mouseenter, mouseleave, mousedown, mouseup, wheel, click, dblclick, touchstart, touchmove, * touchend, tap, dbltap, dragstart, dragmove, and dragend events. The Konva Stage supports * contentMouseover, contentMousemove, contentMouseout, contentMousedown, contentMouseup, contentWheel, contentContextmenu * contentClick, contentDblclick, contentTouchstart, contentTouchmove, contentTouchend, contentTap, * and contentDblTap. Pass in a string of events delimmited by a space to bind multiple events at once * such as 'mousedown mouseup mousemove'. Include a namespace to bind an * event by name such as 'click.foobar'. * @method * @memberof Konva.Node.prototype * @param {String} evtStr e.g. 'click', 'mousedown touchstart', 'mousedown.foo touchstart.foo' * @param {Function} handler The handler function is passed an event object * @returns {Konva.Node} * @example * // add click listener * node.on('click', function() { * console.log('you clicked me!'); * }); * * // get the target node * node.on('click', function(evt) { * console.log(evt.target); * }); * * // stop event propagation * node.on('click', function(evt) { * evt.cancelBubble = true; * }); * * // bind multiple listeners * node.on('click touchstart', function() { * console.log('you clicked/touched me!'); * }); * * // namespace listener * node.on('click.foo', function() { * console.log('you clicked/touched me!'); * }); * * // get the event type * node.on('click tap', function(evt) { * var eventType = evt.type; * }); * * // get native event object * node.on('click tap', function(evt) { * var nativeEvent = evt.evt; * }); * * // for change events, get the old and new val * node.on('xChange', function(evt) { * var oldVal = evt.oldVal; * var newVal = evt.newVal; * }); * * // get event targets * // with event delegations * layer.on('click', 'Group', function(evt) { * var shape = evt.target; * var group = evtn.currentTarger; * }); */ on: function(evtStr, handler) { if (arguments.length === 3) { return this._delegate.apply(this, arguments); } var events = evtStr.split(SPACE), len = events.length, n, event, parts, baseEvent, name; /* * loop through types and attach event listeners to * each one. eg. 'click mouseover.namespace mouseout' * will create three event bindings */ for (n = 0; n < len; n++) { event = events[n]; parts = event.split(DOT); baseEvent = parts[0]; name = parts[1] || EMPTY_STRING; // create events array if it doesn't exist if (!this.eventListeners[baseEvent]) { this.eventListeners[baseEvent] = []; } this.eventListeners[baseEvent].push({ name: name, handler: handler }); } return this; }, /** * remove event bindings from the node. Pass in a string of * event types delimmited by a space to remove multiple event * bindings at once such as 'mousedown mouseup mousemove'. * include a namespace to remove an event binding by name * such as 'click.foobar'. If you only give a name like '.foobar', * all events in that namespace will be removed. * @method * @memberof Konva.Node.prototype * @param {String} evtStr e.g. 'click', 'mousedown touchstart', '.foobar' * @returns {Konva.Node} * @example * // remove listener * node.off('click'); * * // remove multiple listeners * node.off('click touchstart'); * * // remove listener by name * node.off('click.foo'); */ off: function(evtStr, callback) { var events = (evtStr || '').split(SPACE), len = events.length, n, t, event, parts, baseEvent, name; if (!evtStr) { // remove all events for (t in this.eventListeners) { this._off(t); } } for (n = 0; n < len; n++) { event = events[n]; parts = event.split(DOT); baseEvent = parts[0]; name = parts[1]; if (baseEvent) { if (this.eventListeners[baseEvent]) { this._off(baseEvent, name, callback); } } else { for (t in this.eventListeners) { this._off(t, name, callback); } } } return this; }, // some event aliases for third party integration like HammerJS dispatchEvent: function(evt) { var e = { target: this, type: evt.type, evt: evt }; this.fire(evt.type, e); return this; }, addEventListener: function(type, handler) { // we have to pass native event to handler this.on(type, function(evt) { handler.call(this, evt.evt); }); return this; }, removeEventListener: function(type) { this.off(type); return this; }, // like node.on _delegate: function(event, selector, handler) { var stopNode = this; this.on(event, function(evt) { var targets = evt.target.findAncestors(selector, true, stopNode); for (var i = 0; i < targets.length; i++) { evt = Konva.Util.cloneObject(evt); evt.currentTarget = targets[i]; handler.call(targets[i], evt); } }); }, /** * remove self from parent, but don't destroy * @method * @memberof Konva.Node.prototype * @returns {Konva.Node} * @example * node.remove(); */ remove: function() { var parent = this.getParent(); if (parent && parent.children) { parent.children.splice(this.index, 1); parent._setChildrenIndices(); delete this.parent; } // every cached attr that is calculated via node tree // traversal must be cleared when removing a node this._clearSelfAndDescendantCache(STAGE); this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); this._clearSelfAndDescendantCache(VISIBLE); this._clearSelfAndDescendantCache(LISTENING); this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); return this; }, /** * remove and destroy self * @method * @memberof Konva.Node.prototype * @example * node.destroy(); */ destroy: function() { // remove from ids and names hashes Konva._removeId(this.getId()); // remove all names var names = (this.getName() || '').split(/\s/g); for (var i = 0; i < names.length; i++) { var subname = names[i]; Konva._removeName(subname, this._id); } this.remove(); return this; }, /** * get attr * @method * @memberof Konva.Node.prototype * @param {String} attr * @returns {Integer|String|Object|Array} * @example * var x = node.getAttr('x'); */ getAttr: function(attr) { var method = GET + Konva.Util._capitalize(attr); if (Konva.Util._isFunction(this[method])) { return this[method](); } // otherwise get directly return this.attrs[attr]; }, /** * get ancestors * @method * @memberof Konva.Node.prototype * @returns {Konva.Collection} * @example * shape.getAncestors().each(function(node) { * console.log(node.getId()); * }) */ getAncestors: function() { var parent = this.getParent(), ancestors = new Konva.Collection(); while (parent) { ancestors.push(parent); parent = parent.getParent(); } return ancestors; }, /** * get attrs object literal * @method * @memberof Konva.Node.prototype * @returns {Object} */ getAttrs: function() { return this.attrs || {}; }, /** * set multiple attrs at once using an object literal * @method * @memberof Konva.Node.prototype * @param {Object} config object containing key value pairs * @returns {Konva.Node} * @example * node.setAttrs({ * x: 5, * fill: 'red' * }); */ setAttrs: function(config) { var key, method; if (!config) { return this; } for (key in config) { if (key === CHILDREN) { continue; } method = SET + Konva.Util._capitalize(key); // use setter if available if (Konva.Util._isFunction(this[method])) { this[method](config[key]); } else { // otherwise set directly this._setAttr(key, config[key]); } } return this; }, /** * determine if node is listening for events by taking into account ancestors. * * Parent | Self | isListening * listening | listening | * ----------+-----------+------------ * T | T | T * T | F | F * F | T | T * F | F | F * ----------+-----------+------------ * T | I | T * F | I | F * I | I | T * * @method * @memberof Konva.Node.prototype * @returns {Boolean} */ isListening: function() { return this._getCache(LISTENING, this._isListening); }, _isListening: function() { var listening = this.getListening(), parent = this.getParent(); // the following conditions are a simplification of the truth table above. // please modify carefully if (listening === 'inherit') { if (parent) { return parent.isListening(); } else { return true; } } else { return listening; } }, /** * determine if node is visible by taking into account ancestors. * * Parent | Self | isVisible * visible | visible | * ----------+-----------+------------ * T | T | T * T | F | F * F | T | T * F | F | F * ----------+-----------+------------ * T | I | T * F | I | F * I | I | T * @method * @memberof Konva.Node.prototype * @returns {Boolean} */ isVisible: function() { return this._getCache(VISIBLE, this._isVisible); }, _isVisible: function() { var visible = this.getVisible(), parent = this.getParent(); // the following conditions are a simplification of the truth table above. // please modify carefully if (visible === 'inherit') { if (parent) { return parent.isVisible(); } else { return true; } } else { return visible; } }, /** * determine if listening is enabled by taking into account descendants. If self or any children * have _isListeningEnabled set to true, then self also has listening enabled. * @method * @memberof Konva.Node.prototype * @returns {Boolean} */ shouldDrawHit: function(canvas) { var layer = this.getLayer(); return ( (canvas && canvas.isCache) || (layer && layer.hitGraphEnabled() && this.isListening() && this.isVisible()) ); }, /** * show node * @method * @memberof Konva.Node.prototype * @returns {Konva.Node} */ show: function() { this.setVisible(true); return this; }, /** * hide node. Hidden nodes are no longer detectable * @method * @memberof Konva.Node.prototype * @returns {Konva.Node} */ hide: function() { this.setVisible(false); return this; }, /** * get zIndex relative to the node's siblings who share the same parent * @method * @memberof Konva.Node.prototype * @returns {Integer} */ getZIndex: function() { return this.index || 0; }, /** * get absolute z-index which takes into account sibling * and ancestor indices * @method * @memberof Konva.Node.prototype * @returns {Integer} */ getAbsoluteZIndex: function() { var depth = this.getDepth(), that = this, index = 0, nodes, len, n, child; function addChildren(children) { nodes = []; len = children.length; for (n = 0; n < len; n++) { child = children[n]; index++; if (child.nodeType !== SHAPE) { nodes = nodes.concat(child.getChildren().toArray()); } if (child._id === that._id) { n = len; } } if (nodes.length > 0 && nodes[0].getDepth() <= depth) { addChildren(nodes); } } if (that.nodeType !== UPPER_STAGE) { addChildren(that.getStage().getChildren()); } return index; }, /** * get node depth in node tree. Returns an integer. * e.g. Stage depth will always be 0. Layers will always be 1. Groups and Shapes will always * be >= 2 * @method * @memberof Konva.Node.prototype * @returns {Integer} */ getDepth: function() { var depth = 0, parent = this.parent; while (parent) { depth++; parent = parent.parent; } return depth; }, setPosition: function(pos) { this.setX(pos.x); this.setY(pos.y); return this; }, getPosition: function() { return { x: this.getX(), y: this.getY() }; }, /** * get absolute position relative to the top left corner of the stage container div * or relative to passed node * @method * @param {Object} [top] optional parent node * @memberof Konva.Node.prototype * @returns {Object} */ getAbsolutePosition: function(top) { var absoluteMatrix = this.getAbsoluteTransform(top).getMatrix(), absoluteTransform = new Konva.Transform(), offset = this.offset(); // clone the matrix array absoluteTransform.m = absoluteMatrix.slice(); absoluteTransform.translate(offset.x, offset.y); return absoluteTransform.getTranslation(); }, /** * set absolute position * @method * @memberof Konva.Node.prototype * @param {Object} pos * @param {Number} pos.x * @param {Number} pos.y * @returns {Konva.Node} */ setAbsolutePosition: function(pos) { var origTrans = this._clearTransform(), it; // don't clear translation this.attrs.x = origTrans.x; this.attrs.y = origTrans.y; delete origTrans.x; delete origTrans.y; // unravel transform it = this.getAbsoluteTransform(); it.invert(); it.translate(pos.x, pos.y); pos = { x: this.attrs.x + it.getTranslation().x, y: this.attrs.y + it.getTranslation().y }; this.setPosition({ x: pos.x, y: pos.y }); this._setTransform(origTrans); return this; }, _setTransform: function(trans) { var key; for (key in trans) { this.attrs[key] = trans[key]; } this._clearCache(TRANSFORM); this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); }, _clearTransform: function() { var trans = { x: this.getX(), y: this.getY(), rotation: this.getRotation(), scaleX: this.getScaleX(), scaleY: this.getScaleY(), offsetX: this.getOffsetX(), offsetY: this.getOffsetY(), skewX: this.getSkewX(), skewY: this.getSkewY() }; this.attrs.x = 0; this.attrs.y = 0; this.attrs.rotation = 0; this.attrs.scaleX = 1; this.attrs.scaleY = 1; this.attrs.offsetX = 0; this.attrs.offsetY = 0; this.attrs.skewX = 0; this.attrs.skewY = 0; this._clearCache(TRANSFORM); this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); // return original transform return trans; }, /** * move node by an amount relative to its current position * @method * @memberof Konva.Node.prototype * @param {Object} change * @param {Number} change.x * @param {Number} change.y * @returns {Konva.Node} * @example * // move node in x direction by 1px and y direction by 2px * node.move({ * x: 1, * y: 2) * }); */ move: function(change) { var changeX = change.x, changeY = change.y, x = this.getX(), y = this.getY(); if (changeX !== undefined) { x += changeX; } if (changeY !== undefined) { y += changeY; } this.setPosition({ x: x, y: y }); return this; }, _eachAncestorReverse: function(func, top) { var family = [], parent = this.getParent(), len, n; // if top node is defined, and this node is top node, // there's no need to build a family tree. just execute // func with this because it will be the only node if (top && top._id === this._id) { func(this); return true; } family.unshift(this); while (parent && (!top || parent._id !== top._id)) { family.unshift(parent); parent = parent.parent; } len = family.length; for (n = 0; n < len; n++) { func(family[n]); } }, /** * rotate node by an amount in degrees relative to its current rotation * @method * @memberof Konva.Node.prototype * @param {Number} theta * @returns {Konva.Node} */ rotate: function(theta) { this.setRotation(this.getRotation() + theta); return this; }, /** * move node to the top of its siblings * @method * @memberof Konva.Node.prototype * @returns {Boolean} */ moveToTop: function() { if (!this.parent) { Konva.Util.warn('Node has no parent. moveToTop function is ignored.'); return false; } var index = this.index; this.parent.children.splice(index, 1); this.parent.children.push(this); this.parent._setChildrenIndices(); return true; }, /** * move node up * @method * @memberof Konva.Node.prototype * @returns {Boolean} flag is moved or not */ moveUp: function() { if (!this.parent) { Konva.Util.warn('Node has no parent. moveUp function is ignored.'); return false; } var index = this.index, len = this.parent.getChildren().length; if (index < len - 1) { this.parent.children.splice(index, 1); this.parent.children.splice(index + 1, 0, this); this.parent._setChildrenIndices(); return true; } return false; }, /** * move node down * @method * @memberof Konva.Node.prototype * @returns {Boolean} */ moveDown: function() { if (!this.parent) { Konva.Util.warn('Node has no parent. moveDown function is ignored.'); return false; } var index = this.index; if (index > 0) { this.parent.children.splice(index, 1); this.parent.children.splice(index - 1, 0, this); this.parent._setChildrenIndices(); return true; } return false; }, /** * move node to the bottom of its siblings * @method * @memberof Konva.Node.prototype * @returns {Boolean} */ moveToBottom: function() { if (!this.parent) { Konva.Util.warn( 'Node has no parent. moveToBottom function is ignored.' ); return false; } var index = this.index; if (index > 0) { this.parent.children.splice(index, 1); this.parent.children.unshift(this); this.parent._setChildrenIndices(); return true; } return false; }, /** * set zIndex relative to siblings * @method * @memberof Konva.Node.prototype * @param {Integer} zIndex * @returns {Konva.Node} */ setZIndex: function(zIndex) { if (!this.parent) { Konva.Util.warn('Node has no parent. zIndex parameter is ignored.'); return false; } var index = this.index; this.parent.children.splice(index, 1); this.parent.children.splice(zIndex, 0, this); this.parent._setChildrenIndices(); return this; }, /** * get absolute opacity * @method * @memberof Konva.Node.prototype * @returns {Number} */ getAbsoluteOpacity: function() { return this._getCache(ABSOLUTE_OPACITY, this._getAbsoluteOpacity); }, _getAbsoluteOpacity: function() { var absOpacity = this.getOpacity(); var parent = this.getParent(); if (parent && !parent._isUnderCache) { absOpacity *= this.getParent().getAbsoluteOpacity(); } return absOpacity; }, /** * move node to another container * @method * @memberof Konva.Node.prototype * @param {Container} newContainer * @returns {Konva.Node} * @example * // move node from current layer into layer2 * node.moveTo(layer2); */ moveTo: function(newContainer) { // do nothing if new container is already parent if (this.getParent() !== newContainer) { // this.remove my be overrided by drag and drop // buy we need original (this.__originalRemove || this.remove).call(this); newContainer.add(this); } return this; }, /** * convert Node into an object for serialization. Returns an object. * @method * @memberof Konva.Node.prototype * @returns {Object} */ toObject: function() { var obj = {}, attrs = this.getAttrs(), key, val, getter, defaultValue; obj.attrs = {}; for (key in attrs) { val = attrs[key]; getter = this[key]; // remove attr value so that we can extract the default value from the getter delete attrs[key]; defaultValue = getter ? getter.call(this) : null; // restore attr value attrs[key] = val; if (defaultValue !== val) { obj.attrs[key] = val; } } obj.className = this.getClassName(); return Konva.Util._prepareToStringify(obj); }, /** * convert Node into a JSON string. Returns a JSON string. * @method * @memberof Konva.Node.prototype * @returns {String}} */ toJSON: function() { return JSON.stringify(this.toObject()); }, /** * get parent container * @method * @memberof Konva.Node.prototype * @returns {Konva.Node} */ getParent: function() { return this.parent; }, /** * get all ancestros (parent then parent of the parent, etc) of the node * @method * @memberof Konva.Node.prototype * @param {String} [selector] selector for search * @param {Boolean} [includeSelf] show we think that node is ancestro itself? * @param {Konva.Node} [stopNode] optional node where we need to stop searching (one of ancestors) * @returns {Array} [ancestors] * @example * // get one of the parent group * var parentGroups = node.findAncestors('Group'); */ findAncestors: function(selector, includeSelf, stopNode) { var res = []; if (includeSelf && this._isMatch(selector)) { res.push(this); } var ancestor = this.parent; while (ancestor) { if (ancestor === stopNode) { return res; } if (ancestor._isMatch(selector)) { res.push(ancestor); } ancestor = ancestor.parent; } return res; }, /** * get ancestor (parent or parent of the parent, etc) of the node that match passed selector * @method * @memberof Konva.Node.prototype * @param {String} [selector] selector for search * @param {Boolean} [includeSelf] show we think that node is ancestro itself? * @param {Konva.Node} [stopNode] optional node where we need to stop searching (one of ancestors) * @returns {Konva.Node} ancestor * @example * // get one of the parent group * var group = node.findAncestors('.mygroup'); */ findAncestor: function(selector, includeSelf, stopNode) { return this.findAncestors(selector, includeSelf, stopNode)[0]; }, // is current node match passed selector? _isMatch: function(selector) { if (!selector) { return false; } var selectorArr = selector.replace(/ /g, '').split(','), len = selectorArr.length, n, sel; for (n = 0; n < len; n++) { sel = selectorArr[n]; if (!Konva.Util.isValidSelector(sel)) { Konva.Util.warn( 'Selector "' + sel + '" is invalid. Allowed selectors examples are "#foo", ".bar" or "Group".' ); Konva.Util.warn( 'If you have a custom shape with such className, please change it to start with upper letter like "Triangle".' ); Konva.Util.warn('Konva is awesome, right?'); } // id selector if (sel.charAt(0) === '#') { if (this.id() === sel.slice(1)) { return true; } } else if (sel.charAt(0) === '.') { // name selector if (this.hasName(sel.slice(1))) { return true; } } else if (this._get(sel).length !== 0) { return true; } } return false; }, /** * get layer ancestor * @method * @memberof Konva.Node.prototype * @returns {Konva.Layer} */ getLayer: function() { var parent = this.getParent(); return parent ? parent.getLayer() : null; }, /** * get stage ancestor * @method * @memberof Konva.Node.prototype * @returns {Konva.Stage} */ getStage: function() { return this._getCache(STAGE, this._getStage); }, _getStage: function() { var parent = this.getParent(); if (parent) { return parent.getStage(); } else { return undefined; } }, /** * fire event * @method * @memberof Konva.Node.prototype * @param {String} eventType event type. can be a regular event, like click, mouseover, or mouseout, or it can be a custom event, like myCustomEvent * @param {Event} [evt] event object * @param {Boolean} [bubble] setting the value to false, or leaving it undefined, will result in the event * not bubbling. Setting the value to true will result in the event bubbling. * @returns {Konva.Node} * @example * // manually fire click event * node.fire('click'); * * // fire custom event * node.fire('foo'); * * // fire custom event with custom event object * node.fire('foo', { * bar: 10 * }); * * // fire click event that bubbles * node.fire('click', null, true); */ fire: function(eventType, evt, bubble) { evt = evt || {}; evt.target = evt.target || this; // bubble if (bubble) { this._fireAndBubble(eventType, evt); } else { // no bubble this._fire(eventType, evt); } return this; }, /** * get absolute transform of the node which takes into * account its ancestor transforms * @method * @memberof Konva.Node.prototype * @returns {Konva.Transform} */ getAbsoluteTransform: function(top) { // if using an argument, we can't cache the result. if (top) { return this._getAbsoluteTransform(top); } else { // if no argument, we can cache the result return this._getCache(ABSOLUTE_TRANSFORM, this._getAbsoluteTransform); } }, _getAbsoluteTransform: function(top) { var at = new Konva.Transform(), transformsEnabled, trans; // start with stage and traverse downwards to self this._eachAncestorReverse(function(node) { transformsEnabled = node.transformsEnabled(); trans = node.getTransform(); if (transformsEnabled === 'all') { at.multiply(trans); } else if (transformsEnabled === 'position') { at.translate(node.x(), node.y()); } }, top); return at; }, /** * get absolute scale of the node which takes into * account its ancestor scales * @method * @memberof Konva.Node.prototype * @returns {Konva.Transform} */ getAbsoluteScale: function(top) { // if using an argument, we can't cache the result. if (top) { return this._getAbsoluteScale(top); } else { // if no argument, we can cache the result return this._getCache(ABSOLUTE_SCALE, this._getAbsoluteScale); } }, _getAbsoluteScale: function(top) { // this is special logic for caching with some shapes with shadow var parent = this; while (parent) { if (parent._isUnderCache) { top = parent; } parent = parent.getParent(); } var scaleX = 1, scaleY = 1; // start with stage and traverse downwards to self this._eachAncestorReverse(function(node) { scaleX *= node.scaleX(); scaleY *= node.scaleY(); }, top); return { x: scaleX, y: scaleY }; }, /** * get transform of the node * @method * @memberof Konva.Node.prototype * @returns {Konva.Transform} */ getTransform: function() { return this._getCache(TRANSFORM, this._getTransform); }, _getTransform: function() { var m = new Konva.Transform(), x = this.getX(), y = this.getY(), rotation = Konva.getAngle(this.getRotation()), scaleX = this.getScaleX(), scaleY = this.getScaleY(), skewX = this.getSkewX(), skewY = this.getSkewY(), offsetX = this.getOffsetX(), offsetY = this.getOffsetY(); if (x !== 0 || y !== 0) { m.translate(x, y); } if (rotation !== 0) { m.rotate(rotation); } if (skewX !== 0 || skewY !== 0) { m.skew(skewX, skewY); } if (scaleX !== 1 || scaleY !== 1) { m.scale(scaleX, scaleY); } if (offsetX !== 0 || offsetY !== 0) { m.translate(-1 * offsetX, -1 * offsetY); } return m; }, /** * clone node. Returns a new Node instance with identical attributes. You can also override * the node properties with an object literal, enabling you to use an existing node as a template * for another node * @method * @memberof Konva.Node.prototype * @param {Object} obj override attrs * @returns {Konva.Node} * @example * // simple clone * var clone = node.clone(); * * // clone a node and override the x position * var clone = rect.clone({ * x: 5 * }); */ clone: function(obj) { // instantiate new node var attrs = Konva.Util.cloneObject(this.attrs), key, allListeners, len, n, listener; // filter black attrs for (var i in CLONE_BLACK_LIST) { var blockAttr = CLONE_BLACK_LIST[i]; delete attrs[blockAttr]; } // apply attr overrides for (key in obj) { attrs[key] = obj[key]; } var node = new this.constructor(attrs); // copy over listeners for (key in this.eventListeners) { allListeners = this.eventListeners[key]; len = allListeners.length; for (n = 0; n < len; n++) { listener = allListeners[n]; /* * don't include konva namespaced listeners because * these are generated by the constructors */ if (listener.name.indexOf(KONVA) < 0) { // if listeners array doesn't exist, then create it if (!node.eventListeners[key]) { node.eventListeners[key] = []; } node.eventListeners[key].push(listener); } } } return node; }, _toKonvaCanvas: function(config) { config = config || {}; var stage = this.getStage(), x = config.x || 0, y = config.y || 0, pixelRatio = config.pixelRatio || 1, canvas = new Konva.SceneCanvas({ width: config.width || this.getWidth() || (stage ? stage.getWidth() : 0), height: config.height || this.getHeight() || (stage ? stage.getHeight() : 0), pixelRatio: pixelRatio }), context = canvas.getContext(); context.save(); if (x || y) { context.translate(-1 * x, -1 * y); } this.drawScene(canvas); context.restore(); return canvas; }, /** * converts node into an canvas element. * @method * @memberof Konva.Node.prototype * @param {Object} config * @param {Function} config.callback function executed when the composite has completed * @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 * @paremt {Number} [config.pixelRatio] pixelRatio of ouput image. Default is 1. * @example * var canvas = node.toCanvas(); */ toCanvas: function(config) { return this._toKonvaCanvas(config)._canvas; }, /** * Creates a composite data URL. 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) * @method * @memberof Konva.Node.prototype * @param {Object} config * @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 * @paremt {Number} [config.pixelRatio] pixelRatio of ouput image url. Default is 1 * @returns {String} */ toDataURL: function(config) { config = config || {}; var mimeType = config.mimeType || null, quality = config.quality || null; return this._toKonvaCanvas(config).toDataURL(mimeType, quality); }, /** * converts node into an image. Since the toImage * method is asynchronous, a callback is required. toImage is most commonly used * to cache complex drawings as an image so that they don't have to constantly be redrawn * @method * @memberof Konva.Node.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 * @paremt {Number} [config.pixelRatio] pixelRatio of ouput image. Default is 1. * @example * var image = node.toImage({ * callback: function(img) { * // do stuff with img * } * }); */ toImage: function(config) { if (!config || !config.callback) { throw 'callback required for toImage method config argument'; } Konva.Util._getImage(this.toDataURL(config), function(img) { config.callback(img); }); }, setSize: function(size) { this.setWidth(size.width); this.setHeight(size.height); return this; }, getSi