konva/src/Node.js

2036 lines
64 KiB
JavaScript

(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<br>
* // the width and height of the bounding box equal to the width and height of<br>
* // the shape obtained from shape.width() and shape.height()<br>
* image.cache();<br><br>
*
* // cache a node and define the bounding box position and size<br>
* node.cache({<br>
* x: -30,<br>
* y: -30,<br>
* width: 100,<br>
* height: 200<br>
* });<br><br>
*
* // cache a node and draw a red border around the bounding box<br>
* // for debugging purposes<br>
* node.cache({<br>
* x: -30,<br>
* y: -30,<br>
* width: 100,<br>
* height: 200,<br>
* drawBorder: true<br>
* });
*/
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;
this.clearCache();
var layerApplyTrans = layer._applyTransform;
layer._applyTransform = function(shape, context) {
context.translate(x * -1, y * -1);
};
this.drawScene(cachedSceneCanvas);
this.drawHit(cachedHitCanvas);
// this will draw a red border around the cached box for
// debugging purposes
if (drawBorder) {
sceneContext = cachedSceneCanvas.getContext();
sceneContext.save();
sceneContext.beginPath();
sceneContext.rect(0, 0, width, height);
sceneContext.closePath();
sceneContext.setAttr('strokeStyle', 'red');
sceneContext.setAttr('lineWidth', 5);
sceneContext.stroke();
sceneContext.restore();
}
// set the _applyTransform method back to the original
layer._applyTransform = layerApplyTrans;
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<len; n++) {
filter = filters[n];
filter.call(this, imageData);
filterContext.putImageData(imageData, 0, 0);
}
}
catch(e) {
Kinetic.Util.warn('Unable to apply filter. ' + e.message);
}
this._filterUpToDate = true;
}
return filterCanvas;
}
else {
return sceneCanvas;
}
},
_drawCachedHitCanvas: function(context) {
var cachedCanvas = this._cache.canvas,
hitCanvas = cachedCanvas.hit;
context.save();
this.getLayer()._applyTransform(this, context);
context.drawImage(hitCanvas._canvas, 0, 0);
context.restore();
},
/**
* bind events to the node. KineticJS supports mouseover, mousemove,
* mouseout, mouseenter, mouseleave, mousedown, mouseup, click, dblclick, touchstart, touchmove,
* touchend, tap, dbltap, dragstart, dragmove, and dragend events. The Kinetic Stage supports
* contentMouseover, contentMousemove, contentMouseout, contentMousedown, contentMouseup,
* 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 Kinetic.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 {Kinetic.Node}
* @example
* // add click listener<br>
* node.on('click', function() {<br>
* console.log('you clicked me!');<br>
* });<br><br>
*
* // get the target node<br>
* node.on('click', function(evt) {<br>
* console.log(evt.target);<br>
* });<br><br>
*
* // stop event propagation<br>
* node.on('click', function(evt) {<br>
* evt.cancelBubble = true;<br>
* });<br><br>
*
* // bind multiple listeners<br>
* node.on('click touchstart', function() {<br>
* console.log('you clicked/touched me!');<br>
* });<br><br>
*
* // namespace listener<br>
* node.on('click.foo', function() {<br>
* console.log('you clicked/touched me!');<br>
* });<br><br>
*
* // get the event type<br>
* node.on('click tap', function(evt) {<br>
* var eventType = evt.type;<br>
* });<br><br>
*
* // for change events, get the old and new val<br>
* node.on('xChange', function(evt) {<br>
* var oldVal = evt.oldVal;<br>
* var newVal = evt.newVal;<br>
* });
*/
on: function(evtStr, handler) {
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
});
// NOTE: this flag is set to true when any event handler is added, even non
// mouse or touch gesture events. This improves performance for most
// cases where users aren't using events, but is still very light weight.
// To ensure perfect accuracy, devs can explicitly set listening to false.
/*
if (name !== KINETIC) {
this._listeningEnabled = true;
this._clearSelfAndAncestorCache(LISTENING_ENABLED);
}
*/
}
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 Kinetic.Node.prototype
* @param {String} evtStr e.g. 'click', 'mousedown touchstart', '.foobar'
* @returns {Kinetic.Node}
* @example
* // remove listener<br>
* node.off('click');<br><br>
*
* // remove multiple listeners<br>
* node.off('click touchstart');<br><br>
*
* // remove listener by name<br>
* node.off('click.foo');
*/
off: function(evtStr) {
var events = evtStr.split(SPACE),
len = events.length,
n, t, event, parts, baseEvent, name;
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);
}
}
else {
for(t in this.eventListeners) {
this._off(t, name);
}
}
}
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);
},
addEventListener: function(type, handler) {
// we to pass native event to handler
this.on(type, function(evt){
handler.call(this, evt.evt);
});
},
/**
* remove self from parent, but don't destroy
* @method
* @memberof Kinetic.Node.prototype
* @returns {Kinetic.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 Kinetic.Node.prototype
* @example
* node.destroy();
*/
destroy: function() {
// remove from ids and names hashes
Kinetic._removeId(this.getId());
Kinetic._removeName(this.getName(), this._id);
this.remove();
},
/**
* get attr
* @method
* @memberof Kinetic.Node.prototype
* @param {String} attr
* @returns {Integer|String|Object|Array}
* @example
* var x = node.getAttr('x');
*/
getAttr: function(attr) {
var method = GET + Kinetic.Util._capitalize(attr);
if(Kinetic.Util._isFunction(this[method])) {
return this[method]();
}
// otherwise get directly
else {
return this.attrs[attr];
}
},
/**
* get ancestors
* @method
* @memberof Kinetic.Node.prototype
* @returns {Kinetic.Collection}
* @example
* shape.getAncestors().each(function(node) {
* console.log(node.getId());
* })
*/
getAncestors: function() {
var parent = this.getParent(),
ancestors = new Kinetic.Collection();
while (parent) {
ancestors.push(parent);
parent = parent.getParent();
}
return ancestors;
},
/**
* get attrs object literal
* @method
* @memberof Kinetic.Node.prototype
* @returns {Object}
*/
getAttrs: function() {
return this.attrs || {};
},
/**
* set multiple attrs at once using an object literal
* @method
* @memberof Kinetic.Node.prototype
* @param {Object} config object containing key value pairs
* @returns {Kinetic.Node}
* @example
* node.setAttrs({<br>
* x: 5,<br>
* fill: 'red'<br>
* });<br>
*/
setAttrs: function(config) {
var key, method;
if(config) {
for(key in config) {
if (key === CHILDREN) {
}
else {
method = SET + Kinetic.Util._capitalize(key);
// use setter if available
if(Kinetic.Util._isFunction(this[method])) {
this[method](config[key]);
}
// otherwise set directly
else {
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 Kinetic.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 Kinetic.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 Kinetic.Node.prototype
* @returns {Boolean}
*/
shouldDrawHit: function() {
var layer = this.getLayer();
return layer && layer.hitGraphEnabled() && this.isListening() && this.isVisible() && !Kinetic.isDragging();
},
/**
* show node
* @method
* @memberof Kinetic.Node.prototype
* @returns {Kinetic.Node}
*/
show: function() {
this.setVisible(true);
return this;
},
/**
* hide node. Hidden nodes are no longer detectable
* @method
* @memberof Kinetic.Node.prototype
* @returns {Kinetic.Node}
*/
hide: function() {
this.setVisible(false);
return this;
},
/**
* get zIndex relative to the node's siblings who share the same parent
* @method
* @memberof Kinetic.Node.prototype
* @returns {Integer}
*/
getZIndex: function() {
return this.index || 0;
},
/**
* get absolute z-index which takes into account sibling
* and ancestor indices
* @method
* @memberof Kinetic.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.<br><br>
* e.g. Stage depth will always be 0. Layers will always be 1. Groups and Shapes will always
* be >= 2
* @method
* @memberof Kinetic.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
* @method
* @memberof Kinetic.Node.prototype
* @returns {Object}
*/
getAbsolutePosition: function() {
var absoluteMatrix = this.getAbsoluteTransform().getMatrix(),
absoluteTransform = new Kinetic.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 Kinetic.Node.prototype
* @param {Object} pos
* @param {Number} pos.x
* @param {Number} pos.y
* @returns {Kinetic.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 Kinetic.Node.prototype
* @param {Object} change
* @param {Number} change.x
* @param {Number} change.y
* @returns {Kinetic.Node}
* @example
* // move node in x direction by 1px and y direction by 2px<br>
* node.move({<br>
* x: 1,<br>
* y: 2)<br>
* });
*/
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, includeSelf) {
var family = [],
parent = this.getParent(),
len, n;
// build family by traversing ancestors
if(includeSelf) {
family.unshift(this);
}
while(parent) {
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 Kinetic.Node.prototype
* @param {Number} theta
* @returns {Kinetic.Node}
*/
rotate: function(theta) {
this.setRotation(this.getRotation() + theta);
return this;
},
/**
* move node to the top of its siblings
* @method
* @memberof Kinetic.Node.prototype
* @returns {Boolean}
*/
moveToTop: function() {
if (!this.parent) {
Kinetic.Util.warn('Node has no parent. moveToTop function is ignored.');
return;
}
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 Kinetic.Node.prototype
* @returns {Boolean}
*/
moveUp: function() {
if (!this.parent) {
Kinetic.Util.warn('Node has no parent. moveUp function is ignored.');
return;
}
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 Kinetic.Node.prototype
* @returns {Boolean}
*/
moveDown: function() {
if (!this.parent) {
Kinetic.Util.warn('Node has no parent. moveDown function is ignored.');
return;
}
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 Kinetic.Node.prototype
* @returns {Boolean}
*/
moveToBottom: function() {
if (!this.parent) {
Kinetic.Util.warn('Node has no parent. moveToBottom function is ignored.');
return;
}
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 Kinetic.Node.prototype
* @param {Integer} zIndex
* @returns {Kinetic.Node}
*/
setZIndex: function(zIndex) {
if (!this.parent) {
Kinetic.Util.warn('Node has no parent. zIndex parameter is ignored.');
return;
}
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 Kinetic.Node.prototype
* @returns {Number}
*/
getAbsoluteOpacity: function() {
return this._getCache(ABSOLUTE_OPACITY, this._getAbsoluteOpacity);
},
_getAbsoluteOpacity: function() {
var absOpacity = this.getOpacity();
if(this.getParent()) {
absOpacity *= this.getParent().getAbsoluteOpacity();
}
return absOpacity;
},
/**
* move node to another container
* @method
* @memberof Kinetic.Node.prototype
* @param {Container} newContainer
* @returns {Kinetic.Node}
* @example
* // move node from current layer into layer2<br>
* node.moveTo(layer2);
*/
moveTo: function(newContainer) {
Kinetic.Node.prototype.remove.call(this);
newContainer.add(this);
return this;
},
/**
* convert Node into an object for serialization. Returns an object.
* @method
* @memberof Kinetic.Node.prototype
* @returns {Object}
*/
toObject: function() {
var type = Kinetic.Util,
obj = {},
attrs = this.getAttrs(),
key, val, getter, defaultValue;
obj.attrs = {};
// serialize only attributes that are not function, image, DOM, or objects with methods
for(key in attrs) {
val = attrs[key];
if (!type._isFunction(val) && !type._isElement(val) && !(type._isObject(val) && type._hasMethods(val))) {
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 obj;
},
/**
* convert Node into a JSON string. Returns a JSON string.
* @method
* @memberof Kinetic.Node.prototype
* @returns {String}}
*/
toJSON: function() {
return JSON.stringify(this.toObject());
},
/**
* get parent container
* @method
* @memberof Kinetic.Node.prototype
* @returns {Kinetic.Node}
*/
getParent: function() {
return this.parent;
},
/**
* get layer ancestor
* @method
* @memberof Kinetic.Node.prototype
* @returns {Kinetic.Layer}
*/
getLayer: function() {
var parent = this.getParent();
return parent ? parent.getLayer() : null;
},
/**
* get stage ancestor
* @method
* @memberof Kinetic.Node.prototype
* @returns {Kinetic.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 Kinetic.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 {EventObject} [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 {Kinetic.Node}
* @example
* // manually fire click event<br>
* node.fire('click');<br><br>
*
* // fire custom event<br>
* node.fire('foo');<br><br>
*
* // fire custom event with custom event object<br>
* node.fire('foo', {<br>
* bar: 10<br>
* });<br><br>
*
* // fire click event that bubbles<br>
* node.fire('click', null, true);
*/
fire: function(eventType, evt, bubble) {
// bubble
if (bubble) {
this._fireAndBubble(eventType, evt || {});
}
// no bubble
else {
this._fire(eventType, evt || {});
}
return this;
},
/**
* get absolute transform of the node which takes into
* account its ancestor transforms
* @method
* @memberof Kinetic.Node.prototype
* @returns {Kinetic.Transform}
*/
getAbsoluteTransform: function() {
return this._getCache(ABSOLUTE_TRANSFORM, this._getAbsoluteTransform);
},
_getAbsoluteTransform: function() {
var at = new Kinetic.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());
}
}, true);
return at;
},
/**
* get transform of the node
* @method
* @memberof Kinetic.Node.prototype
* @returns {Kinetic.Transform}
*/
getTransform: function() {
return this._getCache(TRANSFORM, this._getTransform);
},
_getTransform: function() {
var m = new Kinetic.Transform(),
x = this.getX(),
y = this.getY(),
rotation = Kinetic.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 Kinetic.Node.prototype
* @param {Object} attrs override attrs
* @returns {Kinetic.Node}
* @example
* // simple clone<br>
* var clone = node.clone();<br><br>
*
* // clone a node and override the x position<br>
* var clone = rect.clone({<br>
* x: 5<br>
* });
*/
clone: function(obj) {
// instantiate new node
var className = this.getClassName(),
attrs = Kinetic.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 Kinetic[className](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 kinetic namespaced listeners because
* these are generated by the constructors
*/
if(listener.name.indexOf(KINETIC) < 0) {
// if listeners array doesn't exist, then create it
if(!node.eventListeners[key]) {
node.eventListeners[key] = [];
}
node.eventListeners[key].push(listener);
}
}
}
return node;
},
/**
* 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 Kinetic.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
* @returns {String}
*/
toDataURL: function(config) {
config = config || {};
var mimeType = config.mimeType || null,
quality = config.quality || null,
stage = this.getStage(),
x = config.x || 0,
y = config.y || 0,
canvas = new Kinetic.SceneCanvas({
width: config.width || this.getWidth() || (stage ? stage.getWidth() : 0),
height: config.height || this.getHeight() || (stage ? stage.getHeight() : 0),
pixelRatio: 1
}),
context = canvas.getContext();
context.save();
if(x || y) {
context.translate(-1 * x, -1 * y);
}
this.drawScene(canvas);
context.restore();
return canvas.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 Kinetic.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
* @example
* var image = node.toImage({<br>
* callback: function(img) {<br>
* // do stuff with img<br>
* }<br>
* });
*/
toImage: function(config) {
Kinetic.Util._getImage(this.toDataURL(config), function(img) {
config.callback(img);
});
},
setSize: function(size) {
this.setWidth(size.width);
this.setHeight(size.height);
return this;
},
getSize: function() {
return {
width: this.getWidth(),
height: this.getHeight()
};
},
/**
* get width
* @method
* @memberof Kinetic.Node.prototype
* @returns {Integer}
*/
getWidth: function() {
return this.attrs.width || 0;
},
/**
* get height
* @method
* @memberof Kinetic.Node.prototype
* @returns {Integer}
*/
getHeight: function() {
return this.attrs.height || 0;
},
/**
* get class name, which may return Stage, Layer, Group, or shape class names like Rect, Circle, Text, etc.
* @method
* @memberof Kinetic.Node.prototype
* @returns {String}
*/
getClassName: function() {
return this.className || this.nodeType;
},
/**
* get the node type, which may return Stage, Layer, Group, or Node
* @method
* @memberof Kinetic.Node.prototype
* @returns {String}
*/
getType: function() {
return this.nodeType;
},
getDragDistance: function() {
// compare with undefined because we need to track 0 value
if (this.attrs.dragDistance !== undefined) {
return this.attrs.dragDistance;
} else if (this.parent) {
return this.parent.getDragDistance();
} else {
return Kinetic.dragDistance;
}
},
_get: function(selector) {
return this.nodeType === selector ? [this] : [];
},
_off: function(type, name) {
var evtListeners = this.eventListeners[type],
i, evtName;
for(i = 0; i < evtListeners.length; i++) {
evtName = evtListeners[i].name;
// the following two conditions must be true in order to remove a handler:
// 1) the current event name cannot be kinetic unless the event name is kinetic
// this enables developers to force remove a kinetic specific listener for whatever reason
// 2) an event name is not specified, or if one is specified, it matches the current event name
if((evtName !== 'kinetic' || name === 'kinetic') && (!name || evtName === name)) {
evtListeners.splice(i, 1);
if(evtListeners.length === 0) {
delete this.eventListeners[type];
break;
}
i--;
}
}
},
_fireChangeEvent: function(attr, oldVal, newVal) {
this._fire(attr + CHANGE, {
oldVal: oldVal,
newVal: newVal
});
},
/**
* set id
* @method
* @memberof Kinetic.Node.prototype
* @param {String} id
* @returns {Kinetic.Node}
*/
setId: function(id) {
var oldId = this.getId();
Kinetic._removeId(oldId);
Kinetic._addId(this, id);
this._setAttr(ID, id);
return this;
},
setName: function(name) {
var oldName = this.getName();
Kinetic._removeName(oldName, this._id);
Kinetic._addName(this, name);
this._setAttr(NAME, name);
return this;
},
/**
* set attr
* @method
* @memberof Kinetic.Node.prototype
* @param {String} attr
* @param {*} val
* @returns {Kinetic.Node}
* @example
* node.setAttr('x', 5);
*/
setAttr: function() {
var args = Array.prototype.slice.call(arguments),
attr = args[0],
val = args[1],
method = SET + Kinetic.Util._capitalize(attr),
func = this[method];
if(Kinetic.Util._isFunction(func)) {
func.call(this, val);
}
// otherwise set directly
else {
this._setAttr(attr, val);
}
return this;
},
_setAttr: function(key, val) {
var oldVal;
if(val !== undefined) {
oldVal = this.attrs[key];
this.attrs[key] = val;
this._fireChangeEvent(key, oldVal, val);
}
},
_setComponentAttr: function(key, component, val) {
var oldVal;
if(val !== undefined) {
oldVal = this.attrs[key];
if (!oldVal) {
// set value to default value using getAttr
this.attrs[key] = this.getAttr(key);
}
this.attrs[key][component] = val;
this._fireChangeEvent(key, oldVal, val);
}
},
_fireAndBubble: function(eventType, evt, compareShape) {
var okayToRun = true;
if(evt && this.nodeType === SHAPE) {
evt.target = this;
}
if(eventType === MOUSEENTER && compareShape && this._id === compareShape._id) {
okayToRun = false;
}
else if(eventType === MOUSELEAVE && compareShape && this._id === compareShape._id) {
okayToRun = false;
}
if(okayToRun) {
this._fire(eventType, evt);
// simulate event bubbling
if(evt && !evt.cancelBubble && this.parent) {
if(compareShape && compareShape.parent) {
this._fireAndBubble.call(this.parent, eventType, evt, compareShape.parent);
}
else {
this._fireAndBubble.call(this.parent, eventType, evt);
}
}
}
},
_fire: function(eventType, evt) {
var events = this.eventListeners[eventType],
i;
evt.type = eventType;
if (events) {
for(i = 0; i < events.length; i++) {
events[i].handler.call(this, evt);
}
}
},
/**
* draw both scene and hit graphs. If the node being drawn is the stage, all of the layers will be cleared and redrawn
* @method
* @memberof Kinetic.Node.prototype
* @returns {Kinetic.Node}
*/
draw: function() {
this.drawScene();
this.drawHit();
return this;
}
});
/**
* create node 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() methods
* @method
* @memberof Kinetic.Node
* @param {String} JSON string
* @param {DomElement} [container] optional container dom element used only if you're
* creating a stage node
*/
Kinetic.Node.create = function(json, container) {
return this._createNode(JSON.parse(json), container);
};
Kinetic.Node._createNode = function(obj, container) {
var className = Kinetic.Node.prototype.getClassName.call(obj),
children = obj.children,
no, len, n;
// if container was passed in, add it to attrs
if(container) {
obj.attrs.container = container;
}
no = new Kinetic[className](obj.attrs);
if(children) {
len = children.length;
for(n = 0; n < len; n++) {
no.add(this._createNode(children[n]));
}
}
return no;
};
// =========================== add getters setters ===========================
Kinetic.Factory.addOverloadedGetterSetter(Kinetic.Node, 'position');
/**
* get/set node position relative to parent
* @name position
* @method
* @memberof Kinetic.Node.prototype
* @param {Object} pos
* @param {Number} pos.x
* @param {Nubmer} pos.y
* @returns {Object}
* @example
* // get position<br>
* var position = node.position();<br><br>
*
* // set position<br>
* node.position({<br>
* x: 5<br>
* y: 10<br>
* });
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'x', 0);
/**
* get/set x position
* @name x
* @method
* @memberof Kinetic.Node.prototype
* @param {Number} x
* @returns {Object}
* @example
* // get x<br>
* var x = node.x();<br><br>
*
* // set x<br>
* node.x(5);
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'y', 0);
/**
* get/set y position
* @name y
* @method
* @memberof Kinetic.Node.prototype
* @param {Number} y
* @returns {Integer}
* @example
* // get y<br>
* var y = node.y();<br><br>
*
* // set y<br>
* node.y(5);
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'opacity', 1);
/**
* get/set opacity. Opacity values range from 0 to 1.
* A node with an opacity of 0 is fully transparent, and a node
* with an opacity of 1 is fully opaque
* @name opacity
* @method
* @memberof Kinetic.Node.prototype
* @param {Object} opacity
* @returns {Number}
* @example
* // get opacity<br>
* var opacity = node.opacity();<br><br>
*
* // set opacity<br>
* node.opacity(0.5);
*/
Kinetic.Factory.addGetter(Kinetic.Node, 'name');
Kinetic.Factory.addOverloadedGetterSetter(Kinetic.Node, 'name');
/**
* get/set name
* @name name
* @method
* @memberof Kinetic.Node.prototype
* @param {String} name
* @returns {String}
* @example
* // get name<br>
* var name = node.name();<br><br>
*
* // set name<br>
* node.name('foo');
*/
Kinetic.Factory.addGetter(Kinetic.Node, 'id');
Kinetic.Factory.addOverloadedGetterSetter(Kinetic.Node, 'id');
/**
* get/set id
* @name id
* @method
* @memberof Kinetic.Node.prototype
* @param {String} id
* @returns {String}
* @example
* // get id<br>
* var name = node.id();<br><br>
*
* // set id<br>
* node.id('foo');
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'rotation', 0);
/**
* get/set rotation in degrees
* @name rotation
* @method
* @memberof Kinetic.Node.prototype
* @param {Number} rotation
* @returns {Number}
* @example
* // get rotation in degrees<br>
* var rotation = node.rotation();<br><br>
*
* // set rotation in degrees<br>
* node.rotation(45);
*/
Kinetic.Factory.addComponentsGetterSetter(Kinetic.Node, 'scale', ['x', 'y']);
/**
* get/set scale
* @name scale
* @param {Object} scale
* @param {Number} scale.x
* @param {Number} scale.y
* @method
* @memberof Kinetic.Node.prototype
* @returns {Object}
* @example
* // get scale<br>
* var scale = node.scale();<br><br>
*
* // set scale <br>
* shape.scale({<br>
* x: 2<br>
* y: 3<br>
* });
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'scaleX', 1);
/**
* get/set scale x
* @name scaleX
* @param {Number} x
* @method
* @memberof Kinetic.Node.prototype
* @returns {Number}
* @example
* // get scale x<br>
* var scaleX = node.scaleX();<br><br>
*
* // set scale x<br>
* node.scaleX(2);
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'scaleY', 1);
/**
* get/set scale y
* @name scaleY
* @param {Number} y
* @method
* @memberof Kinetic.Node.prototype
* @returns {Number}
* @example
* // get scale y<br>
* var scaleY = node.scaleY();<br><br>
*
* // set scale y<br>
* node.scaleY(2);
*/
Kinetic.Factory.addComponentsGetterSetter(Kinetic.Node, 'skew', ['x', 'y']);
/**
* get/set skew
* @name skew
* @param {Object} skew
* @param {Number} skew.x
* @param {Number} skew.y
* @method
* @memberof Kinetic.Node.prototype
* @returns {Object}
* @example
* // get skew<br>
* var skew = node.skew();<br><br>
*
* // set skew <br>
* node.skew({<br>
* x: 20<br>
* y: 10
* });
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'skewX', 0);
/**
* get/set skew x
* @name skewX
* @param {Number} x
* @method
* @memberof Kinetic.Node.prototype
* @returns {Number}
* @example
* // get skew x<br>
* var skewX = node.skewX();<br><br>
*
* // set skew x<br>
* node.skewX(3);
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'skewY', 0);
/**
* get/set skew y
* @name skewY
* @param {Number} y
* @method
* @memberof Kinetic.Node.prototype
* @returns {Number}
* @example
* // get skew y<br>
* var skewY = node.skewY();<br><br>
*
* // set skew y<br>
* node.skewY(3);
*/
Kinetic.Factory.addComponentsGetterSetter(Kinetic.Node, 'offset', ['x', 'y']);
/**
* get/set offset. Offsets the default position and rotation point
* @method
* @memberof Kinetic.Node.prototype
* @param {Object} offset
* @param {Number} offset.x
* @param {Number} offset.y
* @returns {Object}
* @example
* // get offset<br>
* var offset = node.offset();<br><br>
*
* // set offset<br>
* node.offset({<br>
* x: 20<br>
* y: 10<br>
* });
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'offsetX', 0);
/**
* get/set offset x
* @name offsetX
* @memberof Kinetic.Node.prototype
* @param {Number} x
* @returns {Number}
* @example
* // get offset x<br>
* var offsetX = node.offsetX();<br><br>
*
* // set offset x<br>
* node.offsetX(3);
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'offsetY', 0);
/**
* get/set drag distance
* @name dragDistance
* @memberof Kinetic.Node.prototype
* @param {Number} distance
* @returns {Number}
* @example
* // get drag distance<br>
* var dragDistance = node.dragDistance();<br><br>
*
* // set distance<br>
* // node starts dragging only if pointer moved more then 3 pixels<br>
* node.dragDistance(3);<br>
* // or set globally<br>
* Kinetic.dragDistance = 3;
*/
Kinetic.Factory.addSetter(Kinetic.Node, 'dragDistance');
Kinetic.Factory.addOverloadedGetterSetter(Kinetic.Node, 'dragDistance');
/**
* get/set offset y
* @name offsetY
* @method
* @memberof Kinetic.Node.prototype
* @param {Number} y
* @returns {Number}
* @example
* // get offset y<br>
* var offsetY = node.offsetY();<br><br>
*
* // set offset y<br>
* node.offsetY(3);
*/
Kinetic.Factory.addSetter(Kinetic.Node, 'width', 0);
Kinetic.Factory.addOverloadedGetterSetter(Kinetic.Node, 'width');
/**
* get/set width
* @name width
* @method
* @memberof Kinetic.Node.prototype
* @param {Number} width
* @returns {Number}
* @example
* // get width<br>
* var width = node.width();<br><br>
*
* // set width<br>
* node.width(100);
*/
Kinetic.Factory.addSetter(Kinetic.Node, 'height', 0);
Kinetic.Factory.addOverloadedGetterSetter(Kinetic.Node, 'height');
/**
* get/set height
* @name height
* @method
* @memberof Kinetic.Node.prototype
* @param {Number} height
* @returns {Number}
* @example
* // get height<br>
* var height = node.height();<br><br>
*
* // set height<br>
* node.height(100);
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'listening', 'inherit');
/**
* get/set listenig attr. If you need to determine if a node is listening or not
* by taking into account its parents, use the isListening() method
* @name listening
* @method
* @memberof Kinetic.Node.prototype
* @param {Boolean|String} listening Can be "inherit", true, or false. The default is "inherit".
* @returns {Boolean|String}
* @example
* // get listening attr<br>
* var listening = node.listening();<br><br>
*
* // stop listening for events<br>
* node.listening(false);<br><br>
*
* // listen for events<br>
* node.listening(true);<br><br>
*
* // listen to events according to the parent<br>
* node.listening('inherit');
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'filters', undefined, function(val) {this._filterUpToDate = false;return val;});
/**
* get/set filters. Filters are applied to cached canvases
* @name filters
* @method
* @memberof Kinetic.Node.prototype
* @param {Array} filters array of filters
* @returns {Array}
* @example
* // get filters<br>
* var filters = node.filters();<br><br>
*
* // set a single filter<br>
* node.cache();<br>
* node.filters([Kinetic.Filters.Blur]);<br><br>
*
* // set multiple filters<br>
* node.cache();<br>
* node.filters([<br>
* Kinetic.Filters.Blur,<br>
* Kinetic.Filters.Sepia,<br>
* Kinetic.Filters.Invert<br>
* ]);
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'visible', 'inherit');
/**
* get/set visible attr. Can be "inherit", true, or false. The default is "inherit".
* If you need to determine if a node is visible or not
* by taking into account its parents, use the isVisible() method
* @name visible
* @method
* @memberof Kinetic.Node.prototype
* @param {Boolean|String} visible
* @returns {Boolean|String}
* @example
* // get visible attr<br>
* var visible = node.visible();<br><br>
*
* // make invisible<br>
* node.visible(false);<br><br>
*
* // make visible<br>
* node.visible(true);<br><br>
*
* // make visible according to the parent<br>
* node.visible('inherit');
*/
Kinetic.Factory.addGetterSetter(Kinetic.Node, 'transformsEnabled', 'all');
/**
* get/set transforms that are enabled. Can be "all", "none", or "position". The default
* is "all"
* @name transformsEnabled
* @method
* @memberof Kinetic.Node.prototype
* @param {String} enabled
* @returns {String}
* @example
* // enable position transform only to improve draw performance<br>
* node.transformsEnabled('position');<br><br>
*
* // enable all transforms<br>
* node.transformsEnabled('all');
*/
/**
* get/set node size
* @name size
* @method
* @memberof Kinetic.Node.prototype
* @param {Object} size
* @param {Number} size.width
* @param {Number} size.height
* @returns {Object}
* @example
* // get node size<br>
* var size = node.size();<br>
* var x = size.x;<br>
* var y = size.y;<br><br>
*
* // set size<br>
* node.size({<br>
* width: 100,<br>
* height: 200<br>
* });
*/
Kinetic.Factory.addOverloadedGetterSetter(Kinetic.Node, 'size');
Kinetic.Factory.backCompat(Kinetic.Node, {
rotateDeg: 'rotate',
setRotationDeg: 'setRotation',
getRotationDeg: 'getRotation'
});
Kinetic.Collection.mapMethods(Kinetic.Node);
})();