konva/src/Node.js

891 lines
25 KiB
JavaScript
Raw Normal View History

///////////////////////////////////////////////////////////////////////
// Node
///////////////////////////////////////////////////////////////////////
/**
* Node constructor.  Nodes are entities that can move around
* and have events bound to them. They are the building blocks of a KineticJS
* application
* @constructor
* @param {Object} config
*/
Kinetic.Node = function(config) {
this.setDefaultAttrs({
visible: true,
listening: true,
name: undefined,
alpha: 1,
x: 0,
y: 0,
scale: {
x: 1,
y: 1
},
rotation: 0,
centerOffset: {
x: 0,
y: 0
},
dragConstraint: 'none',
dragBounds: {},
draggable: false
});
this.eventListeners = {};
this.setAttrs(config);
};
/*
* Node methods
*/
Kinetic.Node.prototype = {
/**
* bind events to the node. KineticJS supports mouseover, mousemove,
* mouseout, mousedown, mouseup, click, dblclick, touchstart, touchmove,
* touchend, dbltap, dragstart, dragmove, and dragend. Pass in a string
* of event types 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'.
* @param {String} typesStr
* @param {function} handler
*/
on: function(typesStr, handler) {
var types = typesStr.split(' ');
/*
* loop through types and attach event listeners to
* each one. eg. 'click mouseover.namespace mouseout'
* will create three event bindings
*/
for(var n = 0; n < types.length; n++) {
var type = types[n];
var event = (type.indexOf('touch') === -1) ? 'on' + type : type;
var parts = event.split('.');
var baseEvent = parts[0];
var name = parts.length > 1 ? parts[1] : '';
if(!this.eventListeners[baseEvent]) {
this.eventListeners[baseEvent] = [];
}
this.eventListeners[baseEvent].push({
name: name,
handler: handler
});
}
},
/**
* 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'.
* @param {String} typesStr
*/
off: function(typesStr) {
var types = typesStr.split(' ');
for(var n = 0; n < types.length; n++) {
var type = types[n];
var event = (type.indexOf('touch') === -1) ? 'on' + type : type;
var parts = event.split('.');
var baseEvent = parts[0];
if(this.eventListeners[baseEvent] && parts.length > 1) {
var name = parts[1];
for(var i = 0; i < this.eventListeners[baseEvent].length; i++) {
if(this.eventListeners[baseEvent][i].name === name) {
this.eventListeners[baseEvent].splice(i, 1);
if(this.eventListeners[baseEvent].length === 0) {
this.eventListeners[baseEvent] = undefined;
}
break;
}
}
}
else {
this.eventListeners[baseEvent] = undefined;
}
}
},
2012-04-08 09:50:53 +08:00
/**
* get attrs
*/
getAttrs: function() {
return this.attrs;
},
/**
* set default attrs
* @param {Object} confic
*/
setDefaultAttrs: function(config) {
// create attrs object if undefined
if(this.attrs === undefined) {
this.attrs = {};
}
if(config) {
for(var key in config) {
var val = config[key];
this.attrs[key] = config[key];
}
}
},
/**
* set attrs
* @param {Object} config
*/
setAttrs: function(config) {
var go = Kinetic.GlobalObject;
// set properties from config
if(config) {
for(var key in config) {
var val = config[key];
/*
* add functions, DOM elements, and images
* directly to the node
*/
if(go._isFunction(val) || go._isElement(val)) {
this[key] = val;
}
/*
* add all other object types to attrs object
*/
else {
// handle special keys
switch (key) {
/*
* config properties that require a method to
* be set
*/
case 'draggable':
this.draggable(config[key]);
break;
case 'listening':
this.listen(config[key]);
break;
case 'rotationDeg':
this.attrs.rotation = config[key] * Math.PI / 180;
break;
/*
* config objects
*/
case 'centerOffset':
this._setPointAttr(key, val);
break;
case 'scale':
this._setPointAttr(key, val);
break;
case 'points':
/*
* if points contains an array of object, just set
* the attr normally
*/
if(Kinetic.GlobalObject._isObject(val[0])) {
this.attrs[key] = config[key];
}
else {
/*
* convert array of numbers into an array
* of objects containing x, y
*/
var arr = [];
for(var n = 0; n < val.length; n += 2) {
arr.push({
x: val[n],
y: val[n + 1]
});
}
this.attrs[key] = arr;
}
break;
case 'crop':
this._setPointAttr(key, val);
if(val.width !== undefined) {
this.attrs[key].width = val.width;
}
if(val.height !== undefined) {
this.attrs[key].height = val.height;
}
break;
default:
this.attrs[key] = config[key];
break;
}
}
}
}
},
/**
* determine if shape is visible or not
*/
isVisible: function() {
return this.attrs.visible;
},
/**
* show node
*/
show: function() {
this.attrs.visible = true;
},
/**
* hide node
*/
hide: function() {
this.attrs.visible = false;
},
/**
* get zIndex
*/
getZIndex: function() {
return this.index;
},
/**
* get absolute z-index by taking into account
* all parent and sibling indices
*/
getAbsoluteZIndex: function() {
var level = this.getLevel();
var stage = this.getStage();
var that = this;
var index = 0;
function addChildren(children) {
var nodes = [];
for(var n = 0; n < children.length; n++) {
var child = children[n];
index++;
2012-04-05 13:57:36 +08:00
if(child.nodeType !== 'Shape') {
nodes = nodes.concat(child.getChildren());
}
if(child._id === that._id) {
n = children.length;
}
}
if(nodes.length > 0 && nodes[0].getLevel() <= level) {
addChildren(nodes);
}
}
2012-04-05 13:57:36 +08:00
if(that.nodeType !== 'Stage') {
addChildren(that.getStage().getChildren());
}
return index;
},
/**
* get node level in node tree
*/
getLevel: function() {
var level = 0;
var parent = this.parent;
while(parent) {
level++;
parent = parent.parent;
}
return level;
},
/**
* set node scale. If only one parameter is passed in,
* then both scaleX and scaleY are set with that parameter
* @param {Number} scaleX
* @param {Number} scaleY
*/
setScale: function(scaleX, scaleY) {
if(scaleY) {
this.attrs.scale.x = scaleX;
this.attrs.scale.y = scaleY;
}
else {
this.attrs.scale.x = scaleX;
this.attrs.scale.y = scaleX;
}
},
/**
* get scale
*/
getScale: function() {
return this.attrs.scale;
},
/**
* set node position
* @param {Object} point
*/
setPosition: function() {
var pos = Kinetic.GlobalObject._getPoint(arguments);
this.attrs.x = pos.x;
this.attrs.y = pos.y;
},
/**
* set node x position
* @param {Number} x
*/
setX: function(x) {
this.attrs.x = x;
},
/**
* set node y position
* @param {Number} y
*/
setY: function(y) {
this.attrs.y = y;
},
/**
* get node x position
*/
getX: function() {
return this.attrs.x;
},
/**
* get node y position
*/
getY: function() {
return this.attrs.y;
},
/**
* set detection type
* @param {String} type can be "path" or "pixel"
*/
setDetectionType: function(type) {
this.attrs.detectionType = type;
},
/**
* get detection type
*/
getDetectionType: function() {
return this.attrs.detectionType;
},
/**
* get node position relative to container
*/
getPosition: function() {
return {
x: this.attrs.x,
y: this.attrs.y
};
},
/**
* get absolute position relative to stage
*/
getAbsolutePosition: function() {
return this.getAbsoluteTransform().getTranslation();
},
/**
* set absolute position relative to stage
* @param {Object} pos object containing an x and
* y property
*/
setAbsolutePosition: function() {
var pos = Kinetic.GlobalObject._getPoint(arguments);
/*
* save rotation and scale and
* then remove them from the transform
*/
var rot = this.attrs.rotation;
var scale = {
x: this.attrs.scale.x,
y: this.attrs.scale.y
};
var centerOffset = {
x: this.attrs.centerOffset.x,
y: this.attrs.centerOffset.y
};
this.attrs.rotation = 0;
this.attrs.scale = {
x: 1,
y: 1
};
/*
this.attrs.centerOffset = {
x: 0,
y: 0
};
*/
//this.move(-1 * this.attrs.centerOffset.x, -1 * this.attrs.centerOffset.y);
// unravel transform
var 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(pos.x, pos.y);
//this.move(-1* this.attrs.centerOffset.x, -1* this.attrs.centerOffset.y);
// restore rotation and scale
this.rotate(rot);
this.attrs.scale = {
x: scale.x,
y: scale.y
};
},
/**
* move node by an amount
* @param {Number} x
* @param {Number} y
*/
move: function(x, y) {
this.attrs.x += x;
this.attrs.y += y;
},
/**
* set node rotation in radians
* @param {Number} theta
*/
setRotation: function(theta) {
this.attrs.rotation = theta;
},
/**
* set node rotation in degrees
* @param {Number} deg
*/
setRotationDeg: function(deg) {
this.attrs.rotation = (deg * Math.PI / 180);
},
/**
* get rotation in radians
*/
getRotation: function() {
return this.attrs.rotation;
},
/**
* get rotation in degrees
*/
getRotationDeg: function() {
return this.attrs.rotation * 180 / Math.PI;
},
/**
* rotate node by an amount in radians
* @param {Number} theta
*/
rotate: function(theta) {
this.attrs.rotation += theta;
},
/**
* rotate node by an amount in degrees
* @param {Number} deg
*/
rotateDeg: function(deg) {
this.attrs.rotation += (deg * Math.PI / 180);
},
/**
* listen or don't listen to events
2012-04-05 13:57:36 +08:00
* @param {Boolean} listening
*/
2012-04-05 13:57:36 +08:00
listen: function(listening) {
this.attrs.listening = listening;
},
/**
* move node to top
*/
moveToTop: function() {
var index = this.index;
this.parent.children.splice(index, 1);
this.parent.children.push(this);
this.parent._setChildrenIndices();
},
/**
* move node up
*/
moveUp: function() {
var index = this.index;
this.parent.children.splice(index, 1);
this.parent.children.splice(index + 1, 0, this);
this.parent._setChildrenIndices();
},
/**
* move node down
*/
moveDown: function() {
var index = this.index;
if(index > 0) {
this.parent.children.splice(index, 1);
this.parent.children.splice(index - 1, 0, this);
this.parent._setChildrenIndices();
}
},
/**
* move node to bottom
*/
moveToBottom: function() {
var index = this.index;
this.parent.children.splice(index, 1);
this.parent.children.unshift(this);
this.parent._setChildrenIndices();
},
/**
* set zIndex
* @param {int} zIndex
*/
setZIndex: function(zIndex) {
var index = this.index;
this.parent.children.splice(index, 1);
this.parent.children.splice(zIndex, 0, this);
this.parent._setChildrenIndices();
},
/**
* set alpha. Alpha values range from 0 to 1.
* A node with an alpha of 0 is fully transparent, and a node
* with an alpha of 1 is fully opaque
* @param {Object} alpha
*/
setAlpha: function(alpha) {
this.attrs.alpha = alpha;
},
/**
* get alpha. Alpha values range from 0 to 1.
* A node with an alpha of 0 is fully transparent, and a node
* with an alpha of 1 is fully opaque
*/
getAlpha: function() {
return this.attrs.alpha;
},
/**
* get absolute alpha
*/
getAbsoluteAlpha: function() {
var absAlpha = 1;
var node = this;
// traverse upwards
2012-04-05 13:57:36 +08:00
while(node.nodeType !== 'Stage') {
absAlpha *= node.attrs.alpha;
node = node.parent;
}
return absAlpha;
},
/**
* enable or disable drag and drop
* @param {Boolean} isDraggable
*/
draggable: function(isDraggable) {
if(this.attrs.draggable !== isDraggable) {
if(isDraggable) {
this._initDrag();
}
else {
this._dragCleanup();
}
this.attrs.draggable = isDraggable;
}
},
/**
* determine if node is currently in drag and drop mode
*/
isDragging: function() {
var go = Kinetic.GlobalObject;
return go.drag.node !== undefined && go.drag.node._id === this._id && go.drag.moving;
},
/**
* move node to another container
* @param {Container} newContainer
*/
moveTo: function(newContainer) {
var parent = this.parent;
// remove from parent's children
parent.children.splice(this.index, 1);
parent._setChildrenIndices();
// add to new parent
newContainer.children.push(this);
this.index = newContainer.children.length - 1;
this.parent = newContainer;
newContainer._setChildrenIndices();
},
/**
* get parent container
*/
getParent: function() {
return this.parent;
},
/**
* get layer associated to node
*/
getLayer: function() {
2012-04-05 13:57:36 +08:00
if(this.nodeType === 'Layer') {
return this;
}
else {
return this.getParent().getLayer();
}
},
/**
* get stage associated to node
*/
getStage: function() {
2012-04-05 13:57:36 +08:00
if(this.nodeType === 'Stage') {
return this;
}
else {
if(this.getParent() === undefined) {
return undefined;
}
else {
return this.getParent().getStage();
}
}
},
/**
* get name
*/
getName: function() {
return this.attrs.name;
},
/**
* set center offset
* @param {Number} x
* @param {Number} y
*/
setCenterOffset: function(x, y) {
this.attrs.centerOffset.x = x;
this.attrs.centerOffset.y = y;
},
/**
* get center offset
*/
getCenterOffset: function() {
return this.attrs.centerOffset;
},
/**
* transition node to another state. Any property that can accept a real
2012-03-20 13:44:42 +08:00
* number can be transitioned, including x, y, rotation, alpha, strokeWidth,
* radius, scale.x, scale.y, centerOffset.x, centerOffset.y, etc.
* @param {Object} config
2012-03-20 13:44:42 +08:00
* @config {Number} [duration] duration that the transition runs in seconds
* @config {String} [easing] easing function. can be linear, ease-in, ease-out, ease-in-out,
* back-ease-in, back-ease-out, back-ease-in-out, elastic-ease-in, elastic-ease-out,
* elastic-ease-in-out, bounce-ease-out, bounce-ease-in, bounce-ease-in-out,
* strong-ease-in, strong-ease-out, or strong-ease-in-out
* linear is the default
2012-03-20 13:44:42 +08:00
* @config {Function} [callback] callback function to be executed when
* transition completes
*/
transitionTo: function(config) {
var go = Kinetic.GlobalObject;
/*
* clear transition if one is currently running for this
* node
*/
if(this.transAnim !== undefined) {
go._removeAnimation(this.transAnim);
this.transAnim = undefined;
}
/*
* create new transition
*/
2012-04-05 13:57:36 +08:00
var node = this.nodeType === 'Stage' ? this : this.getLayer();
var that = this;
var trans = new Kinetic.Transition(this, config);
var anim = {
func: function() {
trans.onEnterFrame();
},
node: node
};
2012-04-05 13:57:36 +08:00
// store reference to transition animation
this.transAnim = anim;
/*
* adding the animation with the addAnimation
* method auto generates an id
*/
go._addAnimation(anim);
// subscribe to onFinished for first tween
trans.onFinished = function() {
// remove animation
go._removeAnimation(anim);
that.transAnim = undefined;
// callback
if(config.callback !== undefined) {
config.callback();
}
anim.node.draw();
};
// auto start
trans.start();
go._handleAnimation();
return trans;
},
/**
* set drag constraint
* @param {String} constraint
*/
setDragConstraint: function(constraint) {
this.attrs.dragConstraint = constraint;
},
/**
* get drag constraint
*/
getDragConstraint: function() {
return this.attrs.dragConstraint;
},
/**
* set drag bounds
* @param {Object} bounds
* @config {Number} [left] left bounds position
* @config {Number} [top] top bounds position
* @config {Number} [right] right bounds position
* @config {Number} [bottom] bottom bounds position
*/
setDragBounds: function(bounds) {
this.attrs.dragBounds = bounds;
},
/**
* get drag bounds
*/
getDragBounds: function() {
return this.attrs.dragBounds;
},
/**
* get transform of the node while taking into
* account the transforms of its parents
*/
getAbsoluteTransform: function() {
// absolute transform
var am = new Kinetic.Transform();
var family = [];
var parent = this.parent;
family.unshift(this);
while(parent) {
family.unshift(parent);
parent = parent.parent;
}
for(var n = 0; n < family.length; n++) {
var node = family[n];
var m = node.getTransform();
am.multiply(m);
}
return am;
},
/**
* get transform of the node while not taking
* into account the transforms of its parents
*/
getTransform: function() {
var m = new Kinetic.Transform();
if(this.attrs.x !== 0 || this.attrs.y !== 0) {
m.translate(this.attrs.x, this.attrs.y);
}
if(this.attrs.rotation !== 0) {
m.rotate(this.attrs.rotation);
}
if(this.attrs.scale.x !== 1 || this.attrs.scale.y !== 1) {
m.scale(this.attrs.scale.x, this.attrs.scale.y);
}
return m;
},
/**
* initialize drag and drop
*/
_initDrag: function() {
this._dragCleanup();
var go = Kinetic.GlobalObject;
var that = this;
this.on('mousedown.initdrag touchstart.initdrag', function(evt) {
var stage = that.getStage();
var pos = stage.getUserPosition();
if(pos) {
var m = that.getTransform().getTranslation();
var am = that.getAbsoluteTransform().getTranslation();
go.drag.node = that;
go.drag.offset.x = pos.x - that.getAbsoluteTransform().getTranslation().x;
go.drag.offset.y = pos.y - that.getAbsoluteTransform().getTranslation().y;
}
});
},
/**
* remove drag and drop event listener
*/
_dragCleanup: function() {
this.off('mousedown.initdrag');
this.off('touchstart.initdrag');
},
/**
* handle node events
* @param {String} eventType
* @param {Event} evt
*/
_handleEvents: function(eventType, evt) {
2012-04-05 13:57:36 +08:00
if(this.nodeType === 'Shape') {
evt.shape = this;
}
var stage = this.getStage();
this._handleEvent(this, stage.mouseoverShape, stage.mouseoutShape, eventType, evt);
},
/**
* handle node event
*/
_handleEvent: function(node, mouseoverNode, mouseoutNode, eventType, evt) {
var el = node.eventListeners;
var okayToRun = true;
/*
* determine if event handler should be skipped by comparing
* parent nodes
*/
if(eventType === 'onmouseover' && mouseoutNode && mouseoutNode._id === node._id) {
okayToRun = false;
}
else if(eventType === 'onmouseout' && mouseoverNode && mouseoverNode._id === node._id) {
okayToRun = false;
}
if(el[eventType] && okayToRun) {
var events = el[eventType];
for(var i = 0; i < events.length; i++) {
events[i].handler.apply(node, [evt]);
}
}
var mouseoverParent = mouseoverNode ? mouseoverNode.parent : undefined;
var mouseoutParent = mouseoutNode ? mouseoutNode.parent : undefined;
// simulate event bubbling
2012-04-05 13:57:36 +08:00
if(!evt.cancelBubble && node.parent.nodeType !== 'Stage') {
this._handleEvent(node.parent, mouseoverParent, mouseoutParent, eventType, evt);
}
},
/**
* set point attr
*/
_setPointAttr: function(key, val) {
if(Kinetic.GlobalObject._isArray(val)) {
// val is an array
this.attrs[key].x = val[0];
this.attrs[key].y = val[1];
}
else {
// val is an object
if(val.x !== undefined) {
this.attrs[key].x = val.x;
}
if(val.y !== undefined) {
this.attrs[key].y = val.y;
}
}
}
};