konva/src/Node.js

574 lines
15 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.visible = true;
this.isListening = true;
this.name = undefined;
this.alpha = 1;
this.x = 0;
this.y = 0;
this.scale = {
x: 1,
y: 1
};
this.rotation = 0;
this.centerOffset = {
x: 0,
y: 0
};
this.eventListeners = {};
this.drag = {
x: false,
y: false
};
// set properties from config
if(config) {
for(var key in config) {
// handle special keys
switch (key) {
case "draggable":
this.draggable(config[key]);
break;
case "draggableX":
this.draggableX(config[key]);
break;
case "draggableY":
this.draggableY(config[key]);
break;
case "listen":
this.listen(config[key]);
break;
case "rotationDeg":
this.rotation = config[key] * Math.PI / 180;
break;
default:
this[key] = config[key];
break;
}
}
}
// overrides
if(this.centerOffset.x === undefined) {
this.centerOffset.x = 0;
}
if(this.centerOffset.y === undefined) {
this.centerOffset.y = 0;
}
};
/*
* 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;
}
}
},
/**
* show node
*/
show: function() {
this.visible = true;
},
/**
* hide node
*/
hide: function() {
this.visible = false;
},
/**
* get zIndex
*/
getZIndex: function() {
return this.index;
},
/**
* 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.scale.x = scaleX;
this.scale.y = scaleY;
}
else {
this.scale.x = scaleX;
this.scale.y = scaleX;
}
},
/**
* get scale
*/
getScale: function() {
return this.scale;
},
/**
* set node position
* @param {Number} x
* @param {Number} y
*/
setPosition: function(x, y) {
this.x = x;
this.y = y;
},
/**
* get node position relative to container
*/
getPosition: function() {
return {
x: this.x,
y: this.y
};
},
/**
* get absolute position relative to stage
*/
getAbsolutePosition: function() {
var x = this.x;
var y = this.y;
var parent = this.getParent();
while(parent.className !== "Stage") {
x += parent.x;
y += parent.y;
parent = parent.parent;
}
return {
x: x,
y: y
};
},
/**
* move node by an amount
* @param {Number} x
* @param {Number} y
*/
move: function(x, y) {
this.x += x;
this.y += y;
},
/**
* set node rotation in radians
* @param {Number} theta
*/
setRotation: function(theta) {
this.rotation = theta;
},
/**
* set node rotation in degrees
* @param {Number} deg
*/
setRotationDeg: function(deg) {
this.rotation = (deg * Math.PI / 180);
},
/**
* get rotation in radians
*/
getRotation: function() {
return this.rotation;
},
/**
* get rotation in degrees
*/
getRotationDeg: function() {
return this.rotation * 180 / Math.PI;
},
/**
* rotate node by an amount in radians
* @param {Number} theta
*/
rotate: function(theta) {
this.rotation += theta;
},
/**
* rotate node by an amount in degrees
* @param {Number} deg
*/
rotateDeg: function(deg) {
this.rotation += (deg * Math.PI / 180);
},
/**
* listen or don't listen to events
* @param {Boolean} isListening
*/
listen: function(isListening) {
this.isListening = isListening;
},
/**
* 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.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.alpha;
},
/**
* get absolute alpha
*/
getAbsoluteAlpha: function() {
var absAlpha = 1;
var node = this;
// traverse upwards
while(node.className !== "Stage") {
absAlpha *= node.alpha;
node = node.parent;
}
return absAlpha;
},
/**
* enable or disable drag and drop
* @param {Boolean} setDraggable
*/
draggable: function(setDraggable) {
if(setDraggable) {
var needInit = !this.drag.x && !this.drag.y;
this.drag.x = true;
this.drag.y = true;
if(needInit) {
this._initDrag();
}
}
else {
this.drag.x = false;
this.drag.y = false;
this._dragCleanup();
}
},
/**
* enable or disable horizontal drag and drop
* @param {Boolean} setDraggable
*/
draggableX: function(setDraggable) {
if(setDraggable) {
var needInit = !this.drag.x && !this.drag.y;
this.drag.x = true;
if(needInit) {
this._initDrag();
}
}
else {
this.drag.x = false;
this._dragCleanup();
}
},
/**
* enable or disable vertical drag and drop
* @param {Boolean} setDraggable
*/
draggableY: function(setDraggable) {
if(setDraggable) {
var needInit = !this.drag.x && !this.drag.y;
this.drag.y = true;
if(needInit) {
this._initDrag();
}
}
else {
this.drag.y = false;
this._dragCleanup();
}
},
/**
* 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();
// update children hashes
if(this.name) {
parent.childrenNames[this.name] = undefined;
newContainer.childrenNames[this.name] = this;
}
},
/**
* get parent container
*/
getParent: function() {
return this.parent;
},
/**
* get layer associated to node
*/
getLayer: function() {
if(this.className === 'Layer') {
return this;
}
else {
return this.getParent().getLayer();
}
},
/**
* get stage associated to node
*/
getStage: function() {
if(this.className === 'Stage') {
return this;
}
else {
return this.getParent().getStage();
}
},
/**
* get name
*/
getName: function() {
return this.name;
},
/**
* set center offset
* @param {Number} x
* @param {Number} y
*/
setCenterOffset: function(x, y) {
this.centerOffset.x = x;
this.centerOffset.y = y;
},
/**
* get center offset
*/
getCenterOffset: function() {
return this.centerOffset;
},
/**
* transition node to another state. Any property that can accept a real
* number such as x, y, rotation, alpha, strokeWidth, radius, scale.x, scale.y,
* centerOffset.x and centerOffset.y can be transitioned
* @param {Object} config
*/
transitionTo: function(config) {
var layer = this.getLayer();
var that = this;
var duration = config.duration * 1000;
var changes = {};
for(var key in config) {
if(config.hasOwnProperty(key)) {
if(config[key].x !== undefined || config[key].y !== undefined) {
changes[key] = {};
var propArray = ["x", "y"];
for(var n = 0; n < propArray.length; n++) {
var prop = propArray[n];
if(config[key][prop] !== undefined) {
changes[key][prop] = (config[key][prop] - that[key][prop]) / duration;
}
}
}
else {
changes[key] = (config[key] - that[key]) / duration;
}
}
}
layer.transitions.push({
id: layer.transitionIdCounter++,
time: 0,
config: config,
node: this,
changes: changes
});
},
/**
* initialize drag and drop
*/
_initDrag: function() {
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) {
go.drag.node = that;
go.drag.offset.x = pos.x - that.x;
go.drag.offset.y = pos.y - that.y;
}
});
},
/**
* remove drag and drop event listener
*/
_dragCleanup: function() {
if(!this.drag.x && !this.drag.y) {
this.off("mousedown.initdrag");
this.off("touchstart.initdrag");
}
},
/**
* handle node events
* @param {String} eventType
* @param {Event} evt
*/
_handleEvents: function(eventType, evt) {
// generic events handler
function handle(obj) {
var el = obj.eventListeners;
if(el[eventType]) {
var events = el[eventType];
for(var i = 0; i < events.length; i++) {
events[i].handler.apply(obj, [evt]);
}
}
if(obj.parent.className !== "Stage") {
handle(obj.parent);
}
}
/*
* simulate bubbling by handling node events
* first, followed by group events, followed
* by layer events
*/
handle(this);
}
};