mirror of
https://github.com/konvajs/konva.git
synced 2025-04-28 21:59:09 +08:00
4189 lines
114 KiB
JavaScript
4189 lines
114 KiB
JavaScript
/**
|
|
* KineticJS JavaScript Library core
|
|
* http://www.kineticjs.com/
|
|
* Copyright 2012, Eric Rowell
|
|
* Licensed under the MIT or GPL Version 2 licenses.
|
|
* Date: May 08 2012
|
|
*
|
|
* Copyright (C) 2011 - 2012 by Eric Rowell
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Global Object
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Kinetic Namespace
|
|
* @namespace
|
|
*/
|
|
var Kinetic = {};
|
|
/**
|
|
* Kinetic Global Object
|
|
* @property {Object} GlobalObjet
|
|
*/
|
|
Kinetic.GlobalObject = {
|
|
stages: [],
|
|
idCounter: 0,
|
|
tempNodes: [],
|
|
animations: [],
|
|
animIdCounter: 0,
|
|
animRunning: false,
|
|
dragTimeInterval: 0,
|
|
maxDragTimeInterval: 20,
|
|
frame: {
|
|
time: 0,
|
|
timeDiff: 0,
|
|
lastTime: 0
|
|
},
|
|
drag: {
|
|
moving: false,
|
|
node: undefined,
|
|
offset: {
|
|
x: 0,
|
|
y: 0
|
|
},
|
|
lastDrawTime: 0
|
|
},
|
|
extend: function(obj1, obj2) {
|
|
for(var key in obj2.prototype) {
|
|
if(obj2.prototype.hasOwnProperty(key) && obj1.prototype[key] === undefined) {
|
|
obj1.prototype[key] = obj2.prototype[key];
|
|
}
|
|
}
|
|
},
|
|
_pullNodes: function(stage) {
|
|
var tempNodes = this.tempNodes;
|
|
for(var n = 0; n < tempNodes.length; n++) {
|
|
var node = tempNodes[n];
|
|
if(node.getStage() !== undefined && node.getStage()._id === stage._id) {
|
|
stage._addId(node);
|
|
stage._addName(node);
|
|
this.tempNodes.splice(n, 1);
|
|
n -= 1;
|
|
}
|
|
}
|
|
},
|
|
/*
|
|
* animation support
|
|
*/
|
|
_addAnimation: function(anim) {
|
|
anim.id = this.animIdCounter++;
|
|
this.animations.push(anim);
|
|
},
|
|
_removeAnimation: function(anim) {
|
|
var id = anim.id;
|
|
var animations = this.animations;
|
|
for(var n = 0; n < animations.length; n++) {
|
|
if(animations[n].id === id) {
|
|
this.animations.splice(n, 1);
|
|
return false;
|
|
}
|
|
}
|
|
},
|
|
_runFrames: function() {
|
|
var nodes = {};
|
|
for(var n = 0; n < this.animations.length; n++) {
|
|
var anim = this.animations[n];
|
|
if(anim.node && anim.node._id !== undefined) {
|
|
nodes[anim.node._id] = anim.node;
|
|
}
|
|
anim.func(this.frame);
|
|
}
|
|
|
|
for(var key in nodes) {
|
|
nodes[key].draw();
|
|
}
|
|
},
|
|
_updateFrameObject: function() {
|
|
var date = new Date();
|
|
var time = date.getTime();
|
|
if(this.frame.lastTime === 0) {
|
|
this.frame.lastTime = time;
|
|
}
|
|
else {
|
|
this.frame.timeDiff = time - this.frame.lastTime;
|
|
this.frame.lastTime = time;
|
|
this.frame.time += this.frame.timeDiff;
|
|
}
|
|
},
|
|
_animationLoop: function() {
|
|
if(this.animations.length > 0) {
|
|
this._updateFrameObject();
|
|
this._runFrames();
|
|
var that = this;
|
|
requestAnimFrame(function() {
|
|
that._animationLoop();
|
|
});
|
|
}
|
|
else {
|
|
this.animRunning = false;
|
|
this.frame.lastTime = 0;
|
|
}
|
|
},
|
|
_handleAnimation: function() {
|
|
var that = this;
|
|
if(!this.animRunning) {
|
|
this.animRunning = true;
|
|
that._animationLoop();
|
|
}
|
|
else {
|
|
this.frame.lastTime = 0;
|
|
}
|
|
},
|
|
/*
|
|
* utilities
|
|
*/
|
|
_isElement: function(obj) {
|
|
return !!(obj && obj.nodeType == 1);
|
|
},
|
|
_isFunction: function(obj) {
|
|
return !!(obj && obj.constructor && obj.call && obj.apply);
|
|
},
|
|
_getPoint: function(arg) {
|
|
|
|
if(arg.length === 1) {
|
|
return arg[0];
|
|
}
|
|
else {
|
|
return {
|
|
x: arg[0],
|
|
y: arg[1]
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
window.requestAnimFrame = (function(callback) {
|
|
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
|
|
function(callback) {
|
|
window.setTimeout(callback, 1000 / 60);
|
|
};
|
|
})();
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// 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;
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* 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':
|
|
if(val.x !== undefined) {
|
|
this.attrs[key].x = val.x;
|
|
}
|
|
if(val.y !== undefined) {
|
|
this.attrs[key].y = val.y;
|
|
}
|
|
break;
|
|
case 'scale':
|
|
if(val.x !== undefined) {
|
|
this.attrs[key].x = val.x;
|
|
}
|
|
if(val.y !== undefined) {
|
|
this.attrs[key].y = val.y;
|
|
}
|
|
break;
|
|
case 'crop':
|
|
if(val.x !== undefined) {
|
|
this.attrs[key].x = val.x;
|
|
}
|
|
if(val.y !== undefined) {
|
|
this.attrs[key].y = val.y;
|
|
}
|
|
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++;
|
|
|
|
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);
|
|
}
|
|
}
|
|
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
|
|
* @param {Boolean} listening
|
|
*/
|
|
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
|
|
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() {
|
|
if(this.nodeType === 'Layer') {
|
|
return this;
|
|
}
|
|
else {
|
|
return this.getParent().getLayer();
|
|
}
|
|
},
|
|
/**
|
|
* get stage associated to node
|
|
*/
|
|
getStage: function() {
|
|
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
|
|
* number can be transitioned, including x, y, rotation, alpha, strokeWidth,
|
|
* radius, scale.x, scale.y, centerOffset.x, centerOffset.y, etc.
|
|
* @param {Object} config
|
|
* @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
|
|
* @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
|
|
*/
|
|
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
|
|
};
|
|
|
|
// 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) {
|
|
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
|
|
if(!evt.cancelBubble && node.parent.nodeType !== 'Stage') {
|
|
this._handleEvent(node.parent, mouseoverParent, mouseoutParent, eventType, evt);
|
|
}
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Container
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Container constructor. Containers are used to contain nodes or other containers
|
|
* @constructor
|
|
*/
|
|
Kinetic.Container = function() {
|
|
this.children = [];
|
|
};
|
|
/*
|
|
* Container methods
|
|
*/
|
|
Kinetic.Container.prototype = {
|
|
/**
|
|
* get children
|
|
*/
|
|
getChildren: function() {
|
|
return this.children;
|
|
},
|
|
/**
|
|
* remove all children
|
|
*/
|
|
removeChildren: function() {
|
|
while(this.children.length > 0) {
|
|
this.remove(this.children[0]);
|
|
}
|
|
},
|
|
/**
|
|
* remove child from container
|
|
* @param {Node} child
|
|
*/
|
|
_remove: function(child) {
|
|
if(child.index !== undefined && this.children[child.index]._id == child._id) {
|
|
var stage = this.getStage();
|
|
if(stage !== undefined) {
|
|
stage._removeId(child);
|
|
stage._removeName(child);
|
|
}
|
|
|
|
var go = Kinetic.GlobalObject;
|
|
for(var n = 0; n < go.tempNodes.length; n++) {
|
|
var node = go.tempNodes[n];
|
|
if(node._id === child._id) {
|
|
go.tempNodes.splice(n, 1);
|
|
n = go.tempNodes.length;
|
|
}
|
|
}
|
|
|
|
this.children.splice(child.index, 1);
|
|
this._setChildrenIndices();
|
|
child = undefined;
|
|
}
|
|
},
|
|
/**
|
|
* return an array of nodes that match the selector. Use '#' for id selections
|
|
* and '.' for name selections
|
|
* ex:
|
|
* var node = stage.get('#foo'); // selects node with id foo
|
|
* var nodes = layer.get('.bar'); // selects nodes with name bar inside layer
|
|
* @param {String} selector
|
|
*/
|
|
get: function(selector) {
|
|
var stage = this.getStage();
|
|
var arr;
|
|
var key = selector.slice(1);
|
|
if(selector.charAt(0) === '#') {
|
|
arr = stage.ids[key] !== undefined ? [stage.ids[key]] : [];
|
|
}
|
|
else if(selector.charAt(0) === '.') {
|
|
arr = stage.names[key] !== undefined ? stage.names[key] : [];
|
|
}
|
|
else if(selector === 'Shape' || selector === 'Group' || selector === 'Layer') {
|
|
return this._getNodes(selector);
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
|
|
var retArr = [];
|
|
for(var n = 0; n < arr.length; n++) {
|
|
var node = arr[n];
|
|
if(this.isAncestorOf(node)) {
|
|
retArr.push(node);
|
|
}
|
|
}
|
|
|
|
return retArr;
|
|
},
|
|
/**
|
|
* determine if node is an ancestor
|
|
* of descendant
|
|
* @param {Kinetic.Node} node
|
|
*/
|
|
isAncestorOf: function(node) {
|
|
if(this.nodeType === 'Stage') {
|
|
return true;
|
|
}
|
|
|
|
var parent = node.getParent();
|
|
while(parent) {
|
|
if(parent._id === this._id) {
|
|
return true;
|
|
}
|
|
parent = parent.getParent();
|
|
}
|
|
|
|
return false;
|
|
},
|
|
/**
|
|
* get all shapes inside container
|
|
*/
|
|
_getNodes: function(sel) {
|
|
var arr = [];
|
|
function traverse(cont) {
|
|
var children = cont.getChildren();
|
|
for(var n = 0; n < children.length; n++) {
|
|
var child = children[n];
|
|
if(child.nodeType === sel) {
|
|
arr.push(child);
|
|
}
|
|
else if(child.nodeType !== 'Shape') {
|
|
traverse(child);
|
|
}
|
|
}
|
|
}
|
|
traverse(this);
|
|
|
|
return arr;
|
|
},
|
|
/**
|
|
* draw children
|
|
*/
|
|
_drawChildren: function() {
|
|
var stage = this.getStage();
|
|
var children = this.children;
|
|
for(var n = 0; n < children.length; n++) {
|
|
var child = children[n];
|
|
if(child.nodeType === 'Shape' && child.isVisible() && stage.isVisible()) {
|
|
child._draw(child.getLayer());
|
|
}
|
|
else {
|
|
child._draw();
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* add node to container
|
|
* @param {Node} child
|
|
*/
|
|
_add: function(child) {
|
|
child._id = Kinetic.GlobalObject.idCounter++;
|
|
child.index = this.children.length;
|
|
child.parent = this;
|
|
|
|
this.children.push(child);
|
|
|
|
var stage = child.getStage();
|
|
if(stage === undefined) {
|
|
var go = Kinetic.GlobalObject;
|
|
go.tempNodes.push(child);
|
|
}
|
|
else {
|
|
stage._addId(child);
|
|
stage._addName(child);
|
|
|
|
/*
|
|
* pull in other nodes that are now linked
|
|
* to a stage
|
|
*/
|
|
var go = Kinetic.GlobalObject;
|
|
go._pullNodes(stage);
|
|
}
|
|
},
|
|
/**
|
|
* set children indices
|
|
*/
|
|
_setChildrenIndices: function() {
|
|
/*
|
|
* if reordering Layers, remove all canvas elements
|
|
* from the container except the buffer and backstage canvases
|
|
* and then readd all the layers
|
|
*/
|
|
if(this.nodeType === 'Stage') {
|
|
var canvases = this.content.children;
|
|
var bufferCanvas = canvases[0];
|
|
var backstageCanvas = canvases[1];
|
|
|
|
this.content.innerHTML = '';
|
|
this.content.appendChild(bufferCanvas);
|
|
this.content.appendChild(backstageCanvas);
|
|
}
|
|
|
|
for(var n = 0; n < this.children.length; n++) {
|
|
this.children[n].index = n;
|
|
|
|
if(this.nodeType === 'Stage') {
|
|
this.content.appendChild(this.children[n].canvas);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Stage
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Stage constructor. A stage is used to contain multiple layers and handle
|
|
* animations
|
|
* @constructor
|
|
* @augments Kinetic.Container
|
|
* @augments Kinetic.Node
|
|
* @param {String|DomElement} cont Container id or DOM element
|
|
* @param {int} width
|
|
* @param {int} height
|
|
*/
|
|
Kinetic.Stage = function(config) {
|
|
this.setDefaultAttrs({
|
|
width: 400,
|
|
height: 200
|
|
});
|
|
|
|
this.nodeType = 'Stage';
|
|
|
|
/*
|
|
* if container is a string, assume it's an id for
|
|
* a DOM element
|
|
*/
|
|
if( typeof config.container === 'string') {
|
|
config.container = document.getElementById(config.container);
|
|
}
|
|
|
|
// call super constructors
|
|
Kinetic.Container.apply(this, []);
|
|
Kinetic.Node.apply(this, [config]);
|
|
|
|
this.container = config.container;
|
|
this.content = document.createElement('div');
|
|
this.dblClickWindow = 400;
|
|
|
|
this._setDefaults();
|
|
|
|
// set stage id
|
|
this._id = Kinetic.GlobalObject.idCounter++;
|
|
|
|
this._buildDOM();
|
|
this._listen();
|
|
this._prepareDrag();
|
|
|
|
var go = Kinetic.GlobalObject;
|
|
go.stages.push(this);
|
|
this._addId(this);
|
|
this._addName(this);
|
|
};
|
|
/*
|
|
* Stage methods
|
|
*/
|
|
Kinetic.Stage.prototype = {
|
|
/**
|
|
* sets onFrameFunc for animation
|
|
* @param {function} func
|
|
*/
|
|
onFrame: function(func) {
|
|
var go = Kinetic.GlobalObject;
|
|
this.anim = {
|
|
func: func
|
|
};
|
|
},
|
|
/**
|
|
* start animation
|
|
*/
|
|
start: function() {
|
|
if(!this.animRunning) {
|
|
var go = Kinetic.GlobalObject;
|
|
go._addAnimation(this.anim);
|
|
go._handleAnimation();
|
|
this.animRunning = true;
|
|
}
|
|
},
|
|
/**
|
|
* stop animation
|
|
*/
|
|
stop: function() {
|
|
var go = Kinetic.GlobalObject;
|
|
go._removeAnimation(this.anim);
|
|
this.animRunning = false;
|
|
},
|
|
/**
|
|
* draw children
|
|
*/
|
|
draw: function() {
|
|
this._drawChildren();
|
|
},
|
|
/**
|
|
* set stage size
|
|
* @param {int} width
|
|
* @param {int} height
|
|
*/
|
|
setSize: function(width, height) {
|
|
// set stage dimensions
|
|
this.attrs.width = width;
|
|
this.attrs.height = height;
|
|
|
|
// set content dimensions
|
|
this.content.style.width = this.attrs.width + 'px';
|
|
this.content.style.height = this.attrs.height + 'px';
|
|
|
|
// set buffer layer and path layer sizes
|
|
this.bufferLayer.getCanvas().width = width;
|
|
this.bufferLayer.getCanvas().height = height;
|
|
this.pathLayer.getCanvas().width = width;
|
|
this.pathLayer.getCanvas().height = height;
|
|
|
|
// set user defined layer dimensions
|
|
var layers = this.children;
|
|
for(var n = 0; n < layers.length; n++) {
|
|
var layer = layers[n];
|
|
layer.getCanvas().width = width;
|
|
layer.getCanvas().height = height;
|
|
layer.draw();
|
|
}
|
|
},
|
|
/**
|
|
* return stage size
|
|
*/
|
|
getSize: function() {
|
|
return {
|
|
width: this.attrs.width,
|
|
height: this.attrs.height
|
|
};
|
|
},
|
|
/**
|
|
* clear all layers
|
|
*/
|
|
clear: function() {
|
|
var layers = this.children;
|
|
for(var n = 0; n < layers.length; n++) {
|
|
layers[n].clear();
|
|
}
|
|
},
|
|
/**
|
|
* Creates a composite data URL and passes it to a callback. 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)
|
|
* @param {function} callback
|
|
* @param {String} mimeType (optional)
|
|
* @param {Number} quality (optional)
|
|
*/
|
|
toDataURL: function(callback, mimeType, quality) {
|
|
var bufferLayer = this.bufferLayer;
|
|
var bufferContext = bufferLayer.getContext();
|
|
var layers = this.children;
|
|
var that = this;
|
|
|
|
function addLayer(n) {
|
|
var dataURL = layers[n].getCanvas().toDataURL();
|
|
var imageObj = new Image();
|
|
imageObj.onload = function() {
|
|
bufferContext.drawImage(this, 0, 0);
|
|
n++;
|
|
if(n < layers.length) {
|
|
addLayer(n);
|
|
}
|
|
else {
|
|
try {
|
|
// If this call fails (due to browser bug, like in Firefox 3.6),
|
|
// then revert to previous no-parameter image/png behavior
|
|
callback(bufferLayer.getCanvas().toDataURL(mimeType, quality));
|
|
}
|
|
catch(exception) {
|
|
callback(bufferLayer.getCanvas().toDataURL());
|
|
}
|
|
}
|
|
};
|
|
imageObj.src = dataURL;
|
|
}
|
|
|
|
bufferLayer.clear();
|
|
addLayer(0);
|
|
},
|
|
/**
|
|
* serialize stage and children as a JSON object
|
|
*/
|
|
toJSON: function() {
|
|
var go = Kinetic.GlobalObject;
|
|
|
|
function addNode(node) {
|
|
var obj = {};
|
|
obj.attrs = node.attrs;
|
|
|
|
obj.nodeType = node.nodeType;
|
|
obj.shapeType = node.shapeType;
|
|
|
|
if(node.nodeType !== 'Shape') {
|
|
obj.children = [];
|
|
|
|
var children = node.getChildren();
|
|
for(var n = 0; n < children.length; n++) {
|
|
var child = children[n];
|
|
obj.children.push(addNode(child));
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
return JSON.stringify(addNode(this));
|
|
},
|
|
/**
|
|
* reset stage to default state
|
|
*/
|
|
reset: function() {
|
|
// remove children
|
|
this.removeChildren();
|
|
|
|
// reset stage defaults
|
|
this._setDefaults();
|
|
|
|
// reset node attrs
|
|
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
|
|
});
|
|
},
|
|
/**
|
|
* load stage 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()
|
|
* @param {String} JSON string
|
|
*/
|
|
load: function(json) {
|
|
this.reset();
|
|
|
|
function loadNode(node, obj) {
|
|
var children = obj.children;
|
|
if(children !== undefined) {
|
|
for(var n = 0; n < children.length; n++) {
|
|
var child = children[n];
|
|
var type;
|
|
|
|
// determine type
|
|
if(child.nodeType === 'Shape') {
|
|
// add custom shape
|
|
if(child.shapeType === undefined) {
|
|
type = 'Shape';
|
|
}
|
|
// add standard shape
|
|
else {
|
|
type = child.shapeType;
|
|
}
|
|
}
|
|
else {
|
|
type = child.nodeType;
|
|
}
|
|
|
|
var no = new Kinetic[type](child.attrs);
|
|
node.add(no);
|
|
loadNode(no, child);
|
|
}
|
|
}
|
|
}
|
|
var obj = JSON.parse(json);
|
|
|
|
// copy over stage properties
|
|
this.attrs = obj.attrs;
|
|
|
|
loadNode(this, obj);
|
|
this.draw();
|
|
},
|
|
/**
|
|
* remove layer from stage
|
|
* @param {Layer} layer
|
|
*/
|
|
remove: function(layer) {
|
|
/*
|
|
* remove canvas DOM from the document if
|
|
* it exists
|
|
*/
|
|
try {
|
|
this.content.removeChild(layer.canvas);
|
|
}
|
|
catch(e) {
|
|
}
|
|
this._remove(layer);
|
|
},
|
|
/**
|
|
* add layer to stage
|
|
* @param {Layer} layer
|
|
*/
|
|
add: function(layer) {
|
|
layer.canvas.width = this.attrs.width;
|
|
layer.canvas.height = this.attrs.height;
|
|
this._add(layer);
|
|
|
|
// draw layer and append canvas to container
|
|
layer.draw();
|
|
this.content.appendChild(layer.canvas);
|
|
},
|
|
/**
|
|
* get mouse position for desktop apps
|
|
* @param {Event} evt
|
|
*/
|
|
getMousePosition: function(evt) {
|
|
return this.mousePos;
|
|
},
|
|
/**
|
|
* get touch position for mobile apps
|
|
* @param {Event} evt
|
|
*/
|
|
getTouchPosition: function(evt) {
|
|
return this.touchPos;
|
|
},
|
|
/**
|
|
* get user position (mouse position or touch position)
|
|
* @param {Event} evt
|
|
*/
|
|
getUserPosition: function(evt) {
|
|
return this.getTouchPosition() || this.getMousePosition();
|
|
},
|
|
/**
|
|
* get container DOM element
|
|
*/
|
|
getContainer: function() {
|
|
return this.container;
|
|
},
|
|
/**
|
|
* get content DOM element
|
|
*/
|
|
getContent: function() {
|
|
return this.content;
|
|
},
|
|
/**
|
|
* get stage
|
|
*/
|
|
getStage: function() {
|
|
return this;
|
|
},
|
|
/**
|
|
* get width
|
|
*/
|
|
getWidth: function() {
|
|
return this.attrs.width;
|
|
},
|
|
/**
|
|
* get height
|
|
*/
|
|
getHeight: function() {
|
|
return this.attrs.height;
|
|
},
|
|
/**
|
|
* get shapes that intersect a point
|
|
* @param {Object} point
|
|
*/
|
|
getIntersections: function() {
|
|
var pos = Kinetic.GlobalObject._getPoint(arguments);
|
|
var arr = [];
|
|
var shapes = this.get('Shape');
|
|
|
|
for(var n = 0; n < shapes.length; n++) {
|
|
var shape = shapes[n];
|
|
if(shape.intersects(pos)) {
|
|
arr.push(shape);
|
|
}
|
|
}
|
|
|
|
return arr;
|
|
},
|
|
/**
|
|
* get stage DOM node, which is a div element
|
|
* with the class name "kineticjs-content"
|
|
*/
|
|
getDOM: function() {
|
|
return this.content;
|
|
},
|
|
/**
|
|
* detect event
|
|
* @param {Shape} shape
|
|
*/
|
|
_detectEvent: function(shape, evt) {
|
|
var isDragging = Kinetic.GlobalObject.drag.moving;
|
|
var go = Kinetic.GlobalObject;
|
|
var pos = this.getUserPosition();
|
|
var el = shape.eventListeners;
|
|
|
|
if(this.targetShape && shape._id === this.targetShape._id) {
|
|
this.targetFound = true;
|
|
}
|
|
|
|
if(shape.attrs.visible && pos !== undefined && shape.intersects(pos)) {
|
|
// handle onmousedown
|
|
if(!isDragging && this.mouseDown) {
|
|
this.mouseDown = false;
|
|
this.clickStart = true;
|
|
shape._handleEvents('onmousedown', evt);
|
|
return true;
|
|
}
|
|
// handle onmouseup & onclick
|
|
else if(this.mouseUp) {
|
|
this.mouseUp = false;
|
|
shape._handleEvents('onmouseup', evt);
|
|
|
|
// detect if click or double click occurred
|
|
if(this.clickStart) {
|
|
/*
|
|
* if dragging and dropping, don't fire click or dbl click
|
|
* event
|
|
*/
|
|
if((!go.drag.moving) || !go.drag.node) {
|
|
shape._handleEvents('onclick', evt);
|
|
|
|
if(shape.inDoubleClickWindow) {
|
|
shape._handleEvents('ondblclick', evt);
|
|
}
|
|
shape.inDoubleClickWindow = true;
|
|
setTimeout(function() {
|
|
shape.inDoubleClickWindow = false;
|
|
}, this.dblClickWindow);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// handle touchstart
|
|
else if(this.touchStart) {
|
|
this.touchStart = false;
|
|
shape._handleEvents('touchstart', evt);
|
|
|
|
if(el.ondbltap && shape.inDoubleClickWindow) {
|
|
var events = el.ondbltap;
|
|
for(var i = 0; i < events.length; i++) {
|
|
events[i].handler.apply(shape, [evt]);
|
|
}
|
|
}
|
|
|
|
shape.inDoubleClickWindow = true;
|
|
|
|
setTimeout(function() {
|
|
shape.inDoubleClickWindow = false;
|
|
}, this.dblClickWindow);
|
|
return true;
|
|
}
|
|
|
|
// handle touchend
|
|
else if(this.touchEnd) {
|
|
this.touchEnd = false;
|
|
shape._handleEvents('touchend', evt);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* NOTE: these event handlers require target shape
|
|
* handling
|
|
*/
|
|
|
|
// handle onmouseover
|
|
else if(!isDragging && this._isNewTarget(shape, evt)) {
|
|
/*
|
|
* check to see if there are stored mouseout events first.
|
|
* if there are, run those before running the onmouseover
|
|
* events
|
|
*/
|
|
if(this.mouseoutShape) {
|
|
this.mouseoverShape = shape;
|
|
this.mouseoutShape._handleEvents('onmouseout', evt);
|
|
this.mouseoverShape = undefined;
|
|
}
|
|
|
|
shape._handleEvents('onmouseover', evt);
|
|
this._setTarget(shape);
|
|
return true;
|
|
}
|
|
|
|
// handle mousemove and touchmove
|
|
else if(!isDragging) {
|
|
shape._handleEvents('onmousemove', evt);
|
|
shape._handleEvents('touchmove', evt);
|
|
return true;
|
|
}
|
|
}
|
|
// handle mouseout condition
|
|
else if(!isDragging && this.targetShape && this.targetShape._id === shape._id) {
|
|
this._setTarget(undefined);
|
|
this.mouseoutShape = shape;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
/**
|
|
* set new target
|
|
*/
|
|
_setTarget: function(shape) {
|
|
this.targetShape = shape;
|
|
this.targetFound = true;
|
|
},
|
|
/**
|
|
* check if shape should be a new target
|
|
*/
|
|
_isNewTarget: function(shape, evt) {
|
|
if(!this.targetShape || (!this.targetFound && shape._id !== this.targetShape._id)) {
|
|
/*
|
|
* check if old target has an onmouseout event listener
|
|
*/
|
|
if(this.targetShape) {
|
|
var oldEl = this.targetShape.eventListeners;
|
|
if(oldEl) {
|
|
this.mouseoutShape = this.targetShape;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
},
|
|
/**
|
|
* traverse container children
|
|
* @param {Container} obj
|
|
*/
|
|
_traverseChildren: function(obj, evt) {
|
|
var children = obj.children;
|
|
// propapgate backwards through children
|
|
for(var i = children.length - 1; i >= 0; i--) {
|
|
var child = children[i];
|
|
if(child.attrs.listening) {
|
|
if(child.nodeType === 'Shape') {
|
|
var exit = this._detectEvent(child, evt);
|
|
if(exit) {
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
var exit = this._traverseChildren(child, evt);
|
|
if(exit) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
/**
|
|
* handle incoming event
|
|
* @param {Event} evt
|
|
*/
|
|
_handleStageEvent: function(evt) {
|
|
var go = Kinetic.GlobalObject;
|
|
if(!evt) {
|
|
evt = window.event;
|
|
}
|
|
|
|
this._setMousePosition(evt);
|
|
this._setTouchPosition(evt);
|
|
this.pathLayer.clear();
|
|
|
|
/*
|
|
* loop through layers. If at any point an event
|
|
* is triggered, n is set to -1 which will break out of the
|
|
* three nested loops
|
|
*/
|
|
this.targetFound = false;
|
|
var shapeDetected = false;
|
|
for(var n = this.children.length - 1; n >= 0; n--) {
|
|
var layer = this.children[n];
|
|
if(layer.attrs.visible && n >= 0 && layer.attrs.listening) {
|
|
if(this._traverseChildren(layer, evt)) {
|
|
n = -1;
|
|
shapeDetected = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* if no shape was detected and a mouseout shape has been stored,
|
|
* then run the onmouseout event handlers
|
|
*/
|
|
if(!shapeDetected && this.mouseoutShape) {
|
|
this.mouseoutShape._handleEvents('onmouseout', evt);
|
|
this.mouseoutShape = undefined;
|
|
}
|
|
},
|
|
/**
|
|
* begin listening for events by adding event handlers
|
|
* to the container
|
|
*/
|
|
_listen: function() {
|
|
var that = this;
|
|
|
|
// desktop events
|
|
this.content.addEventListener('mousedown', function(evt) {
|
|
that.mouseDown = true;
|
|
that._handleStageEvent(evt);
|
|
}, false);
|
|
|
|
this.content.addEventListener('mousemove', function(evt) {
|
|
that.mouseUp = false;
|
|
that.mouseDown = false;
|
|
that._handleStageEvent(evt);
|
|
}, false);
|
|
|
|
this.content.addEventListener('mouseup', function(evt) {
|
|
that.mouseUp = true;
|
|
that.mouseDown = false;
|
|
that._handleStageEvent(evt);
|
|
|
|
that.clickStart = false;
|
|
}, false);
|
|
|
|
this.content.addEventListener('mouseover', function(evt) {
|
|
that._handleStageEvent(evt);
|
|
}, false);
|
|
|
|
this.content.addEventListener('mouseout', function(evt) {
|
|
// if there's a current target shape, run mouseout handlers
|
|
var targetShape = that.targetShape;
|
|
if(targetShape) {
|
|
targetShape._handleEvents('onmouseout', evt);
|
|
that.targetShape = undefined;
|
|
}
|
|
that.mousePos = undefined;
|
|
}, false);
|
|
// mobile events
|
|
this.content.addEventListener('touchstart', function(evt) {
|
|
evt.preventDefault();
|
|
that.touchStart = true;
|
|
that._handleStageEvent(evt);
|
|
}, false);
|
|
|
|
this.content.addEventListener('touchmove', function(evt) {
|
|
evt.preventDefault();
|
|
that._handleStageEvent(evt);
|
|
}, false);
|
|
|
|
this.content.addEventListener('touchend', function(evt) {
|
|
evt.preventDefault();
|
|
that.touchEnd = true;
|
|
that._handleStageEvent(evt);
|
|
}, false);
|
|
},
|
|
/**
|
|
* set mouse positon for desktop apps
|
|
* @param {Event} evt
|
|
*/
|
|
_setMousePosition: function(evt) {
|
|
var mouseX = evt.offsetX || (evt.clientX - this._getContentPosition().left + window.pageXOffset);
|
|
var mouseY = evt.offsetY || (evt.clientY - this._getContentPosition().top + window.pageYOffset);
|
|
this.mousePos = {
|
|
x: mouseX,
|
|
y: mouseY
|
|
};
|
|
},
|
|
/**
|
|
* set touch position for mobile apps
|
|
* @param {Event} evt
|
|
*/
|
|
_setTouchPosition: function(evt) {
|
|
if(evt.touches !== undefined && evt.touches.length === 1) {// Only deal with
|
|
// one finger
|
|
var touch = evt.touches[0];
|
|
// Get the information for finger #1
|
|
var touchX = touch.clientX - this._getContentPosition().left + window.pageXOffset;
|
|
var touchY = touch.clientY - this._getContentPosition().top + window.pageYOffset;
|
|
|
|
this.touchPos = {
|
|
x: touchX,
|
|
y: touchY
|
|
};
|
|
}
|
|
},
|
|
/**
|
|
* get container position
|
|
*/
|
|
_getContentPosition: function() {
|
|
var obj = this.content;
|
|
var top = 0;
|
|
var left = 0;
|
|
while(obj && obj.tagName !== 'BODY') {
|
|
top += obj.offsetTop - obj.scrollTop;
|
|
left += obj.offsetLeft - obj.scrollLeft;
|
|
obj = obj.offsetParent;
|
|
}
|
|
return {
|
|
top: top,
|
|
left: left
|
|
};
|
|
},
|
|
/**
|
|
* modify path context
|
|
* @param {CanvasContext} context
|
|
*/
|
|
_modifyPathContext: function(context) {
|
|
context.stroke = function() {
|
|
};
|
|
context.fill = function() {
|
|
};
|
|
context.fillRect = function(x, y, width, height) {
|
|
context.rect(x, y, width, height);
|
|
};
|
|
context.strokeRect = function(x, y, width, height) {
|
|
context.rect(x, y, width, height);
|
|
};
|
|
context.drawImage = function() {
|
|
};
|
|
context.fillText = function() {
|
|
};
|
|
context.strokeText = function() {
|
|
};
|
|
},
|
|
/**
|
|
* end drag and drop
|
|
*/
|
|
_endDrag: function(evt) {
|
|
var go = Kinetic.GlobalObject;
|
|
if(go.drag.node) {
|
|
if(go.drag.moving) {
|
|
go.drag.moving = false;
|
|
go.drag.node._handleEvents('ondragend', evt);
|
|
}
|
|
}
|
|
go.drag.node = undefined;
|
|
},
|
|
/**
|
|
* prepare drag and drop
|
|
*/
|
|
_prepareDrag: function() {
|
|
var that = this;
|
|
|
|
this._onContent('mousemove touchmove', function(evt) {
|
|
var go = Kinetic.GlobalObject;
|
|
var node = go.drag.node;
|
|
if(node) {
|
|
var date = new Date();
|
|
var time = date.getTime();
|
|
|
|
if(time - go.drag.lastDrawTime > go.dragTimeInterval) {
|
|
go.drag.lastDrawTime = time;
|
|
|
|
var pos = that.getUserPosition();
|
|
var dc = node.attrs.dragConstraint;
|
|
var db = node.attrs.dragBounds;
|
|
var lastNodePos = {
|
|
x: node.attrs.x,
|
|
y: node.attrs.y
|
|
};
|
|
|
|
// default
|
|
var newNodePos = {
|
|
x: pos.x - go.drag.offset.x,
|
|
y: pos.y - go.drag.offset.y
|
|
};
|
|
|
|
// bounds overrides
|
|
if(db.left !== undefined && newNodePos.x < db.left) {
|
|
newNodePos.x = db.left;
|
|
}
|
|
if(db.right !== undefined && newNodePos.x > db.right) {
|
|
newNodePos.x = db.right;
|
|
}
|
|
if(db.top !== undefined && newNodePos.y < db.top) {
|
|
newNodePos.y = db.top;
|
|
}
|
|
if(db.bottom !== undefined && newNodePos.y > db.bottom) {
|
|
newNodePos.y = db.bottom;
|
|
}
|
|
|
|
node.setAbsolutePosition(newNodePos);
|
|
|
|
// constraint overrides
|
|
if(dc === 'horizontal') {
|
|
node.attrs.y = lastNodePos.y;
|
|
}
|
|
else if(dc === 'vertical') {
|
|
node.attrs.x = lastNodePos.x;
|
|
}
|
|
|
|
go.drag.node.getLayer().draw();
|
|
|
|
if(!go.drag.moving) {
|
|
go.drag.moving = true;
|
|
// execute dragstart events if defined
|
|
go.drag.node._handleEvents('ondragstart', evt);
|
|
}
|
|
|
|
// execute user defined ondragmove if defined
|
|
go.drag.node._handleEvents('ondragmove', evt);
|
|
}
|
|
}
|
|
}, false);
|
|
|
|
this._onContent('mouseup touchend mouseout', function(evt) {
|
|
that._endDrag(evt);
|
|
});
|
|
},
|
|
/**
|
|
* build dom
|
|
*/
|
|
_buildDOM: function() {
|
|
// content
|
|
this.content.style.position = 'relative';
|
|
this.content.style.display = 'inline-block';
|
|
this.content.className = 'kineticjs-content';
|
|
this.container.appendChild(this.content);
|
|
|
|
// default layers
|
|
this.bufferLayer = new Kinetic.Layer({
|
|
name: 'bufferLayer'
|
|
});
|
|
this.pathLayer = new Kinetic.Layer({
|
|
name: 'pathLayer'
|
|
});
|
|
|
|
// set parents
|
|
this.bufferLayer.parent = this;
|
|
this.pathLayer.parent = this;
|
|
|
|
// customize back stage context
|
|
this._modifyPathContext(this.pathLayer.context);
|
|
|
|
// hide canvases
|
|
this.bufferLayer.getCanvas().style.display = 'none';
|
|
this.pathLayer.getCanvas().style.display = 'none';
|
|
|
|
// add buffer layer
|
|
this.bufferLayer.canvas.className = 'kineticjs-buffer-layer';
|
|
this.content.appendChild(this.bufferLayer.canvas);
|
|
|
|
// add path layer
|
|
this.pathLayer.canvas.className = 'kineticjs-path-layer';
|
|
this.content.appendChild(this.pathLayer.canvas);
|
|
|
|
this.setSize(this.attrs.width, this.attrs.height);
|
|
},
|
|
_addId: function(node) {
|
|
if(node.attrs.id !== undefined) {
|
|
this.ids[node.attrs.id] = node;
|
|
}
|
|
},
|
|
_removeId: function(node) {
|
|
if(node.attrs.id !== undefined) {
|
|
this.ids[node.attrs.id] = undefined;
|
|
}
|
|
},
|
|
_addName: function(node) {
|
|
var name = node.attrs.name;
|
|
if(name !== undefined) {
|
|
if(this.names[name] === undefined) {
|
|
this.names[name] = [];
|
|
}
|
|
this.names[name].push(node);
|
|
}
|
|
},
|
|
_removeName: function(node) {
|
|
if(node.attrs.name !== undefined) {
|
|
var nodes = this.names[node.attrs.name];
|
|
if(nodes !== undefined) {
|
|
for(var n = 0; n < nodes.length; n++) {
|
|
var no = nodes[n];
|
|
if(no._id === node._id) {
|
|
nodes.splice(n, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* bind event listener to container DOM element
|
|
* @param {String} typesStr
|
|
* @param {function} handler
|
|
*/
|
|
_onContent: function(typesStr, handler) {
|
|
var types = typesStr.split(' ');
|
|
for(var n = 0; n < types.length; n++) {
|
|
var baseEvent = types[n];
|
|
this.content.addEventListener(baseEvent, handler, false);
|
|
}
|
|
},
|
|
/**
|
|
* set defaults
|
|
*/
|
|
_setDefaults: function() {
|
|
this.clickStart = false;
|
|
this.targetShape = undefined;
|
|
this.targetFound = false;
|
|
this.mouseoverShape = undefined;
|
|
this.mouseoutShape = undefined;
|
|
|
|
// desktop flags
|
|
this.mousePos = undefined;
|
|
this.mouseDown = false;
|
|
this.mouseUp = false;
|
|
|
|
// mobile flags
|
|
this.touchPos = undefined;
|
|
this.touchStart = false;
|
|
this.touchEnd = false;
|
|
|
|
this.ids = {};
|
|
this.names = {};
|
|
this.anim = undefined;
|
|
this.animRunning = false;
|
|
}
|
|
};
|
|
// Extend Container and Node
|
|
Kinetic.GlobalObject.extend(Kinetic.Stage, Kinetic.Container);
|
|
Kinetic.GlobalObject.extend(Kinetic.Stage, Kinetic.Node);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Layer
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Layer constructor. Layers are tied to their own canvas element and are used
|
|
* to contain groups or shapes
|
|
* @constructor
|
|
* @augments Kinetic.Container
|
|
* @augments Kinetic.Node
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Layer = function(config) {
|
|
this.setDefaultAttrs({
|
|
throttle: 80
|
|
});
|
|
|
|
this.nodeType = 'Layer';
|
|
this.lastDrawTime = 0;
|
|
this.beforeDrawFunc = undefined;
|
|
this.afterDrawFunc = undefined;
|
|
|
|
this.canvas = document.createElement('canvas');
|
|
this.context = this.canvas.getContext('2d');
|
|
this.canvas.style.position = 'absolute';
|
|
|
|
// call super constructors
|
|
Kinetic.Container.apply(this, []);
|
|
Kinetic.Node.apply(this, [config]);
|
|
};
|
|
/*
|
|
* Layer methods
|
|
*/
|
|
Kinetic.Layer.prototype = {
|
|
/**
|
|
* draw children nodes. this includes any groups
|
|
* or shapes
|
|
*/
|
|
draw: function() {
|
|
var throttle = this.attrs.throttle;
|
|
var date = new Date();
|
|
var time = date.getTime();
|
|
var timeDiff = time - this.lastDrawTime;
|
|
var tt = 1000 / throttle;
|
|
|
|
if(timeDiff >= tt) {
|
|
this._draw();
|
|
|
|
if(this.drawTimeout !== undefined) {
|
|
clearTimeout(this.drawTimeout);
|
|
this.drawTimeout = undefined;
|
|
}
|
|
}
|
|
/*
|
|
* if we cannot draw the layer due to throttling,
|
|
* try to redraw the layer in the near future
|
|
*/
|
|
else if(this.drawTimeout === undefined) {
|
|
var that = this;
|
|
/*
|
|
* wait 17ms before trying again (60fps)
|
|
*/
|
|
this.drawTimeout = setTimeout(function() {
|
|
that.draw();
|
|
}, 17);
|
|
}
|
|
},
|
|
/**
|
|
* set throttle
|
|
* @param {Number} throttle in ms
|
|
*/
|
|
setThrottle: function(throttle) {
|
|
this.attrs.throttle = throttle;
|
|
},
|
|
/**
|
|
* get throttle
|
|
*/
|
|
getThrottle: function() {
|
|
return this.attrs.throttle;
|
|
},
|
|
/**
|
|
* set before draw function handler
|
|
*/
|
|
beforeDraw: function(func) {
|
|
this.beforeDrawFunc = func;
|
|
},
|
|
/**
|
|
* set after draw function handler
|
|
*/
|
|
afterDraw: function(func) {
|
|
this.afterDrawFunc = func;
|
|
},
|
|
/**
|
|
* clears the canvas context tied to the layer. Clearing
|
|
* a layer does not remove its children. The nodes within
|
|
* the layer will be redrawn whenever the .draw() method
|
|
* is used again.
|
|
*/
|
|
clear: function() {
|
|
var context = this.getContext();
|
|
var canvas = this.getCanvas();
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
},
|
|
/**
|
|
* get layer canvas
|
|
*/
|
|
getCanvas: function() {
|
|
return this.canvas;
|
|
},
|
|
/**
|
|
* get layer context
|
|
*/
|
|
getContext: function() {
|
|
return this.context;
|
|
},
|
|
/**
|
|
* add a node to the layer. New nodes are always
|
|
* placed at the top.
|
|
* @param {Node} node
|
|
*/
|
|
add: function(child) {
|
|
this._add(child);
|
|
},
|
|
/**
|
|
* remove a child from the layer
|
|
* @param {Node} child
|
|
*/
|
|
remove: function(child) {
|
|
this._remove(child);
|
|
},
|
|
/**
|
|
* private draw children
|
|
*/
|
|
_draw: function() {
|
|
var date = new Date();
|
|
var time = date.getTime();
|
|
this.lastDrawTime = time;
|
|
|
|
// before draw handler
|
|
if(this.beforeDrawFunc !== undefined) {
|
|
this.beforeDrawFunc();
|
|
}
|
|
|
|
this.clear();
|
|
if(this.attrs.visible) {
|
|
this._drawChildren();
|
|
}
|
|
|
|
// after draw handler
|
|
if(this.afterDrawFunc !== undefined) {
|
|
this.afterDrawFunc();
|
|
}
|
|
}
|
|
};
|
|
// Extend Container and Node
|
|
Kinetic.GlobalObject.extend(Kinetic.Layer, Kinetic.Container);
|
|
Kinetic.GlobalObject.extend(Kinetic.Layer, Kinetic.Node);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Group
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Group constructor. Groups are used to contain shapes or other groups.
|
|
* @constructor
|
|
* @augments Kinetic.Container
|
|
* @augments Kinetic.Node
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Group = function(config) {
|
|
this.nodeType = 'Group';;
|
|
|
|
// call super constructors
|
|
Kinetic.Container.apply(this, []);
|
|
Kinetic.Node.apply(this, [config]);
|
|
};
|
|
/*
|
|
* Group methods
|
|
*/
|
|
Kinetic.Group.prototype = {
|
|
/**
|
|
* add node to group
|
|
* @param {Node} child
|
|
*/
|
|
add: function(child) {
|
|
this._add(child);
|
|
},
|
|
/**
|
|
* remove a child node from the group
|
|
* @param {Node} child
|
|
*/
|
|
remove: function(child) {
|
|
this._remove(child);
|
|
},
|
|
/**
|
|
* draw children
|
|
*/
|
|
_draw: function() {
|
|
if(this.attrs.visible) {
|
|
this._drawChildren();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Extend Container and Node
|
|
Kinetic.GlobalObject.extend(Kinetic.Group, Kinetic.Container);
|
|
Kinetic.GlobalObject.extend(Kinetic.Group, Kinetic.Node);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Shape
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Shape constructor. Shapes are used to objectify drawing bits of a KineticJS
|
|
* application
|
|
* @constructor
|
|
* @augments Kinetic.Node
|
|
* @param {Object} config
|
|
* @config {String|CanvasGradient|CanvasPattern} [fill] fill
|
|
* @config {String} [stroke] stroke color
|
|
* @config {Number} [strokeWidth] stroke width
|
|
* @config {String} [lineJoin] line join. Can be "miter", "round", or "bevel". The default
|
|
* is "miter"
|
|
* @config {String} [detectionType] shape detection type. Can be "path" or "pixel".
|
|
* The default is "path" because it performs better
|
|
*/
|
|
Kinetic.Shape = function(config) {
|
|
this.setDefaultAttrs({
|
|
fill: undefined,
|
|
stroke: undefined,
|
|
strokeWidth: undefined,
|
|
lineJoin: undefined,
|
|
detectionType: 'path'
|
|
});
|
|
|
|
this.data = [];
|
|
this.nodeType = 'Shape';
|
|
|
|
// call super constructor
|
|
Kinetic.Node.apply(this, [config]);
|
|
};
|
|
/*
|
|
* Shape methods
|
|
*/
|
|
Kinetic.Shape.prototype = {
|
|
/**
|
|
* get layer context where the shape is being drawn. When
|
|
* the shape is being rendered, .getContext() returns the context of the
|
|
* user created layer that contains the shape. When the event detection
|
|
* engine is determining whether or not an event has occured on that shape,
|
|
* .getContext() returns the context of the invisible path layer.
|
|
*/
|
|
getContext: function() {
|
|
return this.tempLayer.getContext();
|
|
},
|
|
/**
|
|
* get shape temp layer canvas
|
|
*/
|
|
getCanvas: function() {
|
|
return this.tempLayer.getCanvas();
|
|
},
|
|
/**
|
|
* helper method to stroke shape
|
|
*/
|
|
stroke: function() {
|
|
var context = this.getContext();
|
|
|
|
if(!!this.attrs.stroke || !!this.attrs.strokeWidth) {
|
|
var stroke = !!this.attrs.stroke ? this.attrs.stroke : 'black';
|
|
var strokeWidth = !!this.attrs.strokeWidth ? this.attrs.strokeWidth : 2;
|
|
|
|
context.lineWidth = strokeWidth;
|
|
context.strokeStyle = stroke;
|
|
context.stroke();
|
|
}
|
|
},
|
|
/**
|
|
* helper method to fill and stroke a shape
|
|
* based on its fill, stroke, and strokeWidth, properties
|
|
*/
|
|
fillStroke: function() {
|
|
var context = this.getContext();
|
|
var fill = this.attrs.fill;
|
|
/*
|
|
* expect that fill, stroke, and strokeWidth could be
|
|
* undfined, '', null, or 0. Use !!
|
|
*/
|
|
if(!!fill) {
|
|
// color fill
|
|
if( typeof fill == 'string') {
|
|
f = this.attrs.fill;
|
|
}
|
|
else {
|
|
var s = fill.start;
|
|
var e = fill.end;
|
|
|
|
// linear gradient
|
|
if(s.x !== undefined && s.y !== undefined && e.x !== undefined && e.y !== undefined) {
|
|
var context = this.getContext();
|
|
var grd = context.createLinearGradient(s.x, s.y, e.x, e.y);
|
|
grd.addColorStop(0, s.color);
|
|
grd.addColorStop(1, e.color);
|
|
f = grd;
|
|
}
|
|
// radial gradient
|
|
else if(s.radius !== undefined && e.radius !== undefined) {
|
|
var context = this.getContext();
|
|
var grd = context.createRadialGradient(s.x, s.y, s.radius, s.x, s.y, e.radius);
|
|
grd.addColorStop(0, s.color);
|
|
grd.addColorStop(1, e.color);
|
|
f = grd;
|
|
}
|
|
else {
|
|
f = 'black';
|
|
}
|
|
}
|
|
|
|
context.fillStyle = f;
|
|
context.fill();
|
|
}
|
|
|
|
this.stroke();
|
|
},
|
|
/**
|
|
* helper method to set the line join of a shape
|
|
* based on the lineJoin property
|
|
*/
|
|
applyLineJoin: function() {
|
|
var context = this.getContext();
|
|
if(this.attrs.lineJoin !== undefined) {
|
|
context.lineJoin = this.attrs.lineJoin;
|
|
}
|
|
},
|
|
/**
|
|
* set fill which can be a color, gradient object,
|
|
* or pattern object
|
|
* @param {String|CanvasGradient|CanvasPattern} fill
|
|
*/
|
|
setFill: function(fill) {
|
|
this.attrs.fill = fill;
|
|
},
|
|
/**
|
|
* get fill
|
|
*/
|
|
getFill: function() {
|
|
return this.attrs.fill;
|
|
},
|
|
/**
|
|
* set stroke color
|
|
* @param {String} stroke
|
|
*/
|
|
setStroke: function(stroke) {
|
|
this.attrs.stroke = stroke;
|
|
},
|
|
/**
|
|
* get stroke color
|
|
*/
|
|
getStroke: function() {
|
|
return this.attrs.stroke;
|
|
},
|
|
/**
|
|
* set line join
|
|
* @param {String} lineJoin. Can be "miter", "round", or "bevel". The
|
|
* default is "miter"
|
|
*/
|
|
setLineJoin: function(lineJoin) {
|
|
this.attrs.lineJoin = lineJoin;
|
|
},
|
|
/**
|
|
* get line join
|
|
*/
|
|
getLineJoin: function() {
|
|
return this.attrs.lineJoin;
|
|
},
|
|
/**
|
|
* set stroke width
|
|
* @param {Number} strokeWidth
|
|
*/
|
|
setStrokeWidth: function(strokeWidth) {
|
|
this.attrs.strokeWidth = strokeWidth;
|
|
},
|
|
/**
|
|
* get stroke width
|
|
*/
|
|
getStrokeWidth: function() {
|
|
return this.attrs.strokeWidth;
|
|
},
|
|
/**
|
|
* set draw function
|
|
* @param {Function} func drawing function
|
|
*/
|
|
setDrawFunc: function(func) {
|
|
this.drawFunc = func;
|
|
},
|
|
/**
|
|
* save shape data when using pixel detection.
|
|
*/
|
|
saveData: function() {
|
|
var stage = this.getStage();
|
|
var w = stage.attrs.width;
|
|
var h = stage.attrs.height;
|
|
|
|
var bufferLayer = stage.bufferLayer;
|
|
var bufferLayerContext = bufferLayer.getContext();
|
|
|
|
bufferLayer.clear();
|
|
this._draw(bufferLayer);
|
|
|
|
var imageData = bufferLayerContext.getImageData(0, 0, w, h);
|
|
this.data = imageData.data;
|
|
},
|
|
/**
|
|
* clear shape data
|
|
*/
|
|
clearData: function() {
|
|
this.data = [];
|
|
},
|
|
/**
|
|
* determines if point is in the shape
|
|
*/
|
|
intersects: function() {
|
|
var pos = Kinetic.GlobalObject._getPoint(arguments);
|
|
var stage = this.getStage();
|
|
|
|
if(this.attrs.detectionType === 'path') {
|
|
var pathLayer = stage.pathLayer;
|
|
var pathLayerContext = pathLayer.getContext();
|
|
|
|
this._draw(pathLayer);
|
|
|
|
return pathLayerContext.isPointInPath(pos.x, pos.y);
|
|
}
|
|
else {
|
|
var w = stage.attrs.width;
|
|
var alpha = this.data[((w * pos.y) + pos.x) * 4 + 3];
|
|
return (alpha !== undefined && alpha !== 0);
|
|
}
|
|
},
|
|
/**
|
|
* draw shape
|
|
* @param {Layer} layer Layer that the shape will be drawn on
|
|
*/
|
|
_draw: function(layer) {
|
|
if(layer !== undefined && this.drawFunc !== undefined) {
|
|
var stage = layer.getStage();
|
|
var context = layer.getContext();
|
|
var family = [];
|
|
var parent = this.parent;
|
|
|
|
family.unshift(this);
|
|
while(parent) {
|
|
family.unshift(parent);
|
|
parent = parent.parent;
|
|
}
|
|
|
|
context.save();
|
|
for(var n = 0; n < family.length; n++) {
|
|
var node = family[n];
|
|
var t = node.getTransform();
|
|
|
|
// center offset
|
|
if(node.attrs.centerOffset.x !== 0 || node.attrs.centerOffset.y !== 0) {
|
|
t.translate(-1 * node.attrs.centerOffset.x, -1 * node.attrs.centerOffset.y);
|
|
}
|
|
|
|
var m = t.getMatrix();
|
|
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
|
|
}
|
|
|
|
if(this.getAbsoluteAlpha() !== 1) {
|
|
context.globalAlpha = this.getAbsoluteAlpha();
|
|
}
|
|
|
|
this.tempLayer = layer;
|
|
this.drawFunc.call(this);
|
|
context.restore();
|
|
}
|
|
}
|
|
};
|
|
// extend Node
|
|
Kinetic.GlobalObject.extend(Kinetic.Shape, Kinetic.Node);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Rect
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Rect constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Rect = function(config) {
|
|
this.setDefaultAttrs({
|
|
width: 0,
|
|
height: 0,
|
|
cornerRadius: 0
|
|
});
|
|
|
|
this.shapeType = "Rect";
|
|
|
|
config.drawFunc = function() {
|
|
var context = this.getContext();
|
|
context.beginPath();
|
|
this.applyLineJoin();
|
|
if(this.attrs.cornerRadius === 0) {
|
|
// simple rect - don't bother doing all that complicated maths stuff.
|
|
context.rect(0, 0, this.attrs.width, this.attrs.height);
|
|
}
|
|
else {
|
|
// arcTo would be nicer, but browser support is patchy (Opera)
|
|
context.moveTo(this.attrs.cornerRadius, 0);
|
|
context.lineTo(this.attrs.width - this.attrs.cornerRadius, 0);
|
|
context.arc(this.attrs.width - this.attrs.cornerRadius, this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI * 3 / 2, 0, false);
|
|
context.lineTo(this.attrs.width, this.attrs.height - this.attrs.cornerRadius);
|
|
context.arc(this.attrs.width - this.attrs.cornerRadius, this.attrs.height - this.attrs.cornerRadius, this.attrs.cornerRadius, 0, Math.PI / 2, false);
|
|
context.lineTo(this.attrs.cornerRadius, this.attrs.height);
|
|
context.arc(this.attrs.cornerRadius, this.attrs.height - this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI / 2, Math.PI, false);
|
|
context.lineTo(0, this.attrs.cornerRadius);
|
|
context.arc(this.attrs.cornerRadius, this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI, Math.PI * 3 / 2, false);
|
|
}
|
|
context.closePath();
|
|
this.fillStroke();
|
|
};
|
|
// call super constructor
|
|
Kinetic.Shape.apply(this, [config]);
|
|
};
|
|
/*
|
|
* Rect methods
|
|
*/
|
|
Kinetic.Rect.prototype = {
|
|
/**
|
|
* set width
|
|
* @param {Number} width
|
|
*/
|
|
setWidth: function(width) {
|
|
this.attrs.width = width;
|
|
},
|
|
/**
|
|
* get width
|
|
*/
|
|
getWidth: function() {
|
|
return this.attrs.width;
|
|
},
|
|
/**
|
|
* set height
|
|
* @param {Number} height
|
|
*/
|
|
setHeight: function(height) {
|
|
this.attrs.height = height;
|
|
},
|
|
/**
|
|
* get height
|
|
*/
|
|
getHeight: function() {
|
|
return this.attrs.height;
|
|
},
|
|
/**
|
|
* set width and height
|
|
* @param {Number} width
|
|
* @param {Number} height
|
|
*/
|
|
setSize: function(width, height) {
|
|
this.attrs.width = width;
|
|
this.attrs.height = height;
|
|
},
|
|
/**
|
|
* return rect size
|
|
*/
|
|
getSize: function() {
|
|
return {
|
|
width: this.attrs.width,
|
|
height: this.attrs.height
|
|
};
|
|
},
|
|
/**
|
|
* set corner radius
|
|
* @param {Number} radius
|
|
*/
|
|
setCornerRadius: function(radius) {
|
|
this.attrs.cornerRadius = radius;
|
|
},
|
|
/**
|
|
* get corner radius
|
|
*/
|
|
getCornerRadius: function() {
|
|
return this.attrs.cornerRadius;
|
|
},
|
|
};
|
|
|
|
// extend Shape
|
|
Kinetic.GlobalObject.extend(Kinetic.Rect, Kinetic.Shape);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Circle
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Circle constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Circle = function(config) {
|
|
this.setDefaultAttrs({
|
|
radius: 0
|
|
});
|
|
|
|
this.shapeType = "Circle";
|
|
|
|
config.drawFunc = function() {
|
|
var canvas = this.getCanvas();
|
|
var context = this.getContext();
|
|
context.beginPath();
|
|
this.applyLineJoin();
|
|
context.arc(0, 0, this.attrs.radius, 0, Math.PI * 2, true);
|
|
context.closePath();
|
|
this.fillStroke();
|
|
};
|
|
// call super constructor
|
|
Kinetic.Shape.apply(this, [config]);
|
|
};
|
|
/*
|
|
* Circle methods
|
|
*/
|
|
Kinetic.Circle.prototype = {
|
|
/**
|
|
* set radius
|
|
* @param {Number} radius
|
|
*/
|
|
setRadius: function(radius) {
|
|
this.attrs.radius = radius;
|
|
},
|
|
/**
|
|
* get radius
|
|
*/
|
|
getRadius: function() {
|
|
return this.attrs.radius;
|
|
}
|
|
};
|
|
|
|
// extend Shape
|
|
Kinetic.GlobalObject.extend(Kinetic.Circle, Kinetic.Shape);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Image
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Image constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Image = function(config) {
|
|
this.setDefaultAttrs({
|
|
crop: {
|
|
x: 0,
|
|
y: 0,
|
|
width: undefined,
|
|
height: undefined
|
|
}
|
|
});
|
|
|
|
this.shapeType = "Image";
|
|
config.drawFunc = function() {
|
|
if(this.image !== undefined) {
|
|
var width = this.attrs.width !== undefined ? this.attrs.width : this.image.width;
|
|
var height = this.attrs.height !== undefined ? this.attrs.height : this.image.height;
|
|
var cropX = this.attrs.crop.x;
|
|
var cropY = this.attrs.crop.y;
|
|
var cropWidth = this.attrs.crop.width;
|
|
var cropHeight = this.attrs.crop.height;
|
|
var canvas = this.getCanvas();
|
|
var context = this.getContext();
|
|
|
|
context.beginPath();
|
|
this.applyLineJoin();
|
|
context.rect(0, 0, width, height);
|
|
context.closePath();
|
|
this.fillStroke();
|
|
|
|
// if cropping
|
|
if(cropWidth !== undefined && cropHeight !== undefined) {
|
|
context.drawImage(this.image, cropX, cropY, cropWidth, cropHeight, 0, 0, width, height);
|
|
}
|
|
// no cropping
|
|
else {
|
|
context.drawImage(this.image, 0, 0, width, height);
|
|
}
|
|
}
|
|
};
|
|
// call super constructor
|
|
Kinetic.Shape.apply(this, [config]);
|
|
};
|
|
/*
|
|
* Image methods
|
|
*/
|
|
Kinetic.Image.prototype = {
|
|
/**
|
|
* set image
|
|
* @param {ImageObject} image
|
|
*/
|
|
setImage: function(image) {
|
|
this.image = image;
|
|
},
|
|
/**
|
|
* get image
|
|
*/
|
|
getImage: function() {
|
|
return this.image;
|
|
},
|
|
/**
|
|
* set width
|
|
* @param {Number} width
|
|
*/
|
|
setWidth: function(width) {
|
|
this.attrs.width = width;
|
|
},
|
|
/**
|
|
* get width
|
|
*/
|
|
getWidth: function() {
|
|
return this.attrs.width;
|
|
},
|
|
/**
|
|
* set height
|
|
* @param {Number} height
|
|
*/
|
|
setHeight: function(height) {
|
|
this.attrs.height = height;
|
|
},
|
|
/**
|
|
* get height
|
|
*/
|
|
getHeight: function() {
|
|
return this.attrs.height;
|
|
},
|
|
/**
|
|
* set width and height
|
|
* @param {Number} width
|
|
* @param {Number} height
|
|
*/
|
|
setSize: function(width, height) {
|
|
this.attrs.width = width;
|
|
this.attrs.height = height;
|
|
},
|
|
/**
|
|
* return image size
|
|
*/
|
|
getSize: function() {
|
|
return {
|
|
width: this.attrs.width,
|
|
height: this.attrs.height
|
|
};
|
|
},
|
|
/**
|
|
* return cropping
|
|
*/
|
|
getCrop: function() {
|
|
return this.attrs.crop;
|
|
},
|
|
/**
|
|
* set cropping
|
|
* @param {Object} crop
|
|
* @config {Number} [x] crop x
|
|
* @config {Number} [y] crop y
|
|
* @config {Number} [width] crop width
|
|
* @config {Number} [height] crop height
|
|
*/
|
|
setCrop: function(config) {
|
|
var c = {};
|
|
c.crop = config;
|
|
this.setAttrs(c);
|
|
}
|
|
};
|
|
// extend Shape
|
|
Kinetic.GlobalObject.extend(Kinetic.Image, Kinetic.Shape);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Sprite
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Sprite constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Sprite = function(config) {
|
|
this.setDefaultAttrs({
|
|
index: 0,
|
|
frameRate: 17
|
|
});
|
|
|
|
config.drawFunc = function() {
|
|
if(this.image !== undefined) {
|
|
var context = this.getContext();
|
|
var anim = this.attrs.animation;
|
|
var index = this.attrs.index;
|
|
var f = this.attrs.animations[anim][index];
|
|
|
|
context.beginPath();
|
|
context.rect(0, 0, f.width, f.height);
|
|
context.closePath();
|
|
context.drawImage(this.image, f.x, f.y, f.width, f.height, 0, 0, f.width, f.height);
|
|
}
|
|
};
|
|
// call super constructor
|
|
Kinetic.Shape.apply(this, [config]);
|
|
};
|
|
/*
|
|
* Sprite methods
|
|
*/
|
|
Kinetic.Sprite.prototype = {
|
|
/**
|
|
* start sprite animation
|
|
*/
|
|
start: function() {
|
|
var that = this;
|
|
var layer = this.getLayer();
|
|
this.interval = setInterval(function() {
|
|
that._updateIndex();
|
|
layer.draw();
|
|
if(that.afterFrameFunc && that.attrs.index === that.afterFrameIndex) {
|
|
that.afterFrameFunc();
|
|
}
|
|
}, 1000 / this.attrs.frameRate)
|
|
},
|
|
/**
|
|
* stop sprite animation
|
|
*/
|
|
stop: function() {
|
|
clearInterval(this.interval);
|
|
},
|
|
/**
|
|
* set after frame event handler
|
|
* @param {Integer} index frame index
|
|
* @param {Function} func function to be executed after frame has been drawn
|
|
*/
|
|
afterFrame: function(index, func) {
|
|
this.afterFrameIndex = index;
|
|
this.afterFrameFunc = func;
|
|
},
|
|
/**
|
|
* set animation key
|
|
* @param {String} anim animation key
|
|
*/
|
|
setAnimation: function(anim) {
|
|
this.attrs.animation = anim;
|
|
},
|
|
/**
|
|
* set animation frame index
|
|
* @param {Integer} index frame index
|
|
*/
|
|
setIndex: function(index) {
|
|
this.attrs.index = index;
|
|
},
|
|
_updateIndex: function() {
|
|
var i = this.attrs.index;
|
|
var a = this.attrs.animation;
|
|
if(i < this.attrs.animations[a].length - 1) {
|
|
this.attrs.index++;
|
|
}
|
|
else {
|
|
this.attrs.index = 0;
|
|
}
|
|
}
|
|
};
|
|
// extend Shape
|
|
Kinetic.GlobalObject.extend(Kinetic.Sprite, Kinetic.Shape);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Polygon
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Polygon constructor. Polygons are defined by an array of points
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Polygon = function(config) {
|
|
this.setDefaultAttrs({
|
|
points: {}
|
|
});
|
|
|
|
this.shapeType = "Polygon";
|
|
config.drawFunc = function() {
|
|
var context = this.getContext();
|
|
context.beginPath();
|
|
this.applyLineJoin();
|
|
context.moveTo(this.attrs.points[0].x, this.attrs.points[0].y);
|
|
for(var n = 1; n < this.attrs.points.length; n++) {
|
|
context.lineTo(this.attrs.points[n].x, this.attrs.points[n].y);
|
|
}
|
|
context.closePath();
|
|
this.fillStroke();
|
|
};
|
|
// call super constructor
|
|
Kinetic.Shape.apply(this, [config]);
|
|
};
|
|
/*
|
|
* Polygon methods
|
|
*/
|
|
Kinetic.Polygon.prototype = {
|
|
/**
|
|
* set points array
|
|
* @param {Array} points
|
|
*/
|
|
setPoints: function(points) {
|
|
this.attrs.points = points;
|
|
},
|
|
/**
|
|
* get points array
|
|
*/
|
|
getPoints: function() {
|
|
return this.attrs.points;
|
|
}
|
|
};
|
|
|
|
// extend Shape
|
|
Kinetic.GlobalObject.extend(Kinetic.Polygon, Kinetic.Shape);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// RegularPolygon
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* RegularPolygon constructor. Examples include triangles, squares, pentagons, hexagons, etc.
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.RegularPolygon = function(config) {
|
|
this.setDefaultAttrs({
|
|
radius: 0,
|
|
sides: 0
|
|
});
|
|
|
|
this.shapeType = "RegularPolygon";
|
|
config.drawFunc = function() {
|
|
var context = this.getContext();
|
|
context.beginPath();
|
|
this.applyLineJoin();
|
|
context.moveTo(0, 0 - this.attrs.radius);
|
|
|
|
for(var n = 1; n < this.attrs.sides; n++) {
|
|
var x = this.attrs.radius * Math.sin(n * 2 * Math.PI / this.attrs.sides);
|
|
var y = -1 * this.attrs.radius * Math.cos(n * 2 * Math.PI / this.attrs.sides);
|
|
context.lineTo(x, y);
|
|
}
|
|
context.closePath();
|
|
this.fillStroke();
|
|
};
|
|
// call super constructor
|
|
Kinetic.Shape.apply(this, [config]);
|
|
};
|
|
/*
|
|
* RegularPolygon methods
|
|
*/
|
|
Kinetic.RegularPolygon.prototype = {
|
|
/**
|
|
* set radius
|
|
* @param {Number} radius
|
|
*/
|
|
setRadius: function(radius) {
|
|
this.attrs.radius = radius;
|
|
},
|
|
/**
|
|
* get radius
|
|
*/
|
|
getRadius: function() {
|
|
return this.attrs.radius;
|
|
},
|
|
/**
|
|
* set number of sides
|
|
* @param {int} sides
|
|
*/
|
|
setSides: function(sides) {
|
|
this.attrs.sides = sides;
|
|
},
|
|
/**
|
|
* get number of sides
|
|
*/
|
|
getSides: function() {
|
|
return this.attrs.sides;
|
|
}
|
|
};
|
|
|
|
// extend Shape
|
|
Kinetic.GlobalObject.extend(Kinetic.RegularPolygon, Kinetic.Shape);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Star
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Star constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Star = function(config) {
|
|
this.setDefaultAttrs({
|
|
points: [],
|
|
innerRadius: 0,
|
|
outerRadius: 0
|
|
});
|
|
|
|
this.shapeType = "Star";
|
|
config.drawFunc = function() {
|
|
var context = this.getContext();
|
|
context.beginPath();
|
|
this.applyLineJoin();
|
|
context.moveTo(0, 0 - this.attrs.outerRadius);
|
|
|
|
for(var n = 1; n < this.attrs.points * 2; n++) {
|
|
var radius = n % 2 === 0 ? this.attrs.outerRadius : this.attrs.innerRadius;
|
|
var x = radius * Math.sin(n * Math.PI / this.attrs.points);
|
|
var y = -1 * radius * Math.cos(n * Math.PI / this.attrs.points);
|
|
context.lineTo(x, y);
|
|
}
|
|
context.closePath();
|
|
this.fillStroke();
|
|
};
|
|
// call super constructor
|
|
Kinetic.Shape.apply(this, [config]);
|
|
};
|
|
/*
|
|
* Star methods
|
|
*/
|
|
Kinetic.Star.prototype = {
|
|
/**
|
|
* set points array
|
|
* @param {Array} points
|
|
*/
|
|
setPoints: function(points) {
|
|
this.attrs.points = points;
|
|
},
|
|
/**
|
|
* get points array
|
|
*/
|
|
getPoints: function() {
|
|
return this.attrs.points;
|
|
},
|
|
/**
|
|
* set outer radius
|
|
* @param {Number} radius
|
|
*/
|
|
setOuterRadius: function(radius) {
|
|
this.attrs.outerRadius = radius;
|
|
},
|
|
/**
|
|
* get outer radius
|
|
*/
|
|
getOuterRadius: function() {
|
|
return this.attrs.outerRadius;
|
|
},
|
|
/**
|
|
* set inner radius
|
|
* @param {Number} radius
|
|
*/
|
|
setInnerRadius: function(radius) {
|
|
this.attrs.innerRadius = radius;
|
|
},
|
|
/**
|
|
* get inner radius
|
|
*/
|
|
getInnerRadius: function() {
|
|
return this.attrs.innerRadius;
|
|
}
|
|
};
|
|
// extend Shape
|
|
Kinetic.GlobalObject.extend(Kinetic.Star, Kinetic.Shape);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Text
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Text constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Text = function(config) {
|
|
this.setDefaultAttrs({
|
|
fontFamily: 'Calibri',
|
|
text: '',
|
|
fontSize: 12,
|
|
fill: undefined,
|
|
textStroke: undefined,
|
|
textStrokeWidth: undefined,
|
|
align: 'left',
|
|
verticalAlign: 'top',
|
|
padding: 0,
|
|
fontStyle: 'normal'
|
|
});
|
|
|
|
this.shapeType = "Text";
|
|
|
|
config.drawFunc = function() {
|
|
var context = this.getContext();
|
|
context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily;
|
|
context.textBaseline = 'middle';
|
|
var textHeight = this.getTextHeight();
|
|
var textWidth = this.getTextWidth();
|
|
var p = this.attrs.padding;
|
|
var x = 0;
|
|
var y = 0;
|
|
|
|
switch (this.attrs.align) {
|
|
case 'center':
|
|
x = textWidth / -2 - p;
|
|
break;
|
|
case 'right':
|
|
x = -1 * textWidth - p;
|
|
break;
|
|
}
|
|
|
|
switch (this.attrs.verticalAlign) {
|
|
case 'middle':
|
|
y = textHeight / -2 - p;
|
|
break;
|
|
case 'bottom':
|
|
y = -1 * textHeight - p;
|
|
break;
|
|
}
|
|
|
|
// draw path
|
|
context.save();
|
|
context.beginPath();
|
|
this.applyLineJoin();
|
|
context.rect(x, y, textWidth + p * 2, textHeight + p * 2);
|
|
context.closePath();
|
|
this.fillStroke();
|
|
context.restore();
|
|
|
|
var tx = p + x;
|
|
var ty = textHeight / 2 + p + y;
|
|
|
|
// draw text
|
|
if(this.attrs.textFill !== undefined) {
|
|
context.fillStyle = this.attrs.textFill;
|
|
context.fillText(this.attrs.text, tx, ty);
|
|
}
|
|
if(this.attrs.textStroke !== undefined || this.attrs.textStrokeWidth !== undefined) {
|
|
// defaults
|
|
if(this.attrs.textStroke === undefined) {
|
|
this.attrs.textStroke = 'black';
|
|
}
|
|
else if(this.attrs.textStrokeWidth === undefined) {
|
|
this.attrs.textStrokeWidth = 2;
|
|
}
|
|
context.lineWidth = this.attrs.textStrokeWidth;
|
|
context.strokeStyle = this.attrs.textStroke;
|
|
context.strokeText(this.attrs.text, tx, ty);
|
|
}
|
|
};
|
|
// call super constructor
|
|
Kinetic.Shape.apply(this, [config]);
|
|
};
|
|
/*
|
|
* Text methods
|
|
*/
|
|
Kinetic.Text.prototype = {
|
|
/**
|
|
* set font family
|
|
* @param {String} fontFamily
|
|
*/
|
|
setFontFamily: function(fontFamily) {
|
|
this.attrs.fontFamily = fontFamily;
|
|
},
|
|
/**
|
|
* get font family
|
|
*/
|
|
getFontFamily: function() {
|
|
return this.attrs.fontFamily;
|
|
},
|
|
/**
|
|
* set font size
|
|
* @param {int} fontSize
|
|
*/
|
|
setFontSize: function(fontSize) {
|
|
this.attrs.fontSize = fontSize;
|
|
},
|
|
/**
|
|
* get font size
|
|
*/
|
|
getFontSize: function() {
|
|
return this.attrs.fontSize;
|
|
},
|
|
/**
|
|
* set font style. Can be "normal", "italic", or "bold". "normal" is the default.
|
|
* @param {String} fontStyle
|
|
*/
|
|
setFontStyle: function(fontStyle) {
|
|
this.attrs.fontStyle = fontStyle;
|
|
},
|
|
/**
|
|
* get font style
|
|
*/
|
|
getFontStyle: function() {
|
|
return this.attrs.fontStyle;
|
|
},
|
|
/**
|
|
* set text fill color
|
|
* @param {String} textFill
|
|
*/
|
|
setTextFill: function(textFill) {
|
|
this.attrs.textFill = textFill;
|
|
},
|
|
/**
|
|
* get text fill color
|
|
*/
|
|
getTextFill: function() {
|
|
return this.attrs.textFill;
|
|
},
|
|
/**
|
|
* set text stroke color
|
|
* @param {String} textStroke
|
|
*/
|
|
setTextStroke: function(textStroke) {
|
|
this.attrs.textStroke = textStroke;
|
|
},
|
|
/**
|
|
* get text stroke color
|
|
*/
|
|
getTextStroke: function() {
|
|
return this.attrs.textStroke;
|
|
},
|
|
/**
|
|
* set text stroke width
|
|
* @param {int} textStrokeWidth
|
|
*/
|
|
setTextStrokeWidth: function(textStrokeWidth) {
|
|
this.attrs.textStrokeWidth = textStrokeWidth;
|
|
},
|
|
/**
|
|
* get text stroke width
|
|
*/
|
|
getTextStrokeWidth: function() {
|
|
return this.attrs.textStrokeWidth;
|
|
},
|
|
/**
|
|
* set padding
|
|
* @param {int} padding
|
|
*/
|
|
setPadding: function(padding) {
|
|
this.attrs.padding = padding;
|
|
},
|
|
/**
|
|
* get padding
|
|
*/
|
|
getPadding: function() {
|
|
return this.attrs.padding;
|
|
},
|
|
/**
|
|
* set horizontal align of text
|
|
* @param {String} align align can be 'left', 'center', or 'right'
|
|
*/
|
|
setAlign: function(align) {
|
|
this.attrs.align = align;
|
|
},
|
|
/**
|
|
* get horizontal align
|
|
*/
|
|
getAlign: function() {
|
|
return this.attrs.align;
|
|
},
|
|
/**
|
|
* set vertical align of text
|
|
* @param {String} verticalAlign verticalAlign can be "top", "middle", or "bottom"
|
|
*/
|
|
setVerticalAlign: function(verticalAlign) {
|
|
this.attrs.verticalAlign = verticalAlign;
|
|
},
|
|
/**
|
|
* get vertical align
|
|
*/
|
|
getVerticalAlign: function() {
|
|
return this.attrs.verticalAlign;
|
|
},
|
|
/**
|
|
* set text
|
|
* @param {String} text
|
|
*/
|
|
setText: function(text) {
|
|
this.attrs.text = text;
|
|
},
|
|
/**
|
|
* get text
|
|
*/
|
|
getText: function() {
|
|
return this.attrs.text;
|
|
},
|
|
/**
|
|
* get text width in pixels
|
|
*/
|
|
getTextWidth: function() {
|
|
return this.getTextSize().width;
|
|
},
|
|
/**
|
|
* get text height in pixels
|
|
*/
|
|
getTextHeight: function() {
|
|
return this.getTextSize().height;
|
|
},
|
|
/**
|
|
* get text size in pixels
|
|
*/
|
|
getTextSize: function() {
|
|
var context = this.getContext();
|
|
context.save();
|
|
context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily;
|
|
var metrics = context.measureText(this.attrs.text);
|
|
context.restore();
|
|
return {
|
|
width: metrics.width,
|
|
height: parseInt(this.attrs.fontSize, 10)
|
|
};
|
|
}
|
|
};
|
|
// extend Shape
|
|
Kinetic.GlobalObject.extend(Kinetic.Text, Kinetic.Shape);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Line
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Line constructor. Lines are defined by an array of points
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Line = function(config) {
|
|
this.setDefaultAttrs({
|
|
points: {},
|
|
lineCap: 'butt',
|
|
dashArray: []
|
|
});
|
|
|
|
this.shapeType = "Line";
|
|
config.drawFunc = function() {
|
|
var context = this.getContext();
|
|
var lastPos = {};
|
|
context.beginPath();
|
|
this.applyLineJoin();
|
|
|
|
context.moveTo(this.attrs.points[0].x, this.attrs.points[0].y);
|
|
|
|
for(var n = 1; n < this.attrs.points.length; n++) {
|
|
var x = this.attrs.points[n].x;
|
|
var y = this.attrs.points[n].y;
|
|
if(this.attrs.dashArray.length > 0) {
|
|
// draw dashed line
|
|
var lastX = this.attrs.points[n - 1].x;
|
|
var lastY = this.attrs.points[n - 1].y;
|
|
this._dashedLine(lastX, lastY, x, y, this.attrs.dashArray);
|
|
}
|
|
else {
|
|
// draw normal line
|
|
context.lineTo(x, y);
|
|
}
|
|
}
|
|
|
|
if(!!this.attrs.lineCap) {
|
|
context.lineCap = this.attrs.lineCap;
|
|
}
|
|
this.stroke();
|
|
};
|
|
// call super constructor
|
|
Kinetic.Shape.apply(this, [config]);
|
|
};
|
|
/*
|
|
* Line methods
|
|
*/
|
|
Kinetic.Line.prototype = {
|
|
/**
|
|
* set points array
|
|
* @param {Array} points
|
|
*/
|
|
setPoints: function(points) {
|
|
this.attrs.points = points;
|
|
},
|
|
/**
|
|
* get points array
|
|
*/
|
|
getPoints: function() {
|
|
return this.attrs.points;
|
|
},
|
|
/**
|
|
* set line cap. Can be butt, round, or square
|
|
* @param {String} lineCap
|
|
*/
|
|
setLineCap: function(lineCap) {
|
|
this.attrs.lineCap = lineCap;
|
|
},
|
|
/**
|
|
* get line cap
|
|
*/
|
|
getLineCap: function() {
|
|
return this.attrs.lineCap;
|
|
},
|
|
/**
|
|
* set dash array.
|
|
* @param {Array} dashArray
|
|
* examples:<br>
|
|
* [10, 5] dashes are 10px long and 5 pixels apart
|
|
* [10, 20, 0, 20] if using a round lineCap, the line will
|
|
* be made up of alternating dashed lines that are 10px long
|
|
* and 20px apart, and dots that have a radius of 5 and are 20px
|
|
* apart
|
|
*/
|
|
setDashArray: function(dashArray) {
|
|
this.attrs.dashArray = dashArray;
|
|
},
|
|
/**
|
|
* get dash array
|
|
*/
|
|
getDashArray: function() {
|
|
return this.attrs.dashArray;
|
|
},
|
|
/**
|
|
* draw dashed line. Written by Phrogz
|
|
*/
|
|
_dashedLine: function(x, y, x2, y2, dashArray) {
|
|
var context = this.getContext();
|
|
var dashCount = dashArray.length;
|
|
|
|
var dx = (x2 - x), dy = (y2 - y);
|
|
var xSlope = (Math.abs(dx) > Math.abs(dy));
|
|
var slope = (xSlope) ? dy / dx : dx / dy;
|
|
var distRemaining = Math.sqrt(dx * dx + dy * dy);
|
|
var dashIndex = 0, draw = true;
|
|
while(distRemaining >= 0.1 && dashIndex < 10000) {
|
|
var dashLength = dashArray[dashIndex++ % dashCount];
|
|
if(dashLength === 0) {
|
|
dashLength = 0.001;
|
|
}
|
|
if(dashLength > distRemaining) {
|
|
dashLength = distRemaining;
|
|
}
|
|
var step = Math.sqrt(dashLength * dashLength / (1 + slope * slope));
|
|
if(xSlope) {
|
|
x += step;
|
|
y += slope * step;
|
|
}
|
|
else {
|
|
x += slope * step;
|
|
y += step;
|
|
}
|
|
context[draw ? 'lineTo' : 'moveTo'](x, y);
|
|
distRemaining -= dashLength;
|
|
draw = !draw;
|
|
}
|
|
}
|
|
};
|
|
|
|
// extend Shape
|
|
Kinetic.GlobalObject.extend(Kinetic.Line, Kinetic.Shape);
|
|
|
|
/*
|
|
* Last updated November 2011
|
|
* By Simon Sarris
|
|
* www.simonsarris.com
|
|
* sarris@acm.org
|
|
*
|
|
* Free to use and distribute at will
|
|
* So long as you are nice to people, etc
|
|
*/
|
|
|
|
/*
|
|
* The usage of this class was inspired by some of the work done by a forked
|
|
* project, KineticJS-Ext by Wappworks, which is based on Simon's Transform
|
|
* class.
|
|
*/
|
|
|
|
/**
|
|
* Matrix object
|
|
*/
|
|
Kinetic.Transform = function() {
|
|
this.m = [1, 0, 0, 1, 0, 0];
|
|
}
|
|
|
|
Kinetic.Transform.prototype = {
|
|
/**
|
|
* Apply translation
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
translate: function(x, y) {
|
|
this.m[4] += this.m[0] * x + this.m[2] * y;
|
|
this.m[5] += this.m[1] * x + this.m[3] * y;
|
|
},
|
|
/**
|
|
* Apply scale
|
|
* @param {Number} sx
|
|
* @param {Number} sy
|
|
*/
|
|
scale: function(sx, sy) {
|
|
this.m[0] *= sx;
|
|
this.m[1] *= sx;
|
|
this.m[2] *= sy;
|
|
this.m[3] *= sy;
|
|
},
|
|
/**
|
|
* Apply rotation
|
|
* @param {Number} rad Angle in radians
|
|
*/
|
|
rotate: function(rad) {
|
|
var c = Math.cos(rad);
|
|
var s = Math.sin(rad);
|
|
var m11 = this.m[0] * c + this.m[2] * s;
|
|
var m12 = this.m[1] * c + this.m[3] * s;
|
|
var m21 = this.m[0] * -s + this.m[2] * c;
|
|
var m22 = this.m[1] * -s + this.m[3] * c;
|
|
this.m[0] = m11;
|
|
this.m[1] = m12;
|
|
this.m[2] = m21;
|
|
this.m[3] = m22;
|
|
},
|
|
/**
|
|
* Returns the translation
|
|
* @returns {Object} 2D point(x, y)
|
|
*/
|
|
getTranslation: function() {
|
|
return {
|
|
x: this.m[4],
|
|
y: this.m[5]
|
|
};
|
|
},
|
|
/**
|
|
* Transform multiplication
|
|
* @param {Kinetic.Transform} matrix
|
|
*/
|
|
multiply: function(matrix) {
|
|
var m11 = this.m[0] * matrix.m[0] + this.m[2] * matrix.m[1];
|
|
var m12 = this.m[1] * matrix.m[0] + this.m[3] * matrix.m[1];
|
|
|
|
var m21 = this.m[0] * matrix.m[2] + this.m[2] * matrix.m[3];
|
|
var m22 = this.m[1] * matrix.m[2] + this.m[3] * matrix.m[3];
|
|
|
|
var dx = this.m[0] * matrix.m[4] + this.m[2] * matrix.m[5] + this.m[4];
|
|
var dy = this.m[1] * matrix.m[4] + this.m[3] * matrix.m[5] + this.m[5];
|
|
|
|
this.m[0] = m11;
|
|
this.m[1] = m12;
|
|
this.m[2] = m21;
|
|
this.m[3] = m22;
|
|
this.m[4] = dx;
|
|
this.m[5] = dy;
|
|
},
|
|
/**
|
|
* Invert the matrix
|
|
*/
|
|
invert: function() {
|
|
var d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]);
|
|
var m0 = this.m[3] * d;
|
|
var m1 = -this.m[1] * d;
|
|
var m2 = -this.m[2] * d;
|
|
var m3 = this.m[0] * d;
|
|
var m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]);
|
|
var m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]);
|
|
this.m[0] = m0;
|
|
this.m[1] = m1;
|
|
this.m[2] = m2;
|
|
this.m[3] = m3;
|
|
this.m[4] = m4;
|
|
this.m[5] = m5;
|
|
},
|
|
/**
|
|
* return matrix
|
|
*/
|
|
getMatrix: function() {
|
|
return this.m;
|
|
}
|
|
};
|
|
|
|
/*
|
|
* The Tween class was ported from an Adobe Flash Tween library
|
|
* to JavaScript by Xaric. In the context of KineticJS, a Tween is
|
|
* an animation of a single Node property. A Transition is a set of
|
|
* multiple tweens
|
|
*/
|
|
|
|
/**
|
|
* Transition constructor. KineticJS transitions contain
|
|
* multiple Tweens
|
|
*/
|
|
Kinetic.Transition = function(node, config) {
|
|
this.node = node;
|
|
this.config = config;
|
|
this.tweens = [];
|
|
|
|
// add tween for each property
|
|
for(var key in config) {
|
|
if(key !== 'duration' && key !== 'easing' && key !== 'callback') {
|
|
if(config[key].x === undefined && config[key].y === undefined) {
|
|
this.add(this._getTween(key, config));
|
|
}
|
|
if(config[key].x !== undefined) {
|
|
this.add(this._getComponentTween(key, 'x', config));
|
|
}
|
|
if(config[key].y !== undefined) {
|
|
this.add(this._getComponentTween(key, 'y', config));
|
|
}
|
|
}
|
|
}
|
|
|
|
var finishedTweens = 0;
|
|
var that = this;
|
|
for(var n = 0; n < this.tweens.length; n++) {
|
|
var tween = this.tweens[n];
|
|
tween.onFinished = function() {
|
|
finishedTweens++;
|
|
if(finishedTweens >= that.tweens.length) {
|
|
that.onFinished();
|
|
}
|
|
};
|
|
}
|
|
};
|
|
/*
|
|
* Transition methods
|
|
*/
|
|
Kinetic.Transition.prototype = {
|
|
/**
|
|
* add tween to tweens array
|
|
* @param {Kinetic.Tween} tween
|
|
*/
|
|
add: function(tween) {
|
|
this.tweens.push(tween);
|
|
},
|
|
/**
|
|
* start transition
|
|
*/
|
|
start: function() {
|
|
for(var n = 0; n < this.tweens.length; n++) {
|
|
this.tweens[n].start();
|
|
}
|
|
},
|
|
/**
|
|
* onEnterFrame
|
|
*/
|
|
onEnterFrame: function() {
|
|
for(var n = 0; n < this.tweens.length; n++) {
|
|
this.tweens[n].onEnterFrame();
|
|
}
|
|
},
|
|
/**
|
|
* stop transition
|
|
*/
|
|
stop: function() {
|
|
for(var n = 0; n < this.tweens.length; n++) {
|
|
this.tweens[n].stop();
|
|
}
|
|
},
|
|
/**
|
|
* resume transition
|
|
*/
|
|
resume: function() {
|
|
for(var n = 0; n < this.tweens.length; n++) {
|
|
this.tweens[n].resume();
|
|
}
|
|
},
|
|
_getTween: function(key) {
|
|
var config = this.config;
|
|
var node = this.node;
|
|
var easing = config.easing;
|
|
if(easing === undefined) {
|
|
easing = 'linear';
|
|
}
|
|
|
|
var tween = new Kinetic.Tween(node, function(i) {
|
|
node.attrs[key] = i;
|
|
}, Kinetic.Tweens[easing], node.attrs[key], config[key], config.duration);
|
|
|
|
return tween;
|
|
},
|
|
_getComponentTween: function(key, prop) {
|
|
var config = this.config;
|
|
var node = this.node;
|
|
var easing = config.easing;
|
|
if(easing === undefined) {
|
|
easing = 'linear';
|
|
}
|
|
|
|
var tween = new Kinetic.Tween(node, function(i) {
|
|
node.attrs[key][prop] = i;
|
|
}, Kinetic.Tweens[easing], node.attrs[key][prop], config[key][prop], config.duration);
|
|
|
|
return tween;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Tween constructor
|
|
*/
|
|
Kinetic.Tween = function(obj, propFunc, func, begin, finish, duration) {
|
|
this._listeners = [];
|
|
this.addListener(this);
|
|
this.obj = obj;
|
|
this.propFunc = propFunc;
|
|
this.begin = begin;
|
|
this._pos = begin;
|
|
this.setDuration(duration);
|
|
this.isPlaying = false;
|
|
this._change = 0;
|
|
this.prevTime = 0;
|
|
this.prevPos = 0;
|
|
this.looping = false;
|
|
this._time = 0;
|
|
this._position = 0;
|
|
this._startTime = 0;
|
|
this._finish = 0;
|
|
this.name = '';
|
|
this.func = func;
|
|
this.setFinish(finish);
|
|
};
|
|
/*
|
|
* Tween methods
|
|
*/
|
|
Kinetic.Tween.prototype = {
|
|
setTime: function(t) {
|
|
this.prevTime = this._time;
|
|
if(t > this.getDuration()) {
|
|
if(this.looping) {
|
|
this.rewind(t - this._duration);
|
|
this.update();
|
|
this.broadcastMessage('onLooped', {
|
|
target: this,
|
|
type: 'onLooped'
|
|
});
|
|
}
|
|
else {
|
|
this._time = this._duration;
|
|
this.update();
|
|
this.stop();
|
|
this.broadcastMessage('onFinished', {
|
|
target: this,
|
|
type: 'onFinished'
|
|
});
|
|
}
|
|
}
|
|
else if(t < 0) {
|
|
this.rewind();
|
|
this.update();
|
|
}
|
|
else {
|
|
this._time = t;
|
|
this.update();
|
|
}
|
|
},
|
|
getTime: function() {
|
|
return this._time;
|
|
},
|
|
setDuration: function(d) {
|
|
this._duration = (d === null || d <= 0) ? 100000 : d;
|
|
},
|
|
getDuration: function() {
|
|
return this._duration;
|
|
},
|
|
setPosition: function(p) {
|
|
this.prevPos = this._pos;
|
|
//var a = this.suffixe != '' ? this.suffixe : '';
|
|
this.propFunc(p);
|
|
//+ a;
|
|
//this.obj(Math.round(p));
|
|
this._pos = p;
|
|
this.broadcastMessage('onChanged', {
|
|
target: this,
|
|
type: 'onChanged'
|
|
});
|
|
},
|
|
getPosition: function(t) {
|
|
if(t === undefined) {
|
|
t = this._time;
|
|
}
|
|
return this.func(t, this.begin, this._change, this._duration);
|
|
},
|
|
setFinish: function(f) {
|
|
this._change = f - this.begin;
|
|
},
|
|
getFinish: function() {
|
|
return this.begin + this._change;
|
|
},
|
|
start: function() {
|
|
this.rewind();
|
|
this.startEnterFrame();
|
|
this.broadcastMessage('onStarted', {
|
|
target: this,
|
|
type: 'onStarted'
|
|
});
|
|
},
|
|
rewind: function(t) {
|
|
this.stop();
|
|
this._time = (t === undefined) ? 0 : t;
|
|
this.fixTime();
|
|
this.update();
|
|
},
|
|
fforward: function() {
|
|
this._time = this._duration;
|
|
this.fixTime();
|
|
this.update();
|
|
},
|
|
update: function() {
|
|
this.setPosition(this.getPosition(this._time));
|
|
},
|
|
startEnterFrame: function() {
|
|
this.stopEnterFrame();
|
|
this.isPlaying = true;
|
|
this.onEnterFrame();
|
|
},
|
|
onEnterFrame: function() {
|
|
if(this.isPlaying) {
|
|
this.nextFrame();
|
|
}
|
|
},
|
|
nextFrame: function() {
|
|
this.setTime((this.getTimer() - this._startTime) / 1000);
|
|
},
|
|
stop: function() {
|
|
this.stopEnterFrame();
|
|
this.broadcastMessage('onStopped', {
|
|
target: this,
|
|
type: 'onStopped'
|
|
});
|
|
},
|
|
stopEnterFrame: function() {
|
|
this.isPlaying = false;
|
|
},
|
|
continueTo: function(finish, duration) {
|
|
this.begin = this._pos;
|
|
this.setFinish(finish);
|
|
if(this._duration != undefined)
|
|
this.setDuration(duration);
|
|
this.start();
|
|
},
|
|
resume: function() {
|
|
this.fixTime();
|
|
this.startEnterFrame();
|
|
this.broadcastMessage('onResumed', {
|
|
target: this,
|
|
type: 'onResumed'
|
|
});
|
|
},
|
|
yoyo: function() {
|
|
this.continueTo(this.begin, this._time);
|
|
},
|
|
addListener: function(o) {
|
|
this.removeListener(o);
|
|
return this._listeners.push(o);
|
|
},
|
|
removeListener: function(o) {
|
|
var a = this._listeners;
|
|
var i = a.length;
|
|
while(i--) {
|
|
if(a[i] == o) {
|
|
a.splice(i, 1);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
broadcastMessage: function() {
|
|
var arr = [];
|
|
for(var i = 0; i < arguments.length; i++) {
|
|
arr.push(arguments[i]);
|
|
}
|
|
var e = arr.shift();
|
|
var a = this._listeners;
|
|
var l = a.length;
|
|
for(var i = 0; i < l; i++) {
|
|
if(a[i][e]) {
|
|
a[i][e].apply(a[i], arr);
|
|
}
|
|
}
|
|
},
|
|
fixTime: function() {
|
|
this._startTime = this.getTimer() - this._time * 1000;
|
|
},
|
|
getTimer: function() {
|
|
return new Date().getTime() - this._time;
|
|
}
|
|
};
|
|
|
|
Kinetic.Tweens = {
|
|
'back-ease-in': function(t, b, c, d, a, p) {
|
|
var s = 1.70158;
|
|
return c * (t /= d) * t * ((s + 1) * t - s) + b;
|
|
},
|
|
'back-ease-out': function(t, b, c, d, a, p) {
|
|
var s = 1.70158;
|
|
return c * (( t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
|
|
},
|
|
'back-ease-in-out': function(t, b, c, d, a, p) {
|
|
var s = 1.70158;
|
|
if((t /= d / 2) < 1) {
|
|
return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
|
|
}
|
|
return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
|
|
},
|
|
'elastic-ease-in': function(t, b, c, d, a, p) {
|
|
// added s = 0
|
|
var s = 0;
|
|
if(t === 0) {
|
|
return b;
|
|
}
|
|
if((t /= d) == 1) {
|
|
return b + c;
|
|
}
|
|
if(!p) {
|
|
p = d * 0.3;
|
|
}
|
|
if(!a || a < Math.abs(c)) {
|
|
a = c;
|
|
s = p / 4;
|
|
}
|
|
else {
|
|
s = p / (2 * Math.PI) * Math.asin(c / a);
|
|
}
|
|
return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
|
|
},
|
|
'elastic-ease-out': function(t, b, c, d, a, p) {
|
|
// added s = 0
|
|
var s = 0;
|
|
if(t === 0) {
|
|
return b;
|
|
}
|
|
if((t /= d) == 1) {
|
|
return b + c;
|
|
}
|
|
if(!p) {
|
|
p = d * 0.3;
|
|
}
|
|
if(!a || a < Math.abs(c)) {
|
|
a = c;
|
|
s = p / 4;
|
|
}
|
|
else {
|
|
s = p / (2 * Math.PI) * Math.asin(c / a);
|
|
}
|
|
return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b);
|
|
},
|
|
'elastic-ease-in-out': function(t, b, c, d, a, p) {
|
|
// added s = 0
|
|
var s = 0;
|
|
if(t === 0) {
|
|
return b;
|
|
}
|
|
if((t /= d / 2) == 2) {
|
|
return b + c;
|
|
}
|
|
if(!p) {
|
|
p = d * (0.3 * 1.5);
|
|
}
|
|
if(!a || a < Math.abs(c)) {
|
|
a = c;
|
|
s = p / 4;
|
|
}
|
|
else {
|
|
s = p / (2 * Math.PI) * Math.asin(c / a);
|
|
}
|
|
if(t < 1) {
|
|
return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
|
|
}
|
|
return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b;
|
|
},
|
|
'bounce-ease-out': function(t, b, c, d) {
|
|
if((t /= d) < (1 / 2.75)) {
|
|
return c * (7.5625 * t * t) + b;
|
|
}
|
|
else if(t < (2 / 2.75)) {
|
|
return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b;
|
|
}
|
|
else if(t < (2.5 / 2.75)) {
|
|
return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b;
|
|
}
|
|
else {
|
|
return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b;
|
|
}
|
|
},
|
|
'bounce-ease-in': function(t, b, c, d) {
|
|
return c - Kinetic.Tweens['bounce-ease-out'](d - t, 0, c, d) + b;
|
|
},
|
|
'bounce-ease-in-out': function(t, b, c, d) {
|
|
if(t < d / 2) {
|
|
return Kinetic.Tweens['bounce-ease-in'](t * 2, 0, c, d) * 0.5 + b;
|
|
}
|
|
else {
|
|
return Kinetic.Tweens['bounce-ease-out'](t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b;
|
|
}
|
|
},
|
|
// duplicate
|
|
/*
|
|
strongEaseInOut: function(t, b, c, d) {
|
|
return c * (t /= d) * t * t * t * t + b;
|
|
},
|
|
*/
|
|
'ease-in': function(t, b, c, d) {
|
|
return c * (t /= d) * t + b;
|
|
},
|
|
'ease-out': function(t, b, c, d) {
|
|
return -c * (t /= d) * (t - 2) + b;
|
|
},
|
|
'ease-in-out': function(t, b, c, d) {
|
|
if((t /= d / 2) < 1) {
|
|
return c / 2 * t * t + b;
|
|
}
|
|
return -c / 2 * ((--t) * (t - 2) - 1) + b;
|
|
},
|
|
'strong-ease-in': function(t, b, c, d) {
|
|
return c * (t /= d) * t * t * t * t + b;
|
|
},
|
|
'strong-ease-out': function(t, b, c, d) {
|
|
return c * (( t = t / d - 1) * t * t * t * t + 1) + b;
|
|
},
|
|
'strong-ease-in-out': function(t, b, c, d) {
|
|
if((t /= d / 2) < 1) {
|
|
return c / 2 * t * t * t * t * t + b;
|
|
}
|
|
return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
|
|
},
|
|
'linear': function(t, b, c, d) {
|
|
return c * t / d + b;
|
|
},
|
|
};
|
|
|