mirror of
https://github.com/konvajs/konva.git
synced 2025-06-28 15:23:44 +08:00

Changed collections each-method to use same syntax as jQuery - function(index, element) and also being able to use 'this' to get the element. Added test for apply('on',...) since it needs more than one argument.
6653 lines
196 KiB
JavaScript
6653 lines
196 KiB
JavaScript
/**
|
|
* KineticJS JavaScript Library core
|
|
* http://www.kineticjs.com/
|
|
* Copyright 2012, Eric Rowell
|
|
* Licensed under the MIT or GPL Version 2 licenses.
|
|
* Date: Sep 23 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
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Kinetic Namespace
|
|
* @namespace
|
|
*/
|
|
var Kinetic = {};
|
|
Kinetic.Filters = {};
|
|
Kinetic.Plugins = {};
|
|
Kinetic.Global = {
|
|
BUBBLE_WHITELIST: ['mousedown', 'mousemove', 'mouseup', 'mouseover', 'mouseout', 'click', 'dblclick', 'touchstart', 'touchmove', 'touchend', 'tap', 'dbltap', 'dragstart', 'dragmove', 'dragend'],
|
|
BUFFER_WHITELIST: ['fill', 'stroke', 'textFill', 'textStroke'],
|
|
BUFFER_BLACKLIST: ['shadow'],
|
|
stages: [],
|
|
idCounter: 0,
|
|
tempNodes: {},
|
|
//shapes hash. rgb keys and shape values
|
|
shapes: {},
|
|
maxDragTimeInterval: 20,
|
|
drag: {
|
|
moving: false,
|
|
offset: {
|
|
x: 0,
|
|
y: 0
|
|
},
|
|
lastDrawTime: 0
|
|
},
|
|
warn: function(str) {
|
|
if(console && console.warn) {
|
|
console.warn('Kinetic warning: ' + str);
|
|
}
|
|
},
|
|
extend: function(c1, c2) {
|
|
for(var key in c2.prototype) {
|
|
if(!( key in c1.prototype)) {
|
|
c1.prototype[key] = c2.prototype[key];
|
|
}
|
|
}
|
|
},
|
|
_pullNodes: function(stage) {
|
|
var tempNodes = this.tempNodes;
|
|
for(var key in tempNodes) {
|
|
var node = tempNodes[key];
|
|
if(node.getStage() !== undefined && node.getStage()._id === stage._id) {
|
|
stage._addId(node);
|
|
stage._addName(node);
|
|
this._removeTempNode(node);
|
|
}
|
|
}
|
|
},
|
|
_addTempNode: function(node) {
|
|
this.tempNodes[node._id] = node;
|
|
},
|
|
_removeTempNode: function(node) {
|
|
delete this.tempNodes[node._id];
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Transition
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Transition constructor. The transitionTo() Node method
|
|
* returns a reference to the transition object which you can use
|
|
* to stop, resume, or restart the transition
|
|
* @constructor
|
|
*/
|
|
Kinetic.Transition = function(node, config) {
|
|
this.node = node;
|
|
this.config = config;
|
|
this.tweens = [];
|
|
var that = this;
|
|
|
|
// add tween for each property
|
|
function addTween(c, attrs, obj, rootObj) {
|
|
for(var key in c) {
|
|
if(key !== 'duration' && key !== 'easing' && key !== 'callback') {
|
|
// if val is an object then traverse
|
|
if(Kinetic.Type._isObject(c[key])) {
|
|
obj[key] = {};
|
|
addTween(c[key], attrs[key], obj[key], rootObj);
|
|
}
|
|
else {
|
|
that._add(that._getTween(attrs, key, c[key], obj, rootObj));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var obj = {};
|
|
addTween(config, node.attrs, obj, obj);
|
|
|
|
var finishedTweens = 0;
|
|
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 = {
|
|
/**
|
|
* start transition
|
|
* @name start
|
|
* @methodOf Kinetic.Transition.prototype
|
|
*/
|
|
start: function() {
|
|
for(var n = 0; n < this.tweens.length; n++) {
|
|
this.tweens[n].start();
|
|
}
|
|
},
|
|
/**
|
|
* stop transition
|
|
* @name stop
|
|
* @methodOf Kinetic.Transition.prototype
|
|
*/
|
|
stop: function() {
|
|
for(var n = 0; n < this.tweens.length; n++) {
|
|
this.tweens[n].stop();
|
|
}
|
|
},
|
|
/**
|
|
* resume transition
|
|
* @name resume
|
|
* @methodOf Kinetic.Transition.prototype
|
|
*/
|
|
resume: function() {
|
|
for(var n = 0; n < this.tweens.length; n++) {
|
|
this.tweens[n].resume();
|
|
}
|
|
},
|
|
_onEnterFrame: function() {
|
|
for(var n = 0; n < this.tweens.length; n++) {
|
|
this.tweens[n].onEnterFrame();
|
|
}
|
|
},
|
|
_add: function(tween) {
|
|
this.tweens.push(tween);
|
|
},
|
|
_getTween: function(attrs, prop, val, obj, rootObj) {
|
|
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) {
|
|
obj[prop] = i;
|
|
node.setAttrs(rootObj);
|
|
}, Kinetic.Tweens[easing], attrs[prop], val, config.duration);
|
|
|
|
return tween;
|
|
}
|
|
};
|
|
|
|
Kinetic.Filters.Grayscale = function(imageData) {
|
|
var data = imageData.data;
|
|
for(var i = 0; i < data.length; i += 4) {
|
|
var brightness = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2];
|
|
// red
|
|
data[i] = brightness;
|
|
// green
|
|
data[i + 1] = brightness;
|
|
// blue
|
|
data[i + 2] = brightness;
|
|
// i+3 is alpha (the fourth element)
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Type
|
|
///////////////////////////////////////////////////////////////////////
|
|
/*
|
|
* utilities that determine data type and transform
|
|
* one type into another
|
|
*/
|
|
Kinetic.Type = {
|
|
/*
|
|
* cherry-picked utilities from underscore.js
|
|
*/
|
|
_isElement: function(obj) {
|
|
return !!(obj && obj.nodeType == 1);
|
|
},
|
|
_isFunction: function(obj) {
|
|
return !!(obj && obj.constructor && obj.call && obj.apply);
|
|
},
|
|
_isObject: function(obj) {
|
|
return (!!obj && obj.constructor == Object);
|
|
},
|
|
_isArray: function(obj) {
|
|
return Object.prototype.toString.call(obj) == '[object Array]';
|
|
},
|
|
_isNumber: function(obj) {
|
|
return Object.prototype.toString.call(obj) == '[object Number]';
|
|
},
|
|
_isString: function(obj) {
|
|
return Object.prototype.toString.call(obj) == '[object String]';
|
|
},
|
|
/*
|
|
* other utils
|
|
*/
|
|
_hasMethods: function(obj) {
|
|
var names = [];
|
|
for(var key in obj) {
|
|
if(this._isFunction(obj[key]))
|
|
names.push(key);
|
|
}
|
|
return names.length > 0;
|
|
},
|
|
/*
|
|
* The argument can be:
|
|
* - an integer (will be applied to both x and y)
|
|
* - an array of one integer (will be applied to both x and y)
|
|
* - an array of two integers (contains x and y)
|
|
* - an array of four integers (contains x, y, width, and height)
|
|
* - an object with x and y properties
|
|
* - an array of one element which is an array of integers
|
|
* - an array of one element of an object
|
|
*/
|
|
_getXY: function(arg) {
|
|
if(this._isNumber(arg)) {
|
|
return {
|
|
x: arg,
|
|
y: arg
|
|
};
|
|
}
|
|
else if(this._isArray(arg)) {
|
|
// if arg is an array of one element
|
|
if(arg.length === 1) {
|
|
var val = arg[0];
|
|
// if arg is an array of one element which is a number
|
|
if(this._isNumber(val)) {
|
|
return {
|
|
x: val,
|
|
y: val
|
|
};
|
|
}
|
|
// if arg is an array of one element which is an array
|
|
else if(this._isArray(val)) {
|
|
return {
|
|
x: val[0],
|
|
y: val[1]
|
|
};
|
|
}
|
|
// if arg is an array of one element which is an object
|
|
else if(this._isObject(val)) {
|
|
return val;
|
|
}
|
|
}
|
|
// if arg is an array of two or more elements
|
|
else if(arg.length >= 2) {
|
|
return {
|
|
x: arg[0],
|
|
y: arg[1]
|
|
};
|
|
}
|
|
}
|
|
// if arg is an object return the object
|
|
else if(this._isObject(arg)) {
|
|
return arg;
|
|
}
|
|
|
|
// default
|
|
return {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
},
|
|
/*
|
|
* The argument can be:
|
|
* - an integer (will be applied to both width and height)
|
|
* - an array of one integer (will be applied to both width and height)
|
|
* - an array of two integers (contains width and height)
|
|
* - an array of four integers (contains x, y, width, and height)
|
|
* - an object with width and height properties
|
|
* - an array of one element which is an array of integers
|
|
* - an array of one element of an object
|
|
*/
|
|
_getSize: function(arg) {
|
|
if(this._isNumber(arg)) {
|
|
return {
|
|
width: arg,
|
|
height: arg
|
|
};
|
|
}
|
|
else if(this._isArray(arg)) {
|
|
// if arg is an array of one element
|
|
if(arg.length === 1) {
|
|
var val = arg[0];
|
|
// if arg is an array of one element which is a number
|
|
if(this._isNumber(val)) {
|
|
return {
|
|
width: val,
|
|
height: val
|
|
};
|
|
}
|
|
// if arg is an array of one element which is an array
|
|
else if(this._isArray(val)) {
|
|
/*
|
|
* if arg is an array of one element which is an
|
|
* array of four elements
|
|
*/
|
|
if(val.length >= 4) {
|
|
return {
|
|
width: val[2],
|
|
height: val[3]
|
|
};
|
|
}
|
|
/*
|
|
* if arg is an array of one element which is an
|
|
* array of two elements
|
|
*/
|
|
else if(val.length >= 2) {
|
|
return {
|
|
width: val[0],
|
|
height: val[1]
|
|
};
|
|
}
|
|
}
|
|
// if arg is an array of one element which is an object
|
|
else if(this._isObject(val)) {
|
|
return val;
|
|
}
|
|
}
|
|
// if arg is an array of four elements
|
|
else if(arg.length >= 4) {
|
|
return {
|
|
width: arg[2],
|
|
height: arg[3]
|
|
};
|
|
}
|
|
// if arg is an array of two elements
|
|
else if(arg.length >= 2) {
|
|
return {
|
|
width: arg[0],
|
|
height: arg[1]
|
|
};
|
|
}
|
|
}
|
|
// if arg is an object return the object
|
|
else if(this._isObject(arg)) {
|
|
return arg;
|
|
}
|
|
|
|
// default
|
|
return {
|
|
width: 0,
|
|
height: 0
|
|
};
|
|
},
|
|
/*
|
|
* arg will be an array of numbers or
|
|
* an array of point objects
|
|
*/
|
|
_getPoints: function(arg) {
|
|
if(arg === undefined) {
|
|
return [];
|
|
}
|
|
|
|
// an array of objects
|
|
if(this._isObject(arg[0])) {
|
|
return arg;
|
|
}
|
|
// an array of integers
|
|
else {
|
|
/*
|
|
* convert array of numbers into an array
|
|
* of objects containing x, y
|
|
*/
|
|
var arr = [];
|
|
for(var n = 0; n < arg.length; n += 2) {
|
|
arr.push({
|
|
x: arg[n],
|
|
y: arg[n + 1]
|
|
});
|
|
}
|
|
|
|
return arr;
|
|
}
|
|
},
|
|
/*
|
|
* arg can be an image object or image data
|
|
*/
|
|
_getImage: function(arg, callback) {
|
|
// if arg is null or undefined
|
|
if(!arg) {
|
|
callback(null);
|
|
}
|
|
|
|
// if arg is already an image object
|
|
else if(this._isElement(arg)) {
|
|
callback(arg);
|
|
}
|
|
|
|
// if arg is a string, then it's a data url
|
|
else if(this._isString(arg)) {
|
|
var imageObj = new Image();
|
|
imageObj.onload = function() {
|
|
callback(imageObj);
|
|
}
|
|
imageObj.src = arg;
|
|
}
|
|
|
|
//if arg is an object that contains the data property, it's an image object
|
|
else if(arg.data) {
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = arg.width;
|
|
canvas.height = arg.height;
|
|
var context = canvas.getContext('2d');
|
|
context.putImageData(arg, 0, 0);
|
|
var dataUrl = canvas.toDataURL();
|
|
var imageObj = new Image();
|
|
imageObj.onload = function() {
|
|
callback(imageObj);
|
|
}
|
|
imageObj.src = dataUrl;
|
|
}
|
|
|
|
else {
|
|
callback(null);
|
|
}
|
|
},
|
|
_rgbToHex: function(r, g, b) {
|
|
return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
|
},
|
|
_hexToRgb: function(hex) {
|
|
var bigint = parseInt(hex, 16);
|
|
return {
|
|
r: (bigint >> 16) & 255,
|
|
g: (bigint >> 8) & 255,
|
|
b: bigint & 255
|
|
};
|
|
},
|
|
_getRandomColorKey: function() {
|
|
var r = Math.round(Math.random() * 255);
|
|
var g = Math.round(Math.random() * 255);
|
|
var b = Math.round(Math.random() * 255);
|
|
return this._rgbToHex(r, g, b);
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Canvas
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Canvas wrapper constructor
|
|
* @constructor
|
|
* @param {Number} width
|
|
* @param {Number} height
|
|
*/
|
|
Kinetic.Canvas = function(width, height) {
|
|
this.element = document.createElement('canvas');
|
|
this.context = this.element.getContext('2d');
|
|
|
|
// set dimensions
|
|
this.element.width = width;
|
|
this.element.height = height;
|
|
};
|
|
|
|
Kinetic.Canvas.prototype = {
|
|
/**
|
|
* clear canvas
|
|
* @name clear
|
|
* @methodOf Kinetic.Canvas.prototype
|
|
*/
|
|
clear: function() {
|
|
var context = this.getContext();
|
|
var el = this.getElement();
|
|
context.clearRect(0, 0, el.width, el.height);
|
|
},
|
|
/**
|
|
* get element
|
|
* @name getElement
|
|
* @methodOf Kinetic.Canvas.prototype
|
|
*/
|
|
getElement: function() {
|
|
return this.element;
|
|
},
|
|
/**
|
|
* get context
|
|
* @name getContext
|
|
* @methodOf Kinetic.Canvas.prototype
|
|
*/
|
|
getContext: function() {
|
|
return this.context;
|
|
},
|
|
/**
|
|
* set width
|
|
* @name setWidth
|
|
* @methodOf Kinetic.Canvas.prototype
|
|
*/
|
|
setWidth: function(width) {
|
|
this.element.width = width;
|
|
},
|
|
/**
|
|
* set height
|
|
* @name setHeight
|
|
* @methodOf Kinetic.Canvas.prototype
|
|
*/
|
|
setHeight: function(height) {
|
|
this.element.height = height;
|
|
},
|
|
/**
|
|
* get width
|
|
* @name getWidth
|
|
* @methodOf Kinetic.Canvas.prototype
|
|
*/
|
|
getWidth: function() {
|
|
return this.element.width;
|
|
},
|
|
/**
|
|
* get height
|
|
* @name getHeight
|
|
* @methodOf Kinetic.Canvas.prototype
|
|
*/
|
|
getHeight: function() {
|
|
return this.element.height;
|
|
},
|
|
/**
|
|
* set size
|
|
* @name setSize
|
|
* @methodOf Kinetic.Canvas.prototype
|
|
*/
|
|
setSize: function(width, height) {
|
|
this.setWidth(width);
|
|
this.setHeight(height);
|
|
},
|
|
/**
|
|
* toDataURL
|
|
*/
|
|
toDataURL: function(mimeType, quality) {
|
|
try {
|
|
// If this call fails (due to browser bug, like in Firefox 3.6),
|
|
// then revert to previous no-parameter image/png behavior
|
|
return this.element.toDataURL(mimeType, quality);
|
|
}
|
|
catch(e) {
|
|
return this.element.toDataURL();
|
|
}
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Tween
|
|
///////////////////////////////////////////////////////////////////////
|
|
/*
|
|
* 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
|
|
*/
|
|
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;
|
|
this.propFunc(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;
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Transform
|
|
///////////////////////////////////////////////////////////////////////
|
|
/*
|
|
* 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.
|
|
*/
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Collection constructor. Collection extends
|
|
* Array. This class is used in conjunction with get()
|
|
* @constructor
|
|
*/
|
|
Kinetic.Collection = function() {
|
|
var args = [].slice.call( arguments ),
|
|
length = args.length, i = 0;
|
|
|
|
this.length = length;
|
|
for (; i < length; i++ ) {
|
|
this[ i ] = args[ i ];
|
|
}
|
|
return this;
|
|
}
|
|
Kinetic.Collection.prototype = new Array();
|
|
/**
|
|
* apply a method to all nodes in the array
|
|
* @name apply
|
|
* @methodOf Kinetic.Collection.prototype
|
|
* @param {String} method
|
|
* @param val
|
|
*/
|
|
Kinetic.Collection.prototype.apply = function(method) {
|
|
args = [].slice.call(arguments);
|
|
args.shift();
|
|
for (var n=0; n<this.length; n++) {
|
|
if(Kinetic.Type._isFunction(this[n][method])) {
|
|
this[n][method].apply(this[n],args);
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* iterate through node array
|
|
* @name each
|
|
* @methodOf Kinetic.Collection.prototype
|
|
* @param {Function} func
|
|
*/
|
|
Kinetic.Collection.prototype.each = function(func) {
|
|
for (var n=0; n<this.length; n++) {
|
|
func.call(this[n],n,this[n]);
|
|
}
|
|
};
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Animation
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Stage constructor. A stage is used to contain multiple layers and handle
|
|
* animations
|
|
* @constructor
|
|
* @augments Kinetic.Container
|
|
* @param {Object} config
|
|
* @param {Function} config.func function to be executed on each animation frame
|
|
*/
|
|
Kinetic.Animation = function(config) {
|
|
if(!config) {
|
|
config = {};
|
|
}
|
|
for(var key in config) {
|
|
this[key] = config[key];
|
|
}
|
|
|
|
// add frame object
|
|
this.frame = {
|
|
time: 0,
|
|
timeDiff: 0,
|
|
lastTime: new Date().getTime()
|
|
};
|
|
|
|
this.id = Kinetic.Animation.animIdCounter++;
|
|
};
|
|
/*
|
|
* Animation methods
|
|
*/
|
|
Kinetic.Animation.prototype = {
|
|
/**
|
|
* start animation
|
|
* @name start
|
|
* @methodOf Kinetic.Animation.prototype
|
|
*/
|
|
start: function() {
|
|
this.stop();
|
|
this.frame.lastTime = new Date().getTime();
|
|
Kinetic.Animation._addAnimation(this);
|
|
Kinetic.Animation._handleAnimation();
|
|
},
|
|
/**
|
|
* stop animation
|
|
* @name stop
|
|
* @methodOf Kinetic.Animation.prototype
|
|
*/
|
|
stop: function() {
|
|
Kinetic.Animation._removeAnimation(this);
|
|
}
|
|
};
|
|
Kinetic.Animation.animations = [];
|
|
Kinetic.Animation.animIdCounter = 0;
|
|
Kinetic.Animation.animRunning = false;
|
|
Kinetic.Animation._addAnimation = function(anim) {
|
|
this.animations.push(anim);
|
|
};
|
|
Kinetic.Animation._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;
|
|
}
|
|
}
|
|
};
|
|
Kinetic.Animation._updateFrameObject = function(anim) {
|
|
var time = new Date().getTime();
|
|
anim.frame.timeDiff = time - anim.frame.lastTime;
|
|
anim.frame.lastTime = time;
|
|
anim.frame.time += anim.frame.timeDiff;
|
|
};
|
|
Kinetic.Animation._runFrames = function() {
|
|
var nodes = {};
|
|
/*
|
|
* loop through all animations and execute animation
|
|
* function. if the animation object has specified node,
|
|
* we can add the node to the nodes hash to eliminate
|
|
* drawing the same node multiple times. The node property
|
|
* can be the stage itself or a layer
|
|
*/
|
|
for(var n = 0; n < this.animations.length; n++) {
|
|
var anim = this.animations[n];
|
|
this._updateFrameObject(anim);
|
|
if(anim.node && anim.node._id !== undefined) {
|
|
nodes[anim.node._id] = anim.node;
|
|
}
|
|
// if animation object has a function, execute it
|
|
if(anim.func) {
|
|
anim.func(anim.frame);
|
|
}
|
|
}
|
|
|
|
for(var key in nodes) {
|
|
nodes[key].draw();
|
|
}
|
|
};
|
|
Kinetic.Animation._animationLoop = function() {
|
|
if(this.animations.length > 0) {
|
|
this._runFrames();
|
|
var that = this;
|
|
requestAnimFrame(function() {
|
|
that._animationLoop();
|
|
});
|
|
}
|
|
else {
|
|
this.animRunning = false;
|
|
}
|
|
};
|
|
Kinetic.Animation._handleAnimation = function() {
|
|
var that = this;
|
|
if(!this.animRunning) {
|
|
this.animRunning = true;
|
|
that._animationLoop();
|
|
}
|
|
};
|
|
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 be transformed, layered,
|
|
* and have events bound to them. They are the building blocks of a KineticJS
|
|
* application
|
|
* @constructor
|
|
* @param {Object} config
|
|
* @param {Number} [config.x]
|
|
* @param {Number} [config.y]
|
|
* @param {Boolean} [config.visible]
|
|
* @param {Boolean} [config.listening] whether or not the node is listening for events
|
|
* @param {String} [config.id] unique id
|
|
* @param {String} [config.name] non-unique name
|
|
* @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1
|
|
* @param {Object} [config.scale]
|
|
* @param {Number} [config.scale.x]
|
|
* @param {Number} [config.scale.y]
|
|
* @param {Number} [config.rotation] rotation in radians
|
|
* @param {Number} [config.rotationDeg] rotation in degrees
|
|
* @param {Object} [config.offset] offsets default position point and rotation point
|
|
* @param {Number} [config.offset.x]
|
|
* @param {Number} [config.offset.y]
|
|
* @param {Boolean} [config.draggable]
|
|
* @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default
|
|
* is none
|
|
* @param {Object} [config.dragBounds]
|
|
* @param {Number} [config.dragBounds.top]
|
|
* @param {Number} [config.dragBounds.right]
|
|
* @param {Number} [config.dragBounds.bottom]
|
|
* @param {Number} [config.dragBounds.left]
|
|
*/
|
|
Kinetic.Node = function(config) {
|
|
this._nodeInit(config);
|
|
};
|
|
|
|
Kinetic.Node.prototype = {
|
|
_nodeInit: function(config) {
|
|
this.defaultNodeAttrs = {
|
|
visible: true,
|
|
listening: true,
|
|
name: undefined,
|
|
opacity: 1,
|
|
x: 0,
|
|
y: 0,
|
|
scale: {
|
|
x: 1,
|
|
y: 1
|
|
},
|
|
rotation: 0,
|
|
offset: {
|
|
x: 0,
|
|
y: 0
|
|
},
|
|
dragConstraint: 'none',
|
|
dragBounds: {},
|
|
draggable: false
|
|
};
|
|
|
|
this.setDefaultAttrs(this.defaultNodeAttrs);
|
|
this.eventListeners = {};
|
|
this.transAnim = new Kinetic.Animation();
|
|
this.setAttrs(config);
|
|
|
|
// bind events
|
|
this.on('draggableChange.kinetic', function() {
|
|
this._onDraggableChange();
|
|
});
|
|
var that = this;
|
|
this.on('idChange.kinetic', function(evt) {
|
|
var stage = that.getStage();
|
|
if(stage) {
|
|
stage._removeId(evt.oldVal);
|
|
stage._addId(that);
|
|
}
|
|
});
|
|
this.on('nameChange.kinetic', function(evt) {
|
|
var stage = that.getStage();
|
|
if(stage) {
|
|
stage._removeName(evt.oldVal, that._id);
|
|
stage._addName(that);
|
|
}
|
|
});
|
|
|
|
this._onDraggableChange();
|
|
},
|
|
/**
|
|
* bind events to the node. KineticJS supports mouseover, mousemove,
|
|
* mouseout, mousedown, mouseup, click, dblclick, touchstart, touchmove,
|
|
* touchend, tap, 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'.
|
|
* @name on
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @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;
|
|
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'.
|
|
* @name off
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @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 event = 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) {
|
|
delete this.eventListeners[baseEvent];
|
|
break;
|
|
}
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
delete this.eventListeners[baseEvent];
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* get attrs
|
|
* @name getAttrs
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
getAttrs: function() {
|
|
return this.attrs;
|
|
},
|
|
/**
|
|
* set default attrs. This method should only be used if
|
|
* you're creating a custom node
|
|
* @name setDefaultAttrs
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Object} confic
|
|
*/
|
|
setDefaultAttrs: function(config) {
|
|
// create attrs object if undefined
|
|
if(this.attrs === undefined) {
|
|
this.attrs = {};
|
|
}
|
|
|
|
if(config) {
|
|
for(var key in config) {
|
|
/*
|
|
* only set the attr if it's undefined in case
|
|
* a developer writes a custom class that extends
|
|
* a Kinetic Class such that their default property
|
|
* isn't overwritten by the Kinetic Class default
|
|
* property
|
|
*/
|
|
if(this.attrs[key] === undefined) {
|
|
this.attrs[key] = config[key];
|
|
}
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* set attrs
|
|
* @name setAttrs
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Object} config
|
|
*/
|
|
setAttrs: function(config) {
|
|
var type = Kinetic.Type;
|
|
var that = this;
|
|
// set properties from config
|
|
if(config !== undefined) {
|
|
function setAttrs(obj, c, level) {
|
|
for(var key in c) {
|
|
var val = c[key];
|
|
var oldVal = obj[key];
|
|
|
|
/*
|
|
* only fire change event for root
|
|
* level attrs
|
|
*/
|
|
if(level === 0) {
|
|
that._fireBeforeChangeEvent(key, oldVal, val);
|
|
}
|
|
|
|
// if obj doesn't have the val property, then create it
|
|
if(obj[key] === undefined && val !== undefined) {
|
|
obj[key] = {};
|
|
}
|
|
|
|
/*
|
|
* if property is a pure object (no methods), then add an empty object
|
|
* to the node and then traverse
|
|
*/
|
|
if(type._isObject(val) && !type._isArray(val) && !type._isElement(val) && !type._hasMethods(val)) {
|
|
/*
|
|
* since some properties can be strings or objects, e.g.
|
|
* fill, we need to first check that obj is an object
|
|
* before setting properties. If it's not an object,
|
|
* overwrite obj with an object literal
|
|
*/
|
|
if(!Kinetic.Type._isObject(obj[key])) {
|
|
obj[key] = {};
|
|
}
|
|
|
|
setAttrs(obj[key], val, level + 1);
|
|
}
|
|
/*
|
|
* add all other object types to attrs object
|
|
*/
|
|
else {
|
|
// handle special keys
|
|
switch (key) {
|
|
case 'radius':
|
|
if(Kinetic.Type._isNumber(val)) {
|
|
that._setAttr(obj, key, val);
|
|
}
|
|
else {
|
|
var xy = type._getXY(val);
|
|
that._setAttr(obj[key], 'x', xy.x);
|
|
that._setAttr(obj[key], 'y', xy.y);
|
|
}
|
|
break;
|
|
case 'rotationDeg':
|
|
that._setAttr(obj, 'rotation', c[key] * Math.PI / 180);
|
|
// override key for change event
|
|
key = 'rotation';
|
|
break;
|
|
/*
|
|
* includes:
|
|
* - node offset
|
|
* - fill pattern offset
|
|
* - shadow offset
|
|
*/
|
|
case 'offset':
|
|
var pos = type._getXY(val);
|
|
that._setAttr(obj[key], 'x', pos.x);
|
|
that._setAttr(obj[key], 'y', pos.y);
|
|
break;
|
|
case 'scale':
|
|
var pos = type._getXY(val);
|
|
that._setAttr(obj[key], 'x', pos.x);
|
|
that._setAttr(obj[key], 'y', pos.y);
|
|
break;
|
|
case 'points':
|
|
that._setAttr(obj, key, type._getPoints(val));
|
|
break;
|
|
case 'crop':
|
|
var pos = type._getXY(val);
|
|
var size = type._getSize(val);
|
|
that._setAttr(obj[key], 'x', pos.x);
|
|
that._setAttr(obj[key], 'y', pos.y);
|
|
that._setAttr(obj[key], 'width', size.width);
|
|
that._setAttr(obj[key], 'height', size.height);
|
|
break;
|
|
default:
|
|
that._setAttr(obj, key, val);
|
|
break;
|
|
}
|
|
}
|
|
/*
|
|
* only fire change event for root
|
|
* level attrs
|
|
*/
|
|
if(level === 0) {
|
|
that._fireChangeEvent(key, oldVal, val);
|
|
}
|
|
}
|
|
}
|
|
setAttrs(this.attrs, config, 0);
|
|
}
|
|
},
|
|
/**
|
|
* determine if shape is visible or not. Shape is visible only
|
|
* if it's visible and all of its ancestors are visible. If an ancestor
|
|
* is invisible, this means that the shape is also invisible
|
|
* @name isVisible
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
isVisible: function() {
|
|
if(this.attrs.visible && this.getParent() && !this.getParent().isVisible()) {
|
|
return false;
|
|
}
|
|
return this.attrs.visible;
|
|
},
|
|
/**
|
|
* show node
|
|
* @name show
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
show: function() {
|
|
this.setAttrs({
|
|
visible: true
|
|
});
|
|
},
|
|
/**
|
|
* hide node. Hidden nodes are no longer detectable
|
|
* @name hide
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
hide: function() {
|
|
this.setAttrs({
|
|
visible: false
|
|
});
|
|
},
|
|
/**
|
|
* get zIndex
|
|
* @name getZIndex
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
getZIndex: function() {
|
|
return this.index;
|
|
},
|
|
/**
|
|
* get absolute z-index which takes into account sibling
|
|
* and parent indices
|
|
* @name getAbsoluteZIndex
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
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
|
|
* @name getLevel
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
getLevel: function() {
|
|
var level = 0;
|
|
var parent = this.parent;
|
|
while(parent) {
|
|
level++;
|
|
parent = parent.parent;
|
|
}
|
|
return level;
|
|
},
|
|
/**
|
|
* set node position
|
|
* @name setPosition
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
setPosition: function() {
|
|
var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments));
|
|
this.setAttrs(pos);
|
|
},
|
|
/**
|
|
* get node position relative to container
|
|
* @name getPosition
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
getPosition: function() {
|
|
return {
|
|
x: this.attrs.x,
|
|
y: this.attrs.y
|
|
};
|
|
},
|
|
/**
|
|
* get absolute position
|
|
* @name getAbsolutePosition
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
getAbsolutePosition: function() {
|
|
var trans = this.getAbsoluteTransform();
|
|
var o = this.getOffset();
|
|
trans.translate(o.x, o.y);
|
|
return trans.getTranslation();
|
|
},
|
|
/**
|
|
* set absolute position
|
|
* @name setAbsolutePosition
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Object} pos object containing an x and
|
|
* y property
|
|
*/
|
|
setAbsolutePosition: function() {
|
|
var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments));
|
|
var trans = this._clearTransform();
|
|
// don't clear translation
|
|
this.attrs.x = trans.x;
|
|
this.attrs.y = trans.y;
|
|
delete trans.x;
|
|
delete trans.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._setTransform(trans);
|
|
},
|
|
/**
|
|
* move node by an amount
|
|
* @name move
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
move: function() {
|
|
var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments));
|
|
|
|
var x = this.getX();
|
|
var y = this.getY();
|
|
|
|
if(pos.x !== undefined) {
|
|
x += pos.x;
|
|
}
|
|
|
|
if(pos.y !== undefined) {
|
|
y += pos.y;
|
|
}
|
|
|
|
this.setAttrs({
|
|
x: x,
|
|
y: y
|
|
});
|
|
},
|
|
/**
|
|
* get rotation in degrees
|
|
* @name getRotationDeg
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
getRotationDeg: function() {
|
|
return this.attrs.rotation * 180 / Math.PI;
|
|
},
|
|
/**
|
|
* rotate node by an amount in radians
|
|
* @name rotate
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Number} theta
|
|
*/
|
|
rotate: function(theta) {
|
|
this.setAttrs({
|
|
rotation: this.getRotation() + theta
|
|
});
|
|
},
|
|
/**
|
|
* rotate node by an amount in degrees
|
|
* @name rotateDeg
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Number} deg
|
|
*/
|
|
rotateDeg: function(deg) {
|
|
this.setAttrs({
|
|
rotation: this.getRotation() + (deg * Math.PI / 180)
|
|
});
|
|
},
|
|
/**
|
|
* move node to the top of its siblings
|
|
* @name moveToTop
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
moveToTop: function() {
|
|
var index = this.index;
|
|
this.parent.children.splice(index, 1);
|
|
this.parent.children.push(this);
|
|
this.parent._setChildrenIndices();
|
|
|
|
if(this.nodeType === 'Layer') {
|
|
var stage = this.getStage();
|
|
if(stage) {
|
|
stage.content.removeChild(this.canvas.element);
|
|
stage.content.appendChild(this.canvas.element);
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* move node up
|
|
* @name moveUp
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
moveUp: function() {
|
|
var index = this.index;
|
|
if(index < this.parent.getChildren().length - 1) {
|
|
this.parent.children.splice(index, 1);
|
|
this.parent.children.splice(index + 1, 0, this);
|
|
this.parent._setChildrenIndices();
|
|
|
|
if(this.nodeType === 'Layer') {
|
|
var stage = this.getStage();
|
|
if(stage) {
|
|
stage.content.removeChild(this.canvas.element);
|
|
|
|
if(this.index < stage.getChildren().length - 1) {
|
|
stage.content.insertBefore(this.canvas.element, stage.getChildren()[this.index + 1].canvas.element);
|
|
}
|
|
else {
|
|
stage.content.appendChild(this.canvas.element);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* move node down
|
|
* @name moveDown
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
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();
|
|
|
|
if(this.nodeType === 'Layer') {
|
|
var stage = this.getStage();
|
|
if(stage) {
|
|
var children = stage.getChildren();
|
|
stage.content.removeChild(this.canvas.element);
|
|
stage.content.insertBefore(this.canvas.element, children[this.index + 1].canvas.element);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* move node to the bottom of its siblings
|
|
* @name moveToBottom
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
moveToBottom: function() {
|
|
var index = this.index;
|
|
if(index > 0) {
|
|
this.parent.children.splice(index, 1);
|
|
this.parent.children.unshift(this);
|
|
this.parent._setChildrenIndices();
|
|
|
|
if(this.nodeType === 'Layer') {
|
|
var stage = this.getStage();
|
|
if(stage) {
|
|
var children = stage.getChildren();
|
|
stage.content.removeChild(this.canvas.element);
|
|
stage.content.insertBefore(this.canvas.element, children[1].canvas.element);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* set zIndex
|
|
* @name setZIndex
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Integer} zIndex
|
|
*/
|
|
setZIndex: function(zIndex) {
|
|
var index = this.index;
|
|
this.parent.children.splice(index, 1);
|
|
this.parent.children.splice(zIndex, 0, this);
|
|
this.parent._setChildrenIndices();
|
|
},
|
|
/**
|
|
* get absolute opacity
|
|
* @name getAbsoluteOpacity
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
getAbsoluteOpacity: function() {
|
|
var absOpacity = 1;
|
|
var node = this;
|
|
// traverse upwards
|
|
while(node.nodeType !== 'Stage') {
|
|
absOpacity *= node.attrs.opacity;
|
|
node = node.parent;
|
|
}
|
|
return absOpacity;
|
|
},
|
|
/**
|
|
* determine if node is currently in drag and drop mode
|
|
* @name isDragging
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
isDragging: function() {
|
|
var go = Kinetic.Global;
|
|
return go.drag.node && go.drag.node._id === this._id && go.drag.moving;
|
|
},
|
|
/**
|
|
* move node to another container
|
|
* @name moveTo
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @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
|
|
* @name getParent
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
getParent: function() {
|
|
return this.parent;
|
|
},
|
|
/**
|
|
* get layer that contains the node
|
|
* @name getLayer
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
getLayer: function() {
|
|
if(this.nodeType === 'Layer') {
|
|
return this;
|
|
}
|
|
else {
|
|
return this.getParent().getLayer();
|
|
}
|
|
},
|
|
/**
|
|
* get stage that contains the node
|
|
* @name getStage
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
getStage: function() {
|
|
if(this.nodeType !== 'Stage' && this.getParent()) {
|
|
return this.getParent().getStage();
|
|
}
|
|
else if(this.nodeType === 'Stage') {
|
|
return this;
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
},
|
|
/**
|
|
* simulate event
|
|
* @name simulate
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {String} eventType
|
|
*/
|
|
simulate: function(eventType) {
|
|
this._handleEvent(eventType, {});
|
|
},
|
|
/**
|
|
* transition node to another state. Any property that can accept a real
|
|
* number can be transitioned, including x, y, rotation, opacity, strokeWidth,
|
|
* radius, scale.x, scale.y, offset.x, offset.y, etc.
|
|
* @name transitionTo
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @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) {
|
|
/*
|
|
* create new transition
|
|
*/
|
|
var node = this.nodeType === 'Stage' ? this : this.getLayer();
|
|
var that = this;
|
|
var trans = new Kinetic.Transition(this, config);
|
|
|
|
this.transAnim.func = function() {
|
|
trans._onEnterFrame();
|
|
};
|
|
this.transAnim.node = node;
|
|
|
|
// subscribe to onFinished for first tween
|
|
trans.onFinished = function() {
|
|
// remove animation
|
|
that.transAnim.stop();
|
|
that.transAnim.node.draw();
|
|
|
|
// callback
|
|
if(config.callback) {
|
|
config.callback();
|
|
}
|
|
};
|
|
// auto start
|
|
trans.start();
|
|
this.transAnim.start();
|
|
return trans;
|
|
},
|
|
/**
|
|
* get absolute transform of the node which takes into
|
|
* account its parent transforms
|
|
* @name getAbsoluteTransform
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
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
|
|
* @name getTransform
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
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);
|
|
}
|
|
if(this.attrs.offset && (this.attrs.offset.x !== 0 || this.attrs.offset.y !== 0)) {
|
|
m.translate(-1 * this.attrs.offset.x, -1 * this.attrs.offset.y);
|
|
}
|
|
|
|
return m;
|
|
},
|
|
/**
|
|
* clone node
|
|
* @name clone
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Object} attrs override attrs
|
|
*/
|
|
clone: function(obj) {
|
|
// instantiate new node
|
|
var classType = this.shapeType || this.nodeType;
|
|
var node = new Kinetic[classType](this.attrs);
|
|
|
|
/*
|
|
* copy over user listeners
|
|
*/
|
|
for(var key in this.eventListeners) {
|
|
var allListeners = this.eventListeners[key];
|
|
for(var n = 0; n < allListeners.length; n++) {
|
|
var listener = allListeners[n];
|
|
/*
|
|
* don't include kinetic namespaced listeners because
|
|
* these are generated by the constructors
|
|
*/
|
|
if(listener.name.indexOf('kinetic') < 0) {
|
|
// if listeners array doesn't exist, then create it
|
|
if(!node.eventListeners[key]) {
|
|
node.eventListeners[key] = [];
|
|
}
|
|
node.eventListeners[key].push(listener);
|
|
}
|
|
}
|
|
}
|
|
|
|
// apply attr overrides
|
|
node.setAttrs(obj);
|
|
return node;
|
|
},
|
|
/**
|
|
* Creates a composite data URL. If MIME type is not
|
|
* specified, then "image/png" will result. For "image/jpeg", specify a quality
|
|
* level as quality (range 0.0 - 1.0)
|
|
* @name toDataURL
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Object} config
|
|
* @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg".
|
|
* "image/png" is the default
|
|
* @param {Number} [config.width] data url image width
|
|
* @param {Number} [config.height] data url image height
|
|
* @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType,
|
|
* you can specify the quality from 0 to 1, where 0 is very poor quality and 1
|
|
* is very high quality
|
|
*/
|
|
toDataURL: function(config) {
|
|
var mimeType = config && config.mimeType ? config.mimeType : null;
|
|
var quality = config && config.quality ? config.quality : null;
|
|
var canvas;
|
|
if(config && config.width && config.height) {
|
|
canvas = new Kinetic.Canvas(config.width, config.height);
|
|
}
|
|
else {
|
|
canvas = this.getStage().bufferCanvas;
|
|
}
|
|
|
|
var context = canvas.getContext();
|
|
canvas.clear();
|
|
this._draw(canvas);
|
|
return canvas.toDataURL(mimeType, quality);
|
|
},
|
|
/**
|
|
* converts node into an image. Since the toImage
|
|
* method is asynchronous, a callback is required
|
|
* @name toImage
|
|
* @methodOf Kinetic.Stage.prototype
|
|
* @param {Object} config
|
|
* @param {Function} callback since the toImage() method is asynchonrous, the
|
|
* resulting image object is passed into the callback function
|
|
* @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg".
|
|
* "image/png" is the default
|
|
* @param {Number} [config.width] data url image width
|
|
* @param {Number} [config.height] data url image height
|
|
* @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType,
|
|
* you can specify the quality from 0 to 1, where 0 is very poor quality and 1
|
|
* is very high quality
|
|
*/
|
|
toImage: function(config) {
|
|
Kinetic.Type._getImage(this.toDataURL(config), function(img) {
|
|
config.callback(img);
|
|
});
|
|
},
|
|
_clearTransform: function() {
|
|
var trans = {
|
|
x: this.attrs.x,
|
|
y: this.attrs.y,
|
|
rotation: this.attrs.rotation,
|
|
scale: {
|
|
x: this.attrs.scale.x,
|
|
y: this.attrs.scale.y
|
|
},
|
|
offset: {
|
|
x: this.attrs.offset.x,
|
|
y: this.attrs.offset.y
|
|
}
|
|
};
|
|
|
|
this.attrs.x = 0;
|
|
this.attrs.y = 0;
|
|
this.attrs.rotation = 0;
|
|
this.attrs.scale = {
|
|
x: 1,
|
|
y: 1
|
|
};
|
|
this.attrs.offset = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
|
|
return trans;
|
|
},
|
|
_setTransform: function(trans) {
|
|
for(var key in trans) {
|
|
this.attrs[key] = trans[key];
|
|
}
|
|
},
|
|
_fireBeforeChangeEvent: function(attr, oldVal, newVal) {
|
|
this._handleEvent('before' + attr.toUpperCase() + 'Change', {
|
|
oldVal: oldVal,
|
|
newVal: newVal
|
|
});
|
|
},
|
|
_fireChangeEvent: function(attr, oldVal, newVal) {
|
|
this._handleEvent(attr + 'Change', {
|
|
oldVal: oldVal,
|
|
newVal: newVal
|
|
});
|
|
},
|
|
_setAttr: function(obj, attr, val) {
|
|
if(val !== undefined) {
|
|
if(obj === undefined) {
|
|
obj = {};
|
|
}
|
|
obj[attr] = val;
|
|
}
|
|
},
|
|
_listenDrag: function() {
|
|
this._dragCleanup();
|
|
var go = Kinetic.Global;
|
|
var that = this;
|
|
this.on('mousedown.kinetic touchstart.kinetic', function(evt) {
|
|
that._initDrag();
|
|
});
|
|
},
|
|
_initDrag: function() {
|
|
var go = Kinetic.Global;
|
|
var stage = this.getStage();
|
|
var pos = stage.getUserPosition();
|
|
|
|
if(pos) {
|
|
var m = this.getTransform().getTranslation();
|
|
var am = this.getAbsoluteTransform().getTranslation();
|
|
var ap = this.getAbsolutePosition();
|
|
go.drag.node = this;
|
|
go.drag.offset.x = pos.x - ap.x;
|
|
go.drag.offset.y = pos.y - ap.y;
|
|
|
|
/*
|
|
* if dragging and dropping the stage,
|
|
* draw all of the layers
|
|
*/
|
|
if(this.nodeType === 'Stage') {
|
|
stage.dragAnim.node = this;
|
|
}
|
|
else {
|
|
stage.dragAnim.node = this.getLayer();
|
|
}
|
|
stage.dragAnim.start();
|
|
}
|
|
},
|
|
_onDraggableChange: function() {
|
|
if(this.attrs.draggable) {
|
|
this._listenDrag();
|
|
}
|
|
else {
|
|
// remove event listeners
|
|
this._dragCleanup();
|
|
|
|
/*
|
|
* force drag and drop to end
|
|
* if this node is currently in
|
|
* drag and drop mode
|
|
*/
|
|
var stage = this.getStage();
|
|
var go = Kinetic.Global;
|
|
if(stage && go.drag.node && go.drag.node._id === this._id) {
|
|
stage._endDrag();
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* remove drag and drop event listener
|
|
*/
|
|
_dragCleanup: function() {
|
|
this.off('mousedown.kinetic');
|
|
this.off('touchstart.kinetic');
|
|
},
|
|
/**
|
|
* handle node event
|
|
*/
|
|
_handleEvent: function(eventType, evt, compareShape) {
|
|
if(this.nodeType === 'Shape') {
|
|
evt.shape = this;
|
|
}
|
|
var stage = this.getStage();
|
|
var el = this.eventListeners;
|
|
var okayToRun = true;
|
|
|
|
if(eventType === 'mouseover' && compareShape && this._id === compareShape._id) {
|
|
okayToRun = false;
|
|
}
|
|
else if(eventType === 'mouseout' && compareShape && this._id === compareShape._id) {
|
|
okayToRun = false;
|
|
}
|
|
|
|
if(okayToRun) {
|
|
if(el[eventType]) {
|
|
var events = el[eventType];
|
|
for(var i = 0; i < events.length; i++) {
|
|
events[i].handler.apply(this, [evt]);
|
|
}
|
|
}
|
|
|
|
// simulate event bubbling
|
|
if(Kinetic.Global.BUBBLE_WHITELIST.indexOf(eventType) >= 0 && !evt.cancelBubble && this.parent) {
|
|
if(compareShape && compareShape.parent) {
|
|
this._handleEvent.call(this.parent, eventType, evt, compareShape.parent);
|
|
}
|
|
else {
|
|
this._handleEvent.call(this.parent, eventType, evt);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
_draw: function(canvas) {
|
|
if(this.isVisible() && (!canvas || canvas.name !== 'buffer' || this.getListening())) {
|
|
if(this.__draw) {
|
|
this.__draw(canvas);
|
|
}
|
|
|
|
var children = this.children;
|
|
if(children) {
|
|
for(var n = 0; n < children.length; n++) {
|
|
var child = children[n];
|
|
if(child.draw) {
|
|
child.draw(canvas);
|
|
}
|
|
else {
|
|
child._draw(canvas);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
// add getter and setter methods
|
|
Kinetic.Node.addSetters = function(constructor, arr) {
|
|
for(var n = 0; n < arr.length; n++) {
|
|
var attr = arr[n];
|
|
this._addSetter(constructor, attr);
|
|
}
|
|
};
|
|
Kinetic.Node.addGetters = function(constructor, arr) {
|
|
for(var n = 0; n < arr.length; n++) {
|
|
var attr = arr[n];
|
|
this._addGetter(constructor, attr);
|
|
}
|
|
};
|
|
Kinetic.Node.addGettersSetters = function(constructor, arr) {
|
|
this.addSetters(constructor, arr);
|
|
this.addGetters(constructor, arr);
|
|
};
|
|
Kinetic.Node._addSetter = function(constructor, attr) {
|
|
var that = this;
|
|
var method = 'set' + attr.charAt(0).toUpperCase() + attr.slice(1);
|
|
constructor.prototype[method] = function() {
|
|
if(arguments.length == 1) {
|
|
arg = arguments[0];
|
|
}
|
|
else {
|
|
arg = Array.prototype.slice.call(arguments);
|
|
}
|
|
var obj = {};
|
|
obj[attr] = arg;
|
|
this.setAttrs(obj);
|
|
};
|
|
};
|
|
Kinetic.Node._addGetter = function(constructor, attr) {
|
|
var that = this;
|
|
var method = 'get' + attr.charAt(0).toUpperCase() + attr.slice(1);
|
|
constructor.prototype[method] = function(arg) {
|
|
return this.attrs[attr];
|
|
};
|
|
};
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Node, ['x', 'y', 'scale', 'rotation', 'opacity', 'name', 'id', 'offset', 'draggable', 'dragConstraint', 'dragBounds', 'listening']);
|
|
Kinetic.Node.addSetters(Kinetic.Node, ['rotationDeg']);
|
|
|
|
/**
|
|
* set node x position
|
|
* @name setX
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Number} x
|
|
*/
|
|
|
|
/**
|
|
* set node y position
|
|
* @name setY
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Number} y
|
|
*/
|
|
|
|
/**
|
|
* set node rotation in radians
|
|
* @name setRotation
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Number} theta
|
|
*/
|
|
|
|
/**
|
|
* set opacity. Opacity values range from 0 to 1.
|
|
* A node with an opacity of 0 is fully transparent, and a node
|
|
* with an opacity of 1 is fully opaque
|
|
* @name setOpacity
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Object} opacity
|
|
*/
|
|
|
|
/**
|
|
* set draggable
|
|
* @name setDraggable
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {String} draggable
|
|
*/
|
|
|
|
/**
|
|
* set drag constraint.
|
|
* @name setDragConstraint
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {String} constraint can be vertical, horizontal, or none
|
|
*/
|
|
|
|
/**
|
|
* set drag bounds.
|
|
* @name setDragBounds
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @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
|
|
*/
|
|
|
|
/**
|
|
* listen or don't listen to events
|
|
* @name setListening
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Boolean} listening
|
|
*/
|
|
|
|
/**
|
|
* set node rotation in degrees
|
|
* @name setRotationDeg
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Number} deg
|
|
*/
|
|
|
|
/**
|
|
* set offset. A node's offset defines the positition and rotation point
|
|
* @name setOffset
|
|
* @methodOf Kinetic.Node.prototype
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
|
|
/**
|
|
* set node scale.
|
|
* @name setScale
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* get scale
|
|
* @name getScale
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* get node x position
|
|
* @name getX
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* get node y position
|
|
* @name getY
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* get rotation in radians
|
|
* @name getRotation
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* get opacity.
|
|
* @name getOpacity
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* get name
|
|
* @name getName
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* get id
|
|
* @name getId
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* get offset
|
|
* @name getOffset
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* get draggable
|
|
* @name getDraggable
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* get drag constraint
|
|
* @name getDragConstraint
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* get drag bounds
|
|
* @name getDragBounds
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
/**
|
|
* determine if listening to events or not
|
|
* @name getListening
|
|
* @methodOf Kinetic.Node.prototype
|
|
*/
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Container
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Container constructor. Containers are used to contain nodes or other containers
|
|
* @constructor
|
|
* @augments Kinetic.Node
|
|
* @param {Object} config
|
|
* @param {Number} [config.x]
|
|
* @param {Number} [config.y]
|
|
* @param {Boolean} [config.visible]
|
|
* @param {Boolean} [config.listening] whether or not the node is listening for events
|
|
* @param {String} [config.id] unique id
|
|
* @param {String} [config.name] non-unique name
|
|
* @param {Number} [config.alpha] determines node opacity. Can be any number between 0 and 1
|
|
* @param {Object} [config.scale]
|
|
* @param {Number} [config.scale.x]
|
|
* @param {Number} [config.scale.y]
|
|
* @param {Number} [config.rotation] rotation in radians
|
|
* @param {Number} [config.rotationDeg] rotation in degrees
|
|
* @param {Object} [config.offset] offsets default position point and rotation point
|
|
* @param {Number} [config.offset.x]
|
|
* @param {Number} [config.offset.y]
|
|
* @param {Boolean} [config.draggable]
|
|
* @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default
|
|
* is none
|
|
* @param {Object} [config.dragBounds]
|
|
* @param {Number} [config.dragBounds.top]
|
|
* @param {Number} [config.dragBounds.right]
|
|
* @param {Number} [config.dragBounds.bottom]
|
|
* @param {Number} [config.dragBounds.left]
|
|
*/
|
|
Kinetic.Container = function(config) {
|
|
this._containerInit(config);
|
|
};
|
|
|
|
Kinetic.Container.prototype = {
|
|
_containerInit: function(config) {
|
|
this.children = [];
|
|
Kinetic.Node.call(this, config);
|
|
},
|
|
/**
|
|
* get children
|
|
* @name getChildren
|
|
* @methodOf Kinetic.Container.prototype
|
|
*/
|
|
getChildren: function() {
|
|
return this.children;
|
|
},
|
|
/**
|
|
* remove all children
|
|
* @name removeChildren
|
|
* @methodOf Kinetic.Container.prototype
|
|
*/
|
|
removeChildren: function() {
|
|
while(this.children.length > 0) {
|
|
this.remove(this.children[0]);
|
|
}
|
|
},
|
|
/**
|
|
* add node to container
|
|
* @name add
|
|
* @methodOf Kinetic.Container.prototype
|
|
* @param {Node} child
|
|
*/
|
|
add: function(child) {
|
|
child._id = Kinetic.Global.idCounter++;
|
|
child.index = this.children.length;
|
|
child.parent = this;
|
|
|
|
this.children.push(child);
|
|
var stage = child.getStage();
|
|
|
|
if(!stage) {
|
|
Kinetic.Global._addTempNode(child);
|
|
}
|
|
else {
|
|
stage._addId(child);
|
|
stage._addName(child);
|
|
|
|
/*
|
|
* pull in other nodes that are now linked
|
|
* to a stage
|
|
*/
|
|
var go = Kinetic.Global;
|
|
go._pullNodes(stage);
|
|
}
|
|
|
|
// do extra stuff if needed
|
|
if(this._add !== undefined) {
|
|
this._add(child);
|
|
}
|
|
|
|
// chainable
|
|
return this;
|
|
},
|
|
/**
|
|
* remove child from container
|
|
* @name remove
|
|
* @methodOf Kinetic.Container.prototype
|
|
* @param {Node} child
|
|
*/
|
|
remove: function(child) {
|
|
if(child && child.index !== undefined && this.children[child.index]._id == child._id) {
|
|
var stage = this.getStage();
|
|
/*
|
|
* remove event listeners and references to the node
|
|
* from the ids and names hashes
|
|
*/
|
|
if(stage) {
|
|
stage._removeId(child.getId());
|
|
stage._removeName(child.getName(), child._id);
|
|
}
|
|
|
|
Kinetic.Global._removeTempNode(child);
|
|
this.children.splice(child.index, 1);
|
|
this._setChildrenIndices();
|
|
|
|
// remove children
|
|
while(child.children && child.children.length > 0) {
|
|
child.remove(child.children[0]);
|
|
}
|
|
|
|
// do extra stuff if needed
|
|
if(child._remove !== undefined) {
|
|
child._remove();
|
|
}
|
|
}
|
|
|
|
// chainable
|
|
return this;
|
|
},
|
|
/**
|
|
* 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
|
|
* @name get
|
|
* @methodOf Kinetic.Container.prototype
|
|
* @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 = new Kinetic.Collection();
|
|
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
|
|
* @name isAncestorOf
|
|
* @methodOf Kinetic.Container.prototype
|
|
* @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;
|
|
},
|
|
/**
|
|
* clone node
|
|
* @name clone
|
|
* @methodOf Kinetic.Container.prototype
|
|
* @param {Object} attrs override attrs
|
|
*/
|
|
clone: function(obj) {
|
|
// call super method
|
|
var node = Kinetic.Node.prototype.clone.call(this, obj)
|
|
|
|
// perform deep clone on containers
|
|
for(var key in this.children) {
|
|
node.add(this.children[key].clone());
|
|
}
|
|
return node;
|
|
},
|
|
/**
|
|
* get shapes that intersect a point
|
|
* @name getIntersections
|
|
* @methodOf Kinetic.Container.prototype
|
|
* @param {Object} point
|
|
*/
|
|
getIntersections: function() {
|
|
var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments));
|
|
var arr = [];
|
|
var shapes = this.get('Shape');
|
|
|
|
for(var n = 0; n < shapes.length; n++) {
|
|
var shape = shapes[n];
|
|
if(shape.isVisible() && shape.intersects(pos)) {
|
|
arr.push(shape);
|
|
}
|
|
}
|
|
|
|
return arr;
|
|
},
|
|
/**
|
|
* get all shapes inside container
|
|
*/
|
|
_getNodes: function(sel) {
|
|
var arr = new Kinetic.Collection();
|
|
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;
|
|
},
|
|
/**
|
|
* set children indices
|
|
*/
|
|
_setChildrenIndices: function() {
|
|
for(var n = 0; n < this.children.length; n++) {
|
|
this.children[n].index = n;
|
|
}
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Container, Kinetic.Node);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Stage
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Stage constructor. A stage is used to contain multiple layers
|
|
* @constructor
|
|
* @augments Kinetic.Container
|
|
* @param {Object} config
|
|
* @param {String|DomElement} config.container Container id or DOM element
|
|
* @param {Number} config.width
|
|
* @param {Number} config.height
|
|
* @param {Number} [config.x]
|
|
* @param {Number} [config.y]
|
|
* @param {Boolean} [config.visible]
|
|
* @param {Boolean} [config.listening] whether or not the node is listening for events
|
|
* @param {String} [config.id] unique id
|
|
* @param {String} [config.name] non-unique name
|
|
* @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1
|
|
* @param {Object} [config.scale]
|
|
* @param {Number} [config.scale.x]
|
|
* @param {Number} [config.scale.y]
|
|
* @param {Number} [config.rotation] rotation in radians
|
|
* @param {Number} [config.rotationDeg] rotation in degrees
|
|
* @param {Object} [config.offset] offsets default position point and rotation point
|
|
* @param {Number} [config.offset.x]
|
|
* @param {Number} [config.offset.y]
|
|
* @param {Boolean} [config.draggable]
|
|
* @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default
|
|
* is none
|
|
* @param {Object} [config.dragBounds]
|
|
* @param {Number} [config.dragBounds.top]
|
|
* @param {Number} [config.dragBounds.right]
|
|
* @param {Number} [config.dragBounds.bottom]
|
|
* @param {Number} [config.dragBounds.left]
|
|
*/
|
|
Kinetic.Stage = function(config) {
|
|
this._initStage(config);
|
|
};
|
|
|
|
Kinetic.Stage.prototype = {
|
|
_initStage: function(config) {
|
|
this.setDefaultAttrs({
|
|
width: 400,
|
|
height: 200
|
|
});
|
|
|
|
/*
|
|
* 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 constructor
|
|
Kinetic.Container.call(this, config);
|
|
|
|
this._setStageDefaultProperties();
|
|
this._id = Kinetic.Global.idCounter++;
|
|
this._buildDOM();
|
|
this._bindContentEvents();
|
|
|
|
//change events
|
|
this.on('widthChange.kinetic', function() {
|
|
this._resizeDOM();
|
|
});
|
|
|
|
this.on('heightChange.kinetic', function() {
|
|
this._resizeDOM();
|
|
});
|
|
var go = Kinetic.Global;
|
|
go.stages.push(this);
|
|
this._addId(this);
|
|
this._addName(this);
|
|
|
|
},
|
|
/**
|
|
* draw children
|
|
* @name draw
|
|
* @methodOf Kinetic.Stage.prototype
|
|
*/
|
|
draw: function() {
|
|
this._draw();
|
|
},
|
|
/**
|
|
* set stage size
|
|
* @name setSize
|
|
* @methodOf Kinetic.Stage.prototype
|
|
* @param {Number} width
|
|
* @param {Number} height
|
|
*/
|
|
setSize: function() {
|
|
// set stage dimensions
|
|
var size = Kinetic.Type._getSize(Array.prototype.slice.call(arguments));
|
|
this.setAttrs(size);
|
|
},
|
|
/**
|
|
* get stage size
|
|
* @name getSize
|
|
* @methodOf Kinetic.Stage.prototype
|
|
*/
|
|
getSize: function() {
|
|
return {
|
|
width: this.attrs.width,
|
|
height: this.attrs.height
|
|
};
|
|
},
|
|
/**
|
|
* clear all layers
|
|
* @name clear
|
|
* @methodOf Kinetic.Stage.prototype
|
|
*/
|
|
clear: function() {
|
|
var layers = this.children;
|
|
for(var n = 0; n < layers.length; n++) {
|
|
layers[n].clear();
|
|
}
|
|
},
|
|
/**
|
|
* serialize stage and children as a JSON object and return
|
|
* the result as a json string
|
|
* @name toJSON
|
|
* @methodOf Kinetic.Stage.prototype
|
|
*/
|
|
toJSON: function() {
|
|
var type = Kinetic.Type;
|
|
|
|
function addNode(node) {
|
|
var obj = {};
|
|
|
|
obj.attrs = {};
|
|
|
|
// serialize only attributes that are not function, image, DOM, or objects with methods
|
|
for(var key in node.attrs) {
|
|
var val = node.attrs[key];
|
|
if(!type._isFunction(val) && !type._isElement(val) && !type._hasMethods(val)) {
|
|
obj.attrs[key] = val;
|
|
}
|
|
}
|
|
|
|
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
|
|
* @name reset
|
|
* @methodOf Kinetic.Stage.prototype
|
|
*/
|
|
reset: function() {
|
|
// remove children
|
|
this.removeChildren();
|
|
|
|
// defaults
|
|
this._setStageDefaultProperties();
|
|
this.setAttrs(this.defaultNodeAttrs);
|
|
},
|
|
/**
|
|
* 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()
|
|
* @name load
|
|
* @methodOf Kinetic.Stage.prototype
|
|
* @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();
|
|
},
|
|
/**
|
|
* get mouse position for desktop apps
|
|
* @name getMousePosition
|
|
* @methodOf Kinetic.Stage.prototype
|
|
* @param {Event} evt
|
|
*/
|
|
getMousePosition: function(evt) {
|
|
return this.mousePos;
|
|
},
|
|
/**
|
|
* get touch position for mobile apps
|
|
* @name getTouchPosition
|
|
* @methodOf Kinetic.Stage.prototype
|
|
* @param {Event} evt
|
|
*/
|
|
getTouchPosition: function(evt) {
|
|
return this.touchPos;
|
|
},
|
|
/**
|
|
* get user position (mouse position or touch position)
|
|
* @name getUserPosition
|
|
* @methodOf Kinetic.Stage.prototype
|
|
* @param {Event} evt
|
|
*/
|
|
getUserPosition: function(evt) {
|
|
return this.getTouchPosition() || this.getMousePosition();
|
|
},
|
|
/**
|
|
* get container DOM element
|
|
* @name getContainer
|
|
* @methodOf Kinetic.Stage.prototype
|
|
*/
|
|
getContainer: function() {
|
|
return this.attrs.container;
|
|
},
|
|
/**
|
|
* get stage
|
|
* @name getStage
|
|
* @methodOf Kinetic.Stage.prototype
|
|
*/
|
|
getStage: function() {
|
|
return this;
|
|
},
|
|
/**
|
|
* get stage DOM node, which is a div element
|
|
* with the class name "kineticjs-content"
|
|
* @name getDOM
|
|
* @methodOf Kinetic.Stage.prototype
|
|
*/
|
|
getDOM: function() {
|
|
return this.content;
|
|
},
|
|
/**
|
|
* Creates a composite data URL and requires a callback because the stage
|
|
* toDataURL method is asynchronous. 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). Note that this method works
|
|
* differently from toDataURL() for other nodes because it generates an absolute dataURL
|
|
* based on what's draw onto the canvases for each layer, rather than drawing
|
|
* the current state of each node
|
|
* @name toDataURL
|
|
* @methodOf Kinetic.Stage.prototype
|
|
* @param {Object} config
|
|
* @param {Function} config.callback since the stage toDataURL() method is asynchronous,
|
|
* the data url string will be passed into the callback
|
|
* @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg".
|
|
* "image/png" is the default
|
|
* @param {Number} [config.width] data url image width
|
|
* @param {Number} [config.height] data url image height
|
|
* @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType,
|
|
* you can specify the quality from 0 to 1, where 0 is very poor quality and 1
|
|
* is very high quality
|
|
*/
|
|
toDataURL: function(config) {
|
|
var mimeType = config && config.mimeType ? config.mimeType : null;
|
|
var quality = config && config.quality ? config.quality : null;
|
|
/*
|
|
* need to create a canvas element rather than using the buffer canvas
|
|
* because this method is asynchonous which means that other parts of the
|
|
* code could modify the buffer canvas before it's finished
|
|
*/
|
|
var width = config && config.width ? config.width : this.attrs.width;
|
|
var height = config && config.height ? config.height : this.attrs.height;
|
|
var canvas = new Kinetic.Canvas(width, height);
|
|
var context = canvas.getContext();
|
|
var layers = this.children;
|
|
|
|
function drawLayer(n) {
|
|
var layer = layers[n];
|
|
var layerUrl = layer.getCanvas().toDataURL();
|
|
var imageObj = new Image();
|
|
imageObj.onload = function() {
|
|
context.drawImage(imageObj, 0, 0);
|
|
|
|
if(n < layers.length - 1) {
|
|
drawLayer(n + 1);
|
|
}
|
|
else {
|
|
config.callback(canvas.toDataURL(mimeType, quality));
|
|
}
|
|
};
|
|
imageObj.src = layerUrl;
|
|
}
|
|
drawLayer(0);
|
|
},
|
|
/**
|
|
* converts stage into an image. Since the stage toImage() method
|
|
* is asynchronous, a callback function is required
|
|
* @name toImage
|
|
* @methodOf Kinetic.Stage.prototype
|
|
* @param {Object} config
|
|
* @param {Function} callback since the toImage() method is asynchonrous, the
|
|
* resulting image object is passed into the callback function
|
|
* @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg".
|
|
* "image/png" is the default
|
|
* @param {Number} [config.width] data url image width
|
|
* @param {Number} [config.height] data url image height
|
|
* @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType,
|
|
* you can specify the quality from 0 to 1, where 0 is very poor quality and 1
|
|
* is very high quality
|
|
*/
|
|
toImage: function(config) {
|
|
this.toDataURL({
|
|
callback: function(dataUrl) {
|
|
Kinetic.Type._getImage(dataUrl, function(img) {
|
|
config.callback(img);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
/**
|
|
* get intersection object that contains shape and pixel data
|
|
* @name getIntersection
|
|
* @methodOf Kinetic.Stage.prototype
|
|
* @param {Object} pos point object
|
|
*/
|
|
getIntersection: function(pos) {
|
|
var shape;
|
|
var layers = this.getChildren();
|
|
|
|
/*
|
|
* traverse through layers from top to bottom and look
|
|
* for hit detection
|
|
*/
|
|
for(var n = layers.length - 1; n >= 0; n--) {
|
|
var layer = layers[n];
|
|
var p = layer.bufferCanvas.context.getImageData(Math.round(pos.x), Math.round(pos.y), 1, 1).data;
|
|
// this indicates that a buffer pixel may have been found
|
|
if(p[3] === 255) {
|
|
var colorKey = Kinetic.Type._rgbToHex(p[0], p[1], p[2]);
|
|
shape = Kinetic.Global.shapes[colorKey];
|
|
return {
|
|
shape: shape,
|
|
pixel: p
|
|
};
|
|
}
|
|
// if no shape mapped to that pixel, return pixel array
|
|
else if(p[0] > 0 || p[1] > 0 || p[2] > 0 || p[3] > 0) {
|
|
return {
|
|
pixel: p
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
_resizeDOM: function() {
|
|
var width = this.attrs.width;
|
|
var height = this.attrs.height;
|
|
|
|
// set content dimensions
|
|
this.content.style.width = width + 'px';
|
|
this.content.style.height = height + 'px';
|
|
|
|
this.bufferCanvas.setSize(width, height);
|
|
// set user defined layer dimensions
|
|
var layers = this.children;
|
|
for(var n = 0; n < layers.length; n++) {
|
|
var layer = layers[n];
|
|
layer.getCanvas().setSize(width, height);
|
|
layer.bufferCanvas.setSize(width, height);
|
|
layer.draw();
|
|
}
|
|
},
|
|
/**
|
|
* add layer to stage
|
|
* @param {Layer} layer
|
|
*/
|
|
_add: function(layer) {
|
|
layer.canvas.setSize(this.attrs.width, this.attrs.height);
|
|
layer.bufferCanvas.setSize(this.attrs.width, this.attrs.height);
|
|
|
|
// draw layer and append canvas to container
|
|
layer.draw();
|
|
this.content.appendChild(layer.canvas.element);
|
|
},
|
|
_setUserPosition: function(evt) {
|
|
if(!evt) {
|
|
evt = window.event;
|
|
}
|
|
this._setMousePosition(evt);
|
|
this._setTouchPosition(evt);
|
|
},
|
|
/**
|
|
* begin listening for events by adding event handlers
|
|
* to the container
|
|
*/
|
|
_bindContentEvents: function() {
|
|
var go = Kinetic.Global;
|
|
var that = this;
|
|
var events = ['mousedown', 'mousemove', 'mouseup', 'mouseout', 'touchstart', 'touchmove', 'touchend'];
|
|
|
|
for(var n = 0; n < events.length; n++) {
|
|
var pubEvent = events[n];
|
|
// induce scope
|
|
( function() {
|
|
var event = pubEvent;
|
|
that.content.addEventListener(event, function(evt) {
|
|
that['_' + event](evt);
|
|
}, false);
|
|
}());
|
|
}
|
|
},
|
|
_mouseout: function(evt) {
|
|
this._setUserPosition(evt);
|
|
var go = Kinetic.Global;
|
|
// if there's a current target shape, run mouseout handlers
|
|
var targetShape = this.targetShape;
|
|
if(targetShape && !go.drag.moving) {
|
|
targetShape._handleEvent('mouseout', evt);
|
|
this.targetShape = null;
|
|
}
|
|
this.mousePos = undefined;
|
|
|
|
// end drag and drop
|
|
this._endDrag(evt);
|
|
},
|
|
_mousemove: function(evt) {
|
|
this._setUserPosition(evt);
|
|
var go = Kinetic.Global;
|
|
var obj = this.getIntersection(this.getUserPosition());
|
|
|
|
if(obj) {
|
|
var shape = obj.shape;
|
|
if(shape) {
|
|
if(!go.drag.moving && obj.pixel[3] === 255 && (!this.targetShape || this.targetShape._id !== shape._id)) {
|
|
if(this.targetShape) {
|
|
this.targetShape._handleEvent('mouseout', evt, shape);
|
|
}
|
|
shape._handleEvent('mouseover', evt, this.targetShape);
|
|
this.targetShape = shape;
|
|
}
|
|
else {
|
|
shape._handleEvent('mousemove', evt);
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
* if no shape was detected, clear target shape and try
|
|
* to run mouseout from previous target shape
|
|
*/
|
|
else if(this.targetShape && !go.drag.moving) {
|
|
this.targetShape._handleEvent('mouseout', evt);
|
|
this.targetShape = null;
|
|
}
|
|
|
|
// start drag and drop
|
|
this._startDrag(evt);
|
|
},
|
|
_mousedown: function(evt) {
|
|
this._setUserPosition(evt);
|
|
var obj = this.getIntersection(this.getUserPosition());
|
|
if(obj && obj.shape) {
|
|
var shape = obj.shape;
|
|
this.clickStart = true;
|
|
shape._handleEvent('mousedown', evt);
|
|
}
|
|
|
|
//init stage drag and drop
|
|
if(this.attrs.draggable) {
|
|
this._initDrag();
|
|
}
|
|
},
|
|
_mouseup: function(evt) {
|
|
this._setUserPosition(evt);
|
|
var go = Kinetic.Global;
|
|
var obj = this.getIntersection(this.getUserPosition());
|
|
var that = this;
|
|
if(obj && obj.shape) {
|
|
var shape = obj.shape;
|
|
shape._handleEvent('mouseup', 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._handleEvent('click', evt);
|
|
|
|
if(this.inDoubleClickWindow) {
|
|
shape._handleEvent('dblclick', evt);
|
|
}
|
|
this.inDoubleClickWindow = true;
|
|
setTimeout(function() {
|
|
that.inDoubleClickWindow = false;
|
|
}, this.dblClickWindow);
|
|
}
|
|
}
|
|
}
|
|
this.clickStart = false;
|
|
|
|
// end drag and drop
|
|
this._endDrag(evt);
|
|
},
|
|
_touchstart: function(evt) {
|
|
this._setUserPosition(evt);
|
|
evt.preventDefault();
|
|
var obj = this.getIntersection(this.getUserPosition());
|
|
|
|
if(obj && obj.shape) {
|
|
var shape = obj.shape;
|
|
this.tapStart = true;
|
|
shape._handleEvent('touchstart', evt);
|
|
}
|
|
|
|
/*
|
|
* init stage drag and drop
|
|
*/
|
|
if(this.attrs.draggable) {
|
|
this._initDrag();
|
|
}
|
|
},
|
|
_touchend: function(evt) {
|
|
this._setUserPosition(evt);
|
|
var go = Kinetic.Global;
|
|
var obj = this.getIntersection(this.getUserPosition());
|
|
var that = this;
|
|
if(obj && obj.shape) {
|
|
var shape = obj.shape;
|
|
shape._handleEvent('touchend', evt);
|
|
|
|
// detect if tap or double tap occurred
|
|
if(this.tapStart) {
|
|
/*
|
|
* if dragging and dropping, don't fire tap or dbltap
|
|
* event
|
|
*/
|
|
if((!go.drag.moving) || !go.drag.node) {
|
|
shape._handleEvent('tap', evt);
|
|
|
|
if(this.inDoubleClickWindow) {
|
|
shape._handleEvent('dbltap', evt);
|
|
}
|
|
this.inDoubleClickWindow = true;
|
|
setTimeout(function() {
|
|
that.inDoubleClickWindow = false;
|
|
}, this.dblClickWindow);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.tapStart = false;
|
|
|
|
// end drag and drop
|
|
this._endDrag(evt);
|
|
},
|
|
_touchmove: function(evt) {
|
|
this._setUserPosition(evt);
|
|
evt.preventDefault();
|
|
var obj = this.getIntersection(this.getUserPosition());
|
|
if(obj && obj.shape) {
|
|
var shape = obj.shape;
|
|
shape._handleEvent('touchmove', evt);
|
|
}
|
|
|
|
// start drag and drop
|
|
this._startDrag(evt);
|
|
},
|
|
/**
|
|
* set mouse positon for desktop apps
|
|
* @param {Event} evt
|
|
*/
|
|
_setMousePosition: function(evt) {
|
|
var mouseX = evt.clientX - this._getContentPosition().left;
|
|
var mouseY = evt.clientY - this._getContentPosition().top;
|
|
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) {
|
|
// one finger
|
|
var touch = evt.touches[0];
|
|
// Get the information for finger #1
|
|
var touchX = touch.clientX - this._getContentPosition().left;
|
|
var touchY = touch.clientY - this._getContentPosition().top;
|
|
|
|
this.touchPos = {
|
|
x: touchX,
|
|
y: touchY
|
|
};
|
|
}
|
|
},
|
|
/**
|
|
* get container position
|
|
*/
|
|
_getContentPosition: function() {
|
|
var rect = this.content.getBoundingClientRect();
|
|
return {
|
|
top: rect.top,
|
|
left: rect.left
|
|
};
|
|
},
|
|
/**
|
|
* end drag and drop
|
|
*/
|
|
_endDrag: function(evt) {
|
|
var go = Kinetic.Global;
|
|
var node = go.drag.node;
|
|
if(node) {
|
|
if(node.nodeType === 'Stage') {
|
|
node.draw();
|
|
}
|
|
else {
|
|
node.getLayer().draw();
|
|
}
|
|
|
|
// handle dragend
|
|
if(go.drag.moving) {
|
|
go.drag.moving = false;
|
|
node._handleEvent('dragend', evt);
|
|
}
|
|
}
|
|
go.drag.node = null;
|
|
this.dragAnim.stop();
|
|
},
|
|
/**
|
|
* start drag and drop
|
|
*/
|
|
_startDrag: function(evt) {
|
|
var that = this;
|
|
var go = Kinetic.Global;
|
|
var node = go.drag.node;
|
|
|
|
if(node) {
|
|
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;
|
|
}
|
|
|
|
if(!go.drag.moving) {
|
|
go.drag.moving = true;
|
|
// execute dragstart events if defined
|
|
go.drag.node._handleEvent('dragstart', evt);
|
|
}
|
|
|
|
// execute user defined ondragmove if defined
|
|
go.drag.node._handleEvent('dragmove', evt);
|
|
}
|
|
},
|
|
/**
|
|
* build dom
|
|
*/
|
|
_buildDOM: function() {
|
|
// content
|
|
this.content = document.createElement('div');
|
|
this.content.style.position = 'relative';
|
|
this.content.style.display = 'inline-block';
|
|
this.content.className = 'kineticjs-content';
|
|
this.attrs.container.appendChild(this.content);
|
|
|
|
this.bufferCanvas = new Kinetic.Canvas({
|
|
width: this.attrs.width,
|
|
height: this.attrs.height
|
|
});
|
|
|
|
this._resizeDOM();
|
|
},
|
|
_addId: function(node) {
|
|
if(node.attrs.id !== undefined) {
|
|
this.ids[node.attrs.id] = node;
|
|
}
|
|
},
|
|
_removeId: function(id) {
|
|
if(id !== undefined) {
|
|
delete this.ids[id];
|
|
}
|
|
},
|
|
_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(name, _id) {
|
|
if(name !== undefined) {
|
|
var nodes = this.names[name];
|
|
if(nodes !== undefined) {
|
|
for(var n = 0; n < nodes.length; n++) {
|
|
var no = nodes[n];
|
|
if(no._id === _id) {
|
|
nodes.splice(n, 1);
|
|
}
|
|
}
|
|
if(nodes.length === 0) {
|
|
delete this.names[name];
|
|
}
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* 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
|
|
*/
|
|
_setStageDefaultProperties: function() {
|
|
this.nodeType = 'Stage';
|
|
this.dblClickWindow = 400;
|
|
this.targetShape = null;
|
|
this.mousePos = undefined;
|
|
this.clickStart = false;
|
|
this.touchPos = undefined;
|
|
this.tapStart = false;
|
|
|
|
/*
|
|
* ids and names hash needs to be stored at the stage level to prevent
|
|
* id and name collisions between multiple stages in the document
|
|
*/
|
|
this.ids = {};
|
|
this.names = {};
|
|
this.dragAnim = new Kinetic.Animation();
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Stage, Kinetic.Container);
|
|
|
|
// add getters and setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Stage, ['width', 'height']);
|
|
|
|
/**
|
|
* get width
|
|
* @name getWidth
|
|
* @methodOf Kinetic.Stage.prototype
|
|
*/
|
|
|
|
/**
|
|
* get height
|
|
* @name getHeight
|
|
* @methodOf Kinetic.Stage.prototype
|
|
*/
|
|
|
|
/**
|
|
* set width
|
|
* @name setWidth
|
|
* @methodOf Kinetic.Stage.prototype
|
|
* @param {Number} width
|
|
*/
|
|
|
|
/**
|
|
* set height
|
|
* @name setHeight
|
|
* @methodOf Kinetic.Stage.prototype
|
|
* @param {Number} height
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Layer
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Layer constructor. Layers are tied to their own canvas element and are used
|
|
* to contain groups or shapes
|
|
* @constructor
|
|
* @augments Kinetic.Container
|
|
* @param {Object} config
|
|
* @param {Boolean} [config.clearBeforeDraw] set this property to true if you'd like to disable
|
|
* canvas clearing before each new layer draw
|
|
* @param {Number} [config.x]
|
|
* @param {Number} [config.y]
|
|
* @param {Boolean} [config.visible]
|
|
* @param {Boolean} [config.listening] whether or not the node is listening for events
|
|
* @param {String} [config.id] unique id
|
|
* @param {String} [config.name] non-unique name
|
|
* @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1
|
|
* @param {Object} [config.scale]
|
|
* @param {Number} [config.scale.x]
|
|
* @param {Number} [config.scale.y]
|
|
* @param {Number} [config.rotation] rotation in radians
|
|
* @param {Number} [config.rotationDeg] rotation in degrees
|
|
* @param {Object} [config.offset] offsets default position point and rotation point
|
|
* @param {Number} [config.offset.x]
|
|
* @param {Number} [config.offset.y]
|
|
* @param {Boolean} [config.draggable]
|
|
* @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default
|
|
* is none
|
|
* @param {Object} [config.dragBounds]
|
|
* @param {Number} [config.dragBounds.top]
|
|
* @param {Number} [config.dragBounds.right]
|
|
* @param {Number} [config.dragBounds.bottom]
|
|
* @param {Number} [config.dragBounds.left]
|
|
*/
|
|
Kinetic.Layer = function(config) {
|
|
this._initLayer(config);
|
|
};
|
|
|
|
Kinetic.Layer.prototype = {
|
|
_initLayer: function(config) {
|
|
this.setDefaultAttrs({
|
|
clearBeforeDraw: true
|
|
});
|
|
|
|
this.nodeType = 'Layer';
|
|
this.beforeDrawFunc = undefined;
|
|
this.afterDrawFunc = undefined;
|
|
this.canvas = new Kinetic.Canvas();
|
|
this.canvas.getElement().style.position = 'absolute';
|
|
this.bufferCanvas = new Kinetic.Canvas();
|
|
this.bufferCanvas.name = 'buffer';
|
|
|
|
// call super constructor
|
|
Kinetic.Container.call(this, config);
|
|
},
|
|
/**
|
|
* draw children nodes. this includes any groups
|
|
* or shapes
|
|
* @name draw
|
|
* @methodOf Kinetic.Layer.prototype
|
|
*/
|
|
draw: function(canvas) {
|
|
// before draw handler
|
|
if(this.beforeDrawFunc !== undefined) {
|
|
this.beforeDrawFunc.call(this);
|
|
}
|
|
|
|
if(canvas) {
|
|
this._draw(canvas);
|
|
}
|
|
else {
|
|
this._draw(this.getCanvas());
|
|
this._draw(this.bufferCanvas);
|
|
}
|
|
|
|
// after draw handler
|
|
if(this.afterDrawFunc !== undefined) {
|
|
this.afterDrawFunc.call(this);
|
|
}
|
|
},
|
|
/**
|
|
* draw children nodes on buffer. this includes any groups
|
|
* or shapes
|
|
* @name drawBuffer
|
|
* @methodOf Kinetic.Layer.prototype
|
|
*/
|
|
drawBuffer: function() {
|
|
this.draw(this.bufferCanvas);
|
|
},
|
|
/**
|
|
* draw children nodes on scene. this includes any groups
|
|
* or shapes
|
|
* @name drawScene
|
|
* @methodOf Kinetic.Layer.prototype
|
|
*/
|
|
drawScene: function() {
|
|
this.draw(this.getCanvas());
|
|
},
|
|
/**
|
|
* set before draw handler
|
|
* @name beforeDraw
|
|
* @methodOf Kinetic.Layer.prototype
|
|
* @param {Function} handler
|
|
*/
|
|
beforeDraw: function(func) {
|
|
this.beforeDrawFunc = func;
|
|
},
|
|
/**
|
|
* set after draw handler
|
|
* @name afterDraw
|
|
* @methodOf Kinetic.Layer.prototype
|
|
* @param {Function} handler
|
|
*/
|
|
afterDraw: function(func) {
|
|
this.afterDrawFunc = func;
|
|
},
|
|
/**
|
|
* get layer canvas
|
|
* @name getCanvas
|
|
* @methodOf Kinetic.Layer.prototype
|
|
*/
|
|
getCanvas: function() {
|
|
return this.canvas;
|
|
},
|
|
/**
|
|
* get layer canvas context
|
|
* @name getContext
|
|
* @methodOf Kinetic.Layer.prototype
|
|
*/
|
|
getContext: function() {
|
|
return this.canvas.context;
|
|
},
|
|
/**
|
|
* clear canvas tied to the layer
|
|
* @name clear
|
|
* @methodOf Kinetic.Layer.prototype
|
|
*/
|
|
clear: function() {
|
|
this.getCanvas().clear();
|
|
},
|
|
/**
|
|
* show layer
|
|
* @name show
|
|
* @methodOf Kinetic.Layer.prototype
|
|
*/
|
|
show: function() {
|
|
Kinetic.Node.prototype.show.call(this);
|
|
this.canvas.element.style.display = 'block';
|
|
},
|
|
/**
|
|
* hide layer. Hidden layers are no longer detectable
|
|
* @name hide
|
|
* @methodOf Kinetic.Layer.prototype
|
|
*/
|
|
hide: function() {
|
|
Kinetic.Node.prototype.hide.call(this);
|
|
this.canvas.element.style.display = 'none';
|
|
},
|
|
/**
|
|
* Creates a composite data URL. If MIME type is not
|
|
* specified, then "image/png" will result. For "image/jpeg", specify a quality
|
|
* level as quality (range 0.0 - 1.0). Note that this method works
|
|
* differently from toDataURL() for other nodes because it generates an absolute dataURL
|
|
* based on what's draw on the layer, rather than drawing
|
|
* the current state of each child node
|
|
* @name toDataURL
|
|
* @methodOf Kinetic.Layer.prototype
|
|
* @param {Object} config
|
|
* @param {String} [config.mimeType] mime type. can be "image/png" or "image/jpeg".
|
|
* "image/png" is the default
|
|
* @param {Number} [config.width] data url image width
|
|
* @param {Number} [config.height] data url image height
|
|
* @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType,
|
|
* you can specify the quality from 0 to 1, where 0 is very poor quality and 1
|
|
* is very high quality
|
|
*/
|
|
toDataURL: function(config) {
|
|
var canvas;
|
|
var mimeType = config && config.mimeType ? config.mimeType : null;
|
|
var quality = config && config.quality ? config.quality : null;
|
|
|
|
if(config && config.width && config.height) {
|
|
canvas = new Kinetic.Canvas(config.width, config.height);
|
|
}
|
|
else {
|
|
canvas = this.getCanvas();
|
|
}
|
|
return canvas.toDataURL(mimeType, quality);
|
|
},
|
|
/**
|
|
* remove layer from stage
|
|
*/
|
|
_remove: function() {
|
|
/*
|
|
* remove canvas DOM from the document if
|
|
* it exists
|
|
*/
|
|
try {
|
|
this.getStage().content.removeChild(this.canvas.element);
|
|
}
|
|
catch(e) {
|
|
Kinetic.Global.warn('unable to remove layer scene canvas element from the document');
|
|
}
|
|
},
|
|
__draw: function(canvas) {
|
|
if(this.attrs.clearBeforeDraw) {
|
|
canvas.clear();
|
|
}
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Layer, Kinetic.Container);
|
|
|
|
// add getters and setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Layer, ['clearBeforeDraw']);
|
|
|
|
/**
|
|
* set flag which determines if the layer is cleared or not
|
|
* before drawing
|
|
* @name setClearBeforeDraw
|
|
* @methodOf Kinetic.Layer.prototype
|
|
* @param {Boolean} clearBeforeDraw
|
|
*/
|
|
|
|
/**
|
|
* get flag which determines if the layer is cleared or not
|
|
* before drawing
|
|
* @name getClearBeforeDraw
|
|
* @methodOf Kinetic.Layer.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Group
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Group constructor. Groups are used to contain shapes or other groups.
|
|
* @constructor
|
|
* @augments Kinetic.Container
|
|
* @param {Object} config
|
|
* @param {Number} [config.x]
|
|
* @param {Number} [config.y]
|
|
* @param {Boolean} [config.visible]
|
|
* @param {Boolean} [config.listening] whether or not the node is listening for events
|
|
* @param {String} [config.id] unique id
|
|
* @param {String} [config.name] non-unique name
|
|
* @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1
|
|
* @param {Object} [config.scale]
|
|
* @param {Number} [config.scale.x]
|
|
* @param {Number} [config.scale.y]
|
|
* @param {Number} [config.rotation] rotation in radians
|
|
* @param {Number} [config.rotationDeg] rotation in degrees
|
|
* @param {Object} [config.offset] offsets default position point and rotation point
|
|
* @param {Number} [config.offset.x]
|
|
* @param {Number} [config.offset.y]
|
|
* @param {Boolean} [config.draggable]
|
|
* @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default
|
|
* is none
|
|
* @param {Object} [config.dragBounds]
|
|
* @param {Number} [config.dragBounds.top]
|
|
* @param {Number} [config.dragBounds.right]
|
|
* @param {Number} [config.dragBounds.bottom]
|
|
* @param {Number} [config.dragBounds.left]
|
|
*/
|
|
Kinetic.Group = function(config) {
|
|
this._initGroup(config);
|
|
};
|
|
|
|
Kinetic.Group.prototype = {
|
|
_initGroup: function(config) {
|
|
this.nodeType = 'Group';
|
|
|
|
// call super constructor
|
|
Kinetic.Container.call(this, config);
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Group, Kinetic.Container);
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Shape
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Shape constructor. Shapes are primitive objects such as rectangles,
|
|
* circles, text, lines, etc.
|
|
* @constructor
|
|
* @augments Kinetic.Node
|
|
* @param {Object} config
|
|
* @config {String|Object} [config.fill] can be a string color, a linear gradient object, a radial
|
|
* gradient object, or a pattern object.
|
|
* @config {Image} [config.fill.image] image object if filling the shape with a pattern
|
|
* @config {Object} [config.fill.offset] pattern offset if filling the shape with a pattern
|
|
* @config {Number} [config.fill.offset.x]
|
|
* @config {Number} [config.fill.offset.y]
|
|
* @config {Object} [config.fill.start] start point if using a linear gradient or
|
|
* radial gradient fill
|
|
* @config {Number} [config.fill.start.x]
|
|
* @config {Number} [config.fill.start.y]
|
|
* @config {Number} [config.fill.start.radius] start radius if using a radial gradient fill
|
|
* @config {Object} [config.fill.end] end point if using a linear gradient or
|
|
* radial gradient fill
|
|
* @config {Number} [config.fill.end.x]
|
|
* @config {Number} [config.fill.end.y]
|
|
* @config {Number} [config.fill.end.radius] end radius if using a radial gradient fill
|
|
* @config {String} [config.stroke] stroke color
|
|
* @config {Number} [config.strokeWidth] stroke width
|
|
* @config {String} [config.lineJoin] line join can be miter, round, or bevel. The default
|
|
* is miter
|
|
* @config {Object} [config.shadow] shadow object
|
|
* @config {String} [config.shadow.color]
|
|
* @config {Number} [config.shadow.blur]
|
|
* @config {Obect} [config.shadow.blur.offset]
|
|
* @config {Number} [config.shadow.blur.offset.x]
|
|
* @config {Number} [config.shadow.blur.offset.y]
|
|
* @config {Number} [config.shadow.opacity] shadow opacity. Can be any real number
|
|
* between 0 and 1
|
|
* @param {Number} [config.x]
|
|
* @param {Number} [config.y]
|
|
* @param {Boolean} [config.visible]
|
|
* @param {Boolean} [config.listening] whether or not the node is listening for events
|
|
* @param {String} [config.id] unique id
|
|
* @param {String} [config.name] non-unique name
|
|
* @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1
|
|
* @param {Object} [config.scale]
|
|
* @param {Number} [config.scale.x]
|
|
* @param {Number} [config.scale.y]
|
|
* @param {Number} [config.rotation] rotation in radians
|
|
* @param {Number} [config.rotationDeg] rotation in degrees
|
|
* @param {Object} [config.offset] offsets default position point and rotation point
|
|
* @param {Number} [config.offset.x]
|
|
* @param {Number} [config.offset.y]
|
|
* @param {Boolean} [config.draggable]
|
|
* @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default
|
|
* is none
|
|
* @param {Object} [config.dragBounds]
|
|
* @param {Number} [config.dragBounds.top]
|
|
* @param {Number} [config.dragBounds.right]
|
|
* @param {Number} [config.dragBounds.bottom]
|
|
* @param {Number} [config.dragBounds.left]
|
|
*/
|
|
Kinetic.Shape = function(config) {
|
|
this._initShape(config);
|
|
};
|
|
|
|
Kinetic.Shape.prototype = {
|
|
_initShape: function(config) {
|
|
this.nodeType = 'Shape';
|
|
this.appliedShadow = false;
|
|
|
|
// set colorKey
|
|
var shapes = Kinetic.Global.shapes;
|
|
var key;
|
|
while(true) {
|
|
key = Kinetic.Type._getRandomColorKey();
|
|
if(key && !( key in shapes)) {
|
|
break;
|
|
}
|
|
}
|
|
this.colorKey = key;
|
|
shapes[key] = this;
|
|
|
|
// call super constructor
|
|
Kinetic.Node.call(this, config);
|
|
},
|
|
/**
|
|
* get canvas context tied to the layer
|
|
* @name getContext
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
getContext: function() {
|
|
return this.getLayer().getContext();
|
|
},
|
|
/**
|
|
* get canvas tied to the layer
|
|
* @name getCanvas
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
getCanvas: function() {
|
|
return this.getLayer().getCanvas();
|
|
},
|
|
/**
|
|
* helper method to stroke the shape and apply
|
|
* shadows if needed
|
|
* @name stroke
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
stroke: function(context) {
|
|
var strokeWidth = this.getStrokeWidth();
|
|
var stroke = this.getStroke();
|
|
if(stroke || strokeWidth) {
|
|
var go = Kinetic.Global;
|
|
var appliedShadow = false;
|
|
|
|
context.save();
|
|
if(this.attrs.shadow && !this.appliedShadow) {
|
|
appliedShadow = this._applyShadow(context);
|
|
}
|
|
|
|
context.lineWidth = strokeWidth || 2;
|
|
context.strokeStyle = stroke || 'black';
|
|
context.stroke(context);
|
|
context.restore();
|
|
|
|
if(appliedShadow) {
|
|
this.stroke(context);
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* helper method to fill the shape with a color, linear gradient,
|
|
* radial gradient, or pattern, and also apply shadows if needed
|
|
* @name fill
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* */
|
|
fill: function(context) {
|
|
var appliedShadow = false;
|
|
var fill = this.attrs.fill;
|
|
if(fill) {
|
|
context.save();
|
|
if(this.attrs.shadow && !this.appliedShadow) {
|
|
appliedShadow = this._applyShadow(context);
|
|
}
|
|
|
|
var s = fill.start;
|
|
var e = fill.end;
|
|
var f = null;
|
|
|
|
// color fill
|
|
if(Kinetic.Type._isString(fill)) {
|
|
context.fillStyle = fill;
|
|
context.fill(context);
|
|
}
|
|
// pattern
|
|
else if(fill.image) {
|
|
var repeat = !fill.repeat ? 'repeat' : fill.repeat;
|
|
if(fill.scale) {
|
|
context.scale(fill.scale.x, fill.scale.y);
|
|
}
|
|
if(fill.offset) {
|
|
context.translate(fill.offset.x, fill.offset.y);
|
|
}
|
|
|
|
context.fillStyle = context.createPattern(fill.image, repeat);
|
|
context.fill(context);
|
|
}
|
|
// linear gradient
|
|
else if(!s.radius && !e.radius) {
|
|
var grd = context.createLinearGradient(s.x, s.y, e.x, e.y);
|
|
var colorStops = fill.colorStops;
|
|
|
|
// build color stops
|
|
for(var n = 0; n < colorStops.length; n += 2) {
|
|
grd.addColorStop(colorStops[n], colorStops[n + 1]);
|
|
}
|
|
context.fillStyle = grd;
|
|
context.fill(context);
|
|
}
|
|
// radial gradient
|
|
else if((s.radius || s.radius === 0) && (e.radius || e.radius === 0)) {
|
|
var grd = context.createRadialGradient(s.x, s.y, s.radius, e.x, e.y, e.radius);
|
|
var colorStops = fill.colorStops;
|
|
|
|
// build color stops
|
|
for(var n = 0; n < colorStops.length; n += 2) {
|
|
grd.addColorStop(colorStops[n], colorStops[n + 1]);
|
|
}
|
|
context.fillStyle = grd;
|
|
context.fill(context);
|
|
}
|
|
else {
|
|
context.fillStyle = 'black';
|
|
context.fill(context);
|
|
}
|
|
context.restore();
|
|
}
|
|
|
|
if(appliedShadow) {
|
|
this.fill(context);
|
|
}
|
|
},
|
|
/**
|
|
* helper method to fill text and appy shadows if needed
|
|
* @param {String} text
|
|
* @name fillText
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
fillText: function(context, text) {
|
|
var appliedShadow = false;
|
|
if(this.attrs.textFill) {
|
|
context.save();
|
|
if(this.attrs.shadow && !this.appliedShadow) {
|
|
appliedShadow = this._applyShadow(context);
|
|
}
|
|
context.fillStyle = this.attrs.textFill;
|
|
context.fillText(text, 0, 0);
|
|
context.restore();
|
|
}
|
|
if(appliedShadow) {
|
|
this.fillText(context, text, 0, 0);
|
|
}
|
|
},
|
|
/**
|
|
* helper method to stroke text and apply shadows
|
|
* if needed
|
|
* @name strokeText
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {String} text
|
|
*/
|
|
strokeText: function(context, text) {
|
|
var appliedShadow = false;
|
|
|
|
if(this.attrs.textStroke || this.attrs.textStrokeWidth) {
|
|
context.save();
|
|
if(this.attrs.shadow && !this.appliedShadow) {
|
|
appliedShadow = this._applyShadow(context);
|
|
}
|
|
// defaults
|
|
var textStroke = this.attrs.textStroke ? this.attrs.textStroke : 'black';
|
|
var textStrokeWidth = this.attrs.textStrokeWidth ? this.attrs.textStrokeWidth : 2;
|
|
context.lineWidth = textStrokeWidth;
|
|
context.strokeStyle = textStroke;
|
|
context.strokeText(text, 0, 0);
|
|
context.restore();
|
|
}
|
|
|
|
if(appliedShadow) {
|
|
this.strokeText(context, text, 0, 0);
|
|
}
|
|
},
|
|
/**
|
|
* helper method to draw an image and apply
|
|
* a shadow if neede
|
|
* @name drawImage
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
drawImage: function() {
|
|
var appliedShadow = false;
|
|
var context = arguments[0];
|
|
context.save();
|
|
var a = Array.prototype.slice.call(arguments);
|
|
|
|
if(a.length === 6 || a.length === 10) {
|
|
if(this.attrs.shadow && !this.appliedShadow) {
|
|
appliedShadow = this._applyShadow(context);
|
|
}
|
|
|
|
if(a.length === 6) {
|
|
context.drawImage(a[1], a[2], a[3], a[4], a[5]);
|
|
}
|
|
else {
|
|
context.drawImage(a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9]);
|
|
}
|
|
}
|
|
|
|
context.restore();
|
|
|
|
if(appliedShadow) {
|
|
this.drawImage.apply(this, a);
|
|
}
|
|
},
|
|
/**
|
|
* helper method to set the line join of a shape
|
|
* based on the lineJoin property
|
|
* @name applyLineJoin
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
applyLineJoin: function(context) {
|
|
if(this.attrs.lineJoin) {
|
|
context.lineJoin = this.attrs.lineJoin;
|
|
}
|
|
},
|
|
/**
|
|
* apply shadow. return true if shadow was applied
|
|
* and false if it was not
|
|
*/
|
|
_applyShadow: function(context) {
|
|
var s = this.attrs.shadow;
|
|
if(s) {
|
|
var aa = this.getAbsoluteOpacity();
|
|
// defaults
|
|
var color = s.color ? s.color : 'black';
|
|
var blur = s.blur ? s.blur : 5;
|
|
var offset = s.offset ? s.offset : {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
|
|
if(s.opacity) {
|
|
context.globalAlpha = s.opacity * aa;
|
|
}
|
|
context.shadowColor = color;
|
|
context.shadowBlur = blur;
|
|
context.shadowOffsetX = offset.x;
|
|
context.shadowOffsetY = offset.y;
|
|
this.appliedShadow = true;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
/**
|
|
* determines if point is in the shape
|
|
* @param {Object|Array} point point can be an object containing
|
|
* an x and y property, or it can be an array with two elements
|
|
* in which the first element is the x component and the second
|
|
* element is the y component
|
|
*/
|
|
intersects: function() {
|
|
var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments));
|
|
var stage = this.getStage();
|
|
var bufferCanvas = stage.bufferCanvas;
|
|
bufferCanvas.clear();
|
|
this._draw(bufferCanvas);
|
|
var p = bufferCanvas.context.getImageData(Math.round(pos.x), Math.round(pos.y), 1, 1).data;
|
|
return p[3] > 0;
|
|
},
|
|
_remove: function() {
|
|
delete Kinetic.Global.shapes[this.colorKey];
|
|
},
|
|
__draw: function(canvas) {
|
|
if(this.attrs.drawFunc) {
|
|
var stage = this.getStage();
|
|
var context = canvas.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();
|
|
var m = t.getMatrix();
|
|
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
|
|
}
|
|
|
|
/*
|
|
* pre styles include opacity, linejoin
|
|
*/
|
|
var absOpacity = this.getAbsoluteOpacity();
|
|
if(absOpacity !== 1) {
|
|
context.globalAlpha = absOpacity;
|
|
}
|
|
this.applyLineJoin(context);
|
|
|
|
// draw the shape
|
|
this.appliedShadow = false;
|
|
|
|
var wl = Kinetic.Global.BUFFER_WHITELIST;
|
|
var bl = Kinetic.Global.BUFFER_BLACKLIST;
|
|
var attrs = {};
|
|
|
|
if(canvas.name === 'buffer') {
|
|
for(var n = 0; n < wl.length; n++) {
|
|
var key = wl[n];
|
|
attrs[key] = this.attrs[key];
|
|
if(this.attrs[key] || (key === 'fill' && !this.attrs.stroke && !('image' in this.attrs))) {
|
|
this.attrs[key] = '#' + this.colorKey;
|
|
}
|
|
}
|
|
|
|
for(var n = 0; n < bl.length; n++) {
|
|
var key = bl[n];
|
|
attrs[key] = this.attrs[key];
|
|
this.attrs[key] = '';
|
|
}
|
|
|
|
// image is a special case
|
|
if('image' in this.attrs) {
|
|
attrs.image = this.attrs.image;
|
|
|
|
if(this.imageBuffer) {
|
|
this.attrs.image = this.imageBuffer;
|
|
}
|
|
else {
|
|
this.attrs.image = null;
|
|
this.attrs.fill = '#' + this.colorKey;
|
|
}
|
|
}
|
|
|
|
context.globalAlpha = 1;
|
|
}
|
|
|
|
this.attrs.drawFunc.call(this, canvas.getContext());
|
|
|
|
if(canvas.name === 'buffer') {
|
|
var bothLists = wl.concat(bl);
|
|
for(var n = 0; n < bothLists.length; n++) {
|
|
var key = bothLists[n];
|
|
this.attrs[key] = attrs[key];
|
|
}
|
|
|
|
// image is a special case
|
|
this.attrs.image = attrs.image;
|
|
}
|
|
|
|
context.restore();
|
|
}
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Shape, Kinetic.Node);
|
|
|
|
// add getters and setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Shape, ['fill', 'stroke', 'lineJoin', 'strokeWidth', 'shadow', 'drawFunc', 'filter']);
|
|
|
|
/**
|
|
* set fill which can be a color, linear gradient object,
|
|
* radial gradient object, or pattern object
|
|
* @name setFill
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {String|Object} fill
|
|
*/
|
|
|
|
/**
|
|
* set stroke color
|
|
* @name setStroke
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {String} stroke
|
|
*/
|
|
|
|
/**
|
|
* set line join
|
|
* @name setLineJoin
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {String} lineJoin. Can be miter, round, or bevel. The
|
|
* default is miter
|
|
*/
|
|
|
|
/**
|
|
* set stroke width
|
|
* @name setStrokeWidth
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {Number} strokeWidth
|
|
*/
|
|
|
|
/**
|
|
* set shadow object
|
|
* @name setShadow
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {Object} config
|
|
*/
|
|
|
|
/**
|
|
* set draw function
|
|
* @name setDrawFunc
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {Function} drawFunc drawing function
|
|
*/
|
|
|
|
/**
|
|
* get fill
|
|
* @name getFill
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
|
|
/**
|
|
* get stroke color
|
|
* @name getStroke
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
|
|
/**
|
|
* get line join
|
|
* @name getLineJoin
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
|
|
/**
|
|
* get stroke width
|
|
* @name getStrokeWidth
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
|
|
/**
|
|
* get shadow object
|
|
* @name getShadow
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
|
|
/**
|
|
* get draw function
|
|
* @name getDrawFunc
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Rect
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Rect constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Rect = function(config) {
|
|
this._initRect(config);
|
|
}
|
|
Kinetic.Rect.prototype = {
|
|
_initRect: function(config) {
|
|
this.setDefaultAttrs({
|
|
width: 0,
|
|
height: 0,
|
|
cornerRadius: 0
|
|
});
|
|
this.shapeType = "Rect";
|
|
config.drawFunc = this.drawFunc;
|
|
|
|
Kinetic.Shape.call(this, config);
|
|
},
|
|
drawFunc: function(context) {
|
|
context.beginPath();
|
|
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.fill(context);
|
|
this.stroke(context);
|
|
},
|
|
/**
|
|
* set width and height
|
|
* @name setSize
|
|
* @methodOf Kinetic.Rect.prototype
|
|
*/
|
|
setSize: function() {
|
|
var size = Kinetic.Type._getSize(Array.prototype.slice.call(arguments));
|
|
this.setAttrs(size);
|
|
},
|
|
/**
|
|
* return rect size
|
|
* @name getSize
|
|
* @methodOf Kinetic.Rect.prototype
|
|
*/
|
|
getSize: function() {
|
|
return {
|
|
width: this.attrs.width,
|
|
height: this.attrs.height
|
|
};
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Rect, Kinetic.Shape);
|
|
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Rect, ['width', 'height', 'cornerRadius']);
|
|
|
|
/**
|
|
* set width
|
|
* @name setWidth
|
|
* @methodOf Kinetic.Rect.prototype
|
|
* @param {Number} width
|
|
*/
|
|
|
|
/**
|
|
* set height
|
|
* @name setHeight
|
|
* @methodOf Kinetic.Rect.prototype
|
|
* @param {Number} height
|
|
*/
|
|
|
|
/**
|
|
* set corner radius
|
|
* @name setCornerRadius
|
|
* @methodOf Kinetic.Rect.prototype
|
|
* @param {Number} radius
|
|
*/
|
|
|
|
/**
|
|
* get width
|
|
* @name getWidth
|
|
* @methodOf Kinetic.Rect.prototype
|
|
*/
|
|
|
|
/**
|
|
* get height
|
|
* @name getHeight
|
|
* @methodOf Kinetic.Rect.prototype
|
|
*/
|
|
|
|
/**
|
|
* get corner radius
|
|
* @name getCornerRadius
|
|
* @methodOf Kinetic.Rect.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Circle
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Circle constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Circle = function(config) {
|
|
this._initCircle(config);
|
|
};
|
|
|
|
Kinetic.Circle.prototype = {
|
|
_initCircle: function(config) {
|
|
this.setDefaultAttrs({
|
|
radius: 0
|
|
});
|
|
|
|
this.shapeType = "Circle";
|
|
config.drawFunc = this.drawFunc;
|
|
|
|
// call super constructor
|
|
Kinetic.Shape.call(this, config);
|
|
},
|
|
drawFunc: function(context) {
|
|
context.beginPath();
|
|
context.arc(0, 0, this.getRadius(), 0, Math.PI * 2, true);
|
|
context.closePath();
|
|
this.fill(context);
|
|
this.stroke(context);
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Circle, Kinetic.Shape);
|
|
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Circle, ['radius']);
|
|
|
|
/**
|
|
* set radius
|
|
* @name setRadius
|
|
* @methodOf Kinetic.Circle.prototype
|
|
* @param {Number} radius
|
|
*/
|
|
|
|
/**
|
|
* get radius
|
|
* @name getRadius
|
|
* @methodOf Kinetic.Circle.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Ellipse
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Ellipse constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Ellipse = function(config) {
|
|
this._initEllipse(config);
|
|
};
|
|
|
|
Kinetic.Ellipse.prototype = {
|
|
_initEllipse: function(config) {
|
|
this.setDefaultAttrs({
|
|
radius: {
|
|
x: 0,
|
|
y: 0
|
|
}
|
|
});
|
|
|
|
this.shapeType = "Ellipse";
|
|
config.drawFunc = this.drawFunc;
|
|
|
|
// call super constructor
|
|
Kinetic.Shape.call(this, config);
|
|
},
|
|
drawFunc: function(context) {
|
|
var r = this.getRadius();
|
|
context.beginPath();
|
|
context.save();
|
|
if(r.x !== r.y) {
|
|
context.scale(1, r.y / r.x);
|
|
}
|
|
context.arc(0, 0, r.x, 0, Math.PI * 2, true);
|
|
context.restore();
|
|
context.closePath();
|
|
this.fill(context);
|
|
this.stroke(context);
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Ellipse, Kinetic.Shape);
|
|
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Ellipse, ['radius']);
|
|
|
|
/**
|
|
* set radius
|
|
* @name setRadius
|
|
* @methodOf Kinetic.Ellipse.prototype
|
|
* @param {Object|Array} radius
|
|
* radius can be a number, in which the ellipse becomes a circle,
|
|
* it can be an object with an x and y component, or it
|
|
* can be an array in which the first element is the x component
|
|
* and the second element is the y component. The x component
|
|
* defines the horizontal radius and the y component
|
|
* defines the vertical radius
|
|
*/
|
|
|
|
/**
|
|
* get radius
|
|
* @name getRadius
|
|
* @methodOf Kinetic.Ellipse.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Image
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Image constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
* @param {ImageObject} config.image
|
|
* @param {Number} [config.width]
|
|
* @param {Number} [config.height]
|
|
* @param {Object} [config.crop]
|
|
*/
|
|
Kinetic.Image = function(config) {
|
|
this._initImage(config);
|
|
};
|
|
|
|
Kinetic.Image.prototype = {
|
|
_initImage: function(config) {
|
|
this.shapeType = "Image";
|
|
config.drawFunc = this.drawFunc;
|
|
// call super constructor
|
|
Kinetic.Shape.call(this, config);
|
|
|
|
var that = this;
|
|
this.on('imageChange', function(evt) {
|
|
that._syncSize();
|
|
});
|
|
|
|
this._syncSize();
|
|
},
|
|
drawFunc: function(context) {
|
|
var width = this.getWidth();
|
|
var height = this.getHeight();
|
|
|
|
context.beginPath();
|
|
context.rect(0, 0, width, height);
|
|
context.closePath();
|
|
this.fill(context);
|
|
this.stroke(context);
|
|
|
|
if(this.attrs.image) {
|
|
// if cropping
|
|
if(this.attrs.crop && this.attrs.crop.width && this.attrs.crop.height) {
|
|
var cropX = this.attrs.crop.x ? this.attrs.crop.x : 0;
|
|
var cropY = this.attrs.crop.y ? this.attrs.crop.y : 0;
|
|
var cropWidth = this.attrs.crop.width;
|
|
var cropHeight = this.attrs.crop.height;
|
|
this.drawImage(context, this.attrs.image, cropX, cropY, cropWidth, cropHeight, 0, 0, width, height);
|
|
}
|
|
// no cropping
|
|
else {
|
|
this.drawImage(context, this.attrs.image, 0, 0, width, height);
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* set width and height
|
|
* @name setSize
|
|
* @methodOf Kinetic.Image.prototype
|
|
*/
|
|
setSize: function() {
|
|
var size = Kinetic.Type._getSize(Array.prototype.slice.call(arguments));
|
|
this.setAttrs(size);
|
|
},
|
|
/**
|
|
* return image size
|
|
* @name getSize
|
|
* @methodOf Kinetic.Image.prototype
|
|
*/
|
|
getSize: function() {
|
|
return {
|
|
width: this.attrs.width,
|
|
height: this.attrs.height
|
|
};
|
|
},
|
|
/**
|
|
* apply filter
|
|
* @name applyFilter
|
|
* @methodOf Kinetic.Image.prototype
|
|
* @param {Object} config
|
|
* @param {Function} config.filter filter function
|
|
* @param {Function} [config.callback] callback function to be called once
|
|
* filter has been applied
|
|
*/
|
|
applyFilter: function(config) {
|
|
var canvas = new Kinetic.Canvas(this.attrs.image.width, this.attrs.image.height);
|
|
var context = canvas.getContext();
|
|
context.drawImage(this.attrs.image, 0, 0);
|
|
try {
|
|
var imageData = context.getImageData(0, 0, canvas.getWidth(), canvas.getHeight());
|
|
config.filter(imageData, config);
|
|
var that = this;
|
|
Kinetic.Type._getImage(imageData, function(imageObj) {
|
|
that.setImage(imageObj);
|
|
|
|
if(config.callback) {
|
|
config.callback();
|
|
}
|
|
});
|
|
}
|
|
catch(e) {
|
|
Kinetic.Global.warn('Unable to apply filter.');
|
|
}
|
|
},
|
|
/**
|
|
* create image buffer which enables more accurate hit detection mapping of the image
|
|
* by avoiding event detections for transparent pixels
|
|
* @name createImageBuffer
|
|
* @methodOf Kinetic.Image.prototype
|
|
* @param {Function} [callback] callback function to be called once
|
|
* the buffer image has been created and set
|
|
*/
|
|
createImageBuffer: function(callback) {
|
|
var canvas = new Kinetic.Canvas(this.attrs.width, this.attrs.height);
|
|
var context = canvas.getContext();
|
|
context.drawImage(this.attrs.image, 0, 0);
|
|
try {
|
|
var imageData = context.getImageData(0, 0, canvas.getWidth(), canvas.getHeight());
|
|
var data = imageData.data;
|
|
var rgbColorKey = Kinetic.Type._hexToRgb(this.colorKey);
|
|
// replace non transparent pixels with color key
|
|
for(var i = 0, n = data.length; i < n; i += 4) {
|
|
data[i] = rgbColorKey.r;
|
|
data[i + 1] = rgbColorKey.g;
|
|
data[i + 2] = rgbColorKey.b;
|
|
// i+3 is alpha (the fourth element)
|
|
}
|
|
|
|
var that = this;
|
|
Kinetic.Type._getImage(imageData, function(imageObj) {
|
|
that.imageBuffer = imageObj;
|
|
if(callback) {
|
|
callback();
|
|
}
|
|
});
|
|
}
|
|
catch(e) {
|
|
Kinetic.Global.warn('Unable to create image buffer.');
|
|
}
|
|
},
|
|
/**
|
|
* clear buffer image
|
|
* @name clearImageBuffer
|
|
* @methodOf Kinetic.Image.prototype
|
|
*/
|
|
clearImageBuffer: function() {
|
|
delete this.imageBuffer;
|
|
},
|
|
_syncSize: function() {
|
|
if(this.attrs.image) {
|
|
if(!this.attrs.width) {
|
|
this.setAttrs({
|
|
width: this.attrs.image.width
|
|
});
|
|
}
|
|
if(!this.attrs.height) {
|
|
this.setAttrs({
|
|
height: this.attrs.image.height
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Image, Kinetic.Shape);
|
|
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Image, ['image', 'crop', 'filter', 'width', 'height']);
|
|
|
|
/**
|
|
* set width
|
|
* @name setWidth
|
|
* @methodOf Kinetic.Image.prototype
|
|
* @param {Number} width
|
|
*/
|
|
|
|
/**
|
|
* set height
|
|
* @name setHeight
|
|
* @methodOf Kinetic.Image.prototype
|
|
* @param {Number} height
|
|
*/
|
|
|
|
/**
|
|
* set image
|
|
* @name setImage
|
|
* @methodOf Kinetic.Image.prototype
|
|
* @param {ImageObject} image
|
|
*/
|
|
|
|
/**
|
|
* set crop
|
|
* @name setCrop
|
|
* @methodOf Kinetic.Image.prototype
|
|
* @param {Object} config
|
|
*/
|
|
|
|
/**
|
|
* set filter
|
|
* @name setFilter
|
|
* @methodOf Kinetic.Image.prototype
|
|
* @param {Object} config
|
|
*/
|
|
|
|
/**
|
|
* get crop
|
|
* @name getCrop
|
|
* @methodOf Kinetic.Image.prototype
|
|
*/
|
|
|
|
/**
|
|
* get image
|
|
* @name getImage
|
|
* @methodOf Kinetic.Image.prototype
|
|
*/
|
|
|
|
/**
|
|
* get filter
|
|
* @name getFilter
|
|
* @methodOf Kinetic.Image.prototype
|
|
*/
|
|
|
|
/**
|
|
* get width
|
|
* @name getWidth
|
|
* @methodOf Kinetic.Image.prototype
|
|
*/
|
|
|
|
/**
|
|
* get height
|
|
* @name getHeight
|
|
* @methodOf Kinetic.Image.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Polygon
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Polygon constructor. Polygons are defined by an array of points
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Polygon = function(config) {
|
|
this._initPolygon(config);
|
|
};
|
|
|
|
Kinetic.Polygon.prototype = {
|
|
_initPolygon: function(config) {
|
|
this.setDefaultAttrs({
|
|
points: []
|
|
});
|
|
|
|
this.shapeType = "Polygon";
|
|
config.drawFunc = this.drawFunc;
|
|
// call super constructor
|
|
Kinetic.Shape.call(this, config);
|
|
},
|
|
drawFunc: function(context) {
|
|
context.beginPath();
|
|
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.fill(context);
|
|
this.stroke(context);
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Polygon, Kinetic.Shape);
|
|
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Polygon, ['points']);
|
|
|
|
/**
|
|
* set points array
|
|
* @name setPoints
|
|
* @methodOf Kinetic.Polygon.prototype
|
|
* @param {Array} points can be an array of point objects or an array
|
|
* of Numbers. e.g. [{x:1,y:2},{x:3,y:4}] or [1,2,3,4]
|
|
*/
|
|
|
|
/**
|
|
* get points array
|
|
* @name getPoints
|
|
* @methodOf Kinetic.Polygon.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Text
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Text constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Text = function(config) {
|
|
this._initText(config);
|
|
};
|
|
|
|
Kinetic.Text.prototype = {
|
|
_initText: function(config) {
|
|
this.setDefaultAttrs({
|
|
fontFamily: 'Calibri',
|
|
text: '',
|
|
fontSize: 12,
|
|
align: 'left',
|
|
verticalAlign: 'top',
|
|
fontStyle: 'normal',
|
|
padding: 0,
|
|
width: 'auto',
|
|
height: 'auto',
|
|
detectionType: 'path',
|
|
cornerRadius: 0,
|
|
lineHeight: 1.2
|
|
});
|
|
|
|
this.dummyCanvas = document.createElement('canvas');
|
|
this.shapeType = "Text";
|
|
|
|
config.drawFunc = this.drawFunc;
|
|
// call super constructor
|
|
Kinetic.Shape.call(this, config);
|
|
|
|
// update text data for certain attr changes
|
|
var attrs = ['fontFamily', 'fontSize', 'fontStyle', 'padding', 'align', 'lineHeight', 'text', 'width', 'height'];
|
|
var that = this;
|
|
for(var n = 0; n < attrs.length; n++) {
|
|
var attr = attrs[n];
|
|
this.on(attr + 'Change.kinetic', that._setTextData);
|
|
}
|
|
that._setTextData();
|
|
},
|
|
drawFunc: function(context) {
|
|
// draw rect
|
|
context.beginPath();
|
|
var boxWidth = this.getBoxWidth();
|
|
var boxHeight = this.getBoxHeight();
|
|
|
|
if(this.attrs.cornerRadius === 0) {
|
|
// simple rect - don't bother doing all that complicated maths stuff.
|
|
context.rect(0, 0, boxWidth, boxHeight);
|
|
}
|
|
else {
|
|
// arcTo would be nicer, but browser support is patchy (Opera)
|
|
context.moveTo(this.attrs.cornerRadius, 0);
|
|
context.lineTo(boxWidth - this.attrs.cornerRadius, 0);
|
|
context.arc(boxWidth - this.attrs.cornerRadius, this.attrs.cornerRadius, this.attrs.cornerRadius, Math.PI * 3 / 2, 0, false);
|
|
context.lineTo(boxWidth, boxHeight - this.attrs.cornerRadius);
|
|
context.arc(boxWidth - this.attrs.cornerRadius, boxHeight - this.attrs.cornerRadius, this.attrs.cornerRadius, 0, Math.PI / 2, false);
|
|
context.lineTo(this.attrs.cornerRadius, boxHeight);
|
|
context.arc(this.attrs.cornerRadius, boxHeight - 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.fill(context);
|
|
this.stroke(context);
|
|
/*
|
|
* draw text
|
|
*/
|
|
var p = this.attrs.padding;
|
|
var lineHeightPx = this.attrs.lineHeight * this.getTextHeight();
|
|
var textArr = this.textArr;
|
|
|
|
context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily;
|
|
context.textBaseline = 'middle';
|
|
context.textAlign = 'left';
|
|
context.save();
|
|
context.translate(p, 0);
|
|
context.translate(0, p + this.getTextHeight() / 2);
|
|
|
|
// draw text lines
|
|
var appliedShadow = this.appliedShadow;
|
|
for(var n = 0; n < textArr.length; n++) {
|
|
var text = textArr[n];
|
|
/*
|
|
* need to reset appliedShadow flag so that shadows
|
|
* are appropriately applied to each line of text
|
|
*/
|
|
this.appliedShadow = appliedShadow;
|
|
|
|
// horizontal alignment
|
|
context.save();
|
|
if(this.attrs.align === 'right') {
|
|
context.translate(this.getBoxWidth() - this._getTextSize(text).width - p * 2, 0);
|
|
}
|
|
else if(this.attrs.align === 'center') {
|
|
context.translate((this.getBoxWidth() - this._getTextSize(text).width - p * 2) / 2, 0);
|
|
}
|
|
|
|
this.fillText(context, text);
|
|
this.strokeText(context, text);
|
|
context.restore();
|
|
|
|
context.translate(0, lineHeightPx);
|
|
}
|
|
context.restore();
|
|
},
|
|
/**
|
|
* get box width
|
|
* @name getBoxWidth
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
getBoxWidth: function() {
|
|
return this.attrs.width === 'auto' ? this.getTextWidth() + this.attrs.padding * 2 : this.attrs.width;
|
|
},
|
|
/**
|
|
* get box height
|
|
* @name getBoxHeight
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
getBoxHeight: function() {
|
|
return this.attrs.height === 'auto' ? (this.getTextHeight() * this.textArr.length * this.attrs.lineHeight) + this.attrs.padding * 2 : this.attrs.height;
|
|
},
|
|
/**
|
|
* get text width in pixels
|
|
* @name getTextWidth
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
getTextWidth: function() {
|
|
return this.textWidth;
|
|
},
|
|
/**
|
|
* get text height in pixels
|
|
* @name getTextHeight
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
getTextHeight: function() {
|
|
return this.textHeight;
|
|
},
|
|
_getTextSize: function(text) {
|
|
var dummyCanvas = this.dummyCanvas;
|
|
var context = dummyCanvas.getContext('2d');
|
|
|
|
context.save();
|
|
context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily;
|
|
var metrics = context.measureText(text);
|
|
context.restore();
|
|
return {
|
|
width: metrics.width,
|
|
height: parseInt(this.attrs.fontSize, 10)
|
|
};
|
|
},
|
|
/**
|
|
* set text data. wrap logic and width and height setting occurs
|
|
* here
|
|
*/
|
|
_setTextData: function() {
|
|
var charArr = this.attrs.text.split('');
|
|
var arr = [];
|
|
var row = 0;
|
|
var addLine = true;
|
|
this.textWidth = 0;
|
|
this.textHeight = this._getTextSize(this.attrs.text).height;
|
|
var lineHeightPx = this.attrs.lineHeight * this.textHeight;
|
|
while(charArr.length > 0 && addLine && (this.attrs.height === 'auto' || lineHeightPx * (row + 1) < this.attrs.height - this.attrs.padding * 2)) {
|
|
var index = 0;
|
|
var line = undefined;
|
|
addLine = false;
|
|
|
|
while(index < charArr.length) {
|
|
if(charArr.indexOf('\n') === index) {
|
|
// remove newline char
|
|
charArr.splice(index, 1);
|
|
line = charArr.splice(0, index).join('');
|
|
break;
|
|
}
|
|
|
|
// if line exceeds inner box width
|
|
var lineArr = charArr.slice(0, index);
|
|
if(this.attrs.width !== 'auto' && this._getTextSize(lineArr.join('')).width > this.attrs.width - this.attrs.padding * 2) {
|
|
/*
|
|
* if a single character is too large to fit inside
|
|
* the text box width, then break out of the loop
|
|
* and stop processing
|
|
*/
|
|
if(index == 0) {
|
|
break;
|
|
}
|
|
var lastSpace = lineArr.lastIndexOf(' ');
|
|
var lastDash = lineArr.lastIndexOf('-');
|
|
var wrapIndex = Math.max(lastSpace, lastDash);
|
|
if(wrapIndex >= 0) {
|
|
line = charArr.splice(0, 1 + wrapIndex).join('');
|
|
break;
|
|
}
|
|
/*
|
|
* if not able to word wrap based on space or dash,
|
|
* go ahead and wrap in the middle of a word if needed
|
|
*/
|
|
line = charArr.splice(0, index).join('');
|
|
break;
|
|
}
|
|
index++;
|
|
|
|
// if the end is reached
|
|
if(index === charArr.length) {
|
|
line = charArr.splice(0, index).join('');
|
|
}
|
|
}
|
|
this.textWidth = Math.max(this.textWidth, this._getTextSize(line).width);
|
|
if(line !== undefined) {
|
|
arr.push(line);
|
|
addLine = true;
|
|
}
|
|
row++;
|
|
}
|
|
this.textArr = arr;
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Text, Kinetic.Shape);
|
|
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Text, ['fontFamily', 'fontSize', 'fontStyle', 'textFill', 'textStroke', 'textStrokeWidth', 'padding', 'align', 'lineHeight', 'text', 'width', 'height', 'cornerRadius', 'fill', 'stroke', 'strokeWidth', 'shadow']);
|
|
|
|
/**
|
|
* set font family
|
|
* @name setFontFamily
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {String} fontFamily
|
|
*/
|
|
|
|
/**
|
|
* set font size
|
|
* @name setFontSize
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {int} fontSize
|
|
*/
|
|
|
|
/**
|
|
* set font style. Can be "normal", "italic", or "bold". "normal" is the default.
|
|
* @name setFontStyle
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {String} fontStyle
|
|
*/
|
|
|
|
/**
|
|
* set text fill color
|
|
* @name setTextFill
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {String} textFill
|
|
*/
|
|
|
|
/**
|
|
* set text stroke color
|
|
* @name setFontStroke
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {String} textStroke
|
|
*/
|
|
|
|
/**
|
|
* set text stroke width
|
|
* @name setTextStrokeWidth
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {int} textStrokeWidth
|
|
*/
|
|
|
|
/**
|
|
* set padding
|
|
* @name setPadding
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {int} padding
|
|
*/
|
|
|
|
/**
|
|
* set horizontal align of text
|
|
* @name setAlign
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {String} align align can be 'left', 'center', or 'right'
|
|
*/
|
|
|
|
/**
|
|
* set line height
|
|
* @name setLineHeight
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {Number} lineHeight default is 1.2
|
|
*/
|
|
|
|
/**
|
|
* set text
|
|
* @name setText
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {String} text
|
|
*/
|
|
|
|
/**
|
|
* set width of text box
|
|
* @name setWidth
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {Number} width
|
|
*/
|
|
|
|
/**
|
|
* set height of text box
|
|
* @name setHeight
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {Number} height
|
|
*/
|
|
|
|
/**
|
|
* set shadow of text or textbox
|
|
* @name setShadow
|
|
* @methodOf Kinetic.Text.prototype
|
|
* @param {Object} config
|
|
*/
|
|
|
|
/**
|
|
* get font family
|
|
* @name getFontFamily
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get font size
|
|
* @name getFontSize
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get font style
|
|
* @name getFontStyle
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get text fill color
|
|
* @name getTextFill
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get text stroke color
|
|
* @name getTextStroke
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get text stroke width
|
|
* @name getTextStrokeWidth
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get padding
|
|
* @name getPadding
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get horizontal align
|
|
* @name getAlign
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get line height
|
|
* @name getLineHeight
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get text
|
|
* @name getText
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get width of text box
|
|
* @name getWidth
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get height of text box
|
|
* @name getHeight
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
|
|
/**
|
|
* get shadow of text or textbox
|
|
* @name getShadow
|
|
* @methodOf Kinetic.Text.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Line
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Line constructor. Lines are defined by an array of points
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Line = function(config) {
|
|
this._initLine(config);
|
|
};
|
|
|
|
Kinetic.Line.prototype = {
|
|
_initLine: function(config) {
|
|
this.setDefaultAttrs({
|
|
points: [],
|
|
lineCap: 'butt',
|
|
dashArray: [],
|
|
detectionType: 'pixel'
|
|
});
|
|
|
|
this.shapeType = "Line";
|
|
config.drawFunc = this.drawFunc;
|
|
// call super constructor
|
|
Kinetic.Shape.call(this, config);
|
|
},
|
|
drawFunc: function(context) {
|
|
var lastPos = {};
|
|
context.beginPath();
|
|
|
|
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(context, 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(context);
|
|
},
|
|
/**
|
|
* draw dashed line. Written by Phrogz
|
|
*/
|
|
_dashedLine: function(context, x, y, x2, y2, dashArray) {
|
|
var dashCount = dashArray.length;
|
|
|
|
var dx = (x2 - x), dy = (y2 - y);
|
|
var xSlope = dx > dy;
|
|
var slope = (xSlope) ? dy / dx : dx / dy;
|
|
|
|
/*
|
|
* gaurd against slopes of infinity
|
|
*/
|
|
if(slope > 9999) {
|
|
slope = 9999;
|
|
}
|
|
else if(slope < -9999) {
|
|
slope = -9999;
|
|
}
|
|
|
|
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 += dx < 0 && dy < 0 ? step * -1 : step;
|
|
y += dx < 0 && dy < 0 ? slope * step * -1 : slope * step;
|
|
}
|
|
else {
|
|
x += dx < 0 && dy < 0 ? slope * step * -1 : slope * step;
|
|
y += dx < 0 && dy < 0 ? step * -1 : step;
|
|
}
|
|
context[draw ? 'lineTo' : 'moveTo'](x, y);
|
|
distRemaining -= dashLength;
|
|
draw = !draw;
|
|
}
|
|
|
|
context.moveTo(x2, y2);
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Line, Kinetic.Shape);
|
|
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Line, ['dashArray', 'lineCap', 'points']);
|
|
|
|
/**
|
|
* set dash array.
|
|
* @name setDashArray
|
|
* @methodOf Kinetic.Line.prototype
|
|
* @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
|
|
*/
|
|
|
|
/**
|
|
* set line cap. Can be butt, round, or square
|
|
* @name setLineCap
|
|
* @methodOf Kinetic.Line.prototype
|
|
* @param {String} lineCap
|
|
*/
|
|
|
|
/**
|
|
* set points array
|
|
* @name setPoints
|
|
* @methodOf Kinetic.Line.prototype
|
|
* @param {Array} can be an array of point objects or an array
|
|
* of Numbers. e.g. [{x:1,y:2},{x:3,y:4}] or [1,2,3,4]
|
|
*/
|
|
|
|
/**
|
|
* get dash array
|
|
* @name getDashArray
|
|
* @methodOf Kinetic.Line.prototype
|
|
*/
|
|
|
|
/**
|
|
* get line cap
|
|
* @name getLineCap
|
|
* @methodOf Kinetic.Line.prototype
|
|
*/
|
|
|
|
/**
|
|
* get points array
|
|
* @name getPoints
|
|
* @methodOf Kinetic.Line.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Sprite
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Sprite constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Sprite = function(config) {
|
|
this._initSprite(config);
|
|
};
|
|
|
|
Kinetic.Sprite.prototype = {
|
|
_initSprite: function(config) {
|
|
this.setDefaultAttrs({
|
|
index: 0,
|
|
frameRate: 17
|
|
});
|
|
|
|
config.drawFunc = this.drawFunc;
|
|
// call super constructor
|
|
Kinetic.Shape.call(this, config);
|
|
this.anim = new Kinetic.Animation();
|
|
var that = this;
|
|
this.on('animationChange.kinetic', function() {
|
|
// reset index when animation changes
|
|
that.setIndex(0);
|
|
});
|
|
},
|
|
drawFunc: function(context) {
|
|
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();
|
|
this.fill(context);
|
|
this.stroke(context);
|
|
|
|
if(this.attrs.image) {
|
|
|
|
context.beginPath();
|
|
context.rect(0, 0, f.width, f.height);
|
|
context.closePath();
|
|
|
|
this.drawImage(context, this.attrs.image, f.x, f.y, f.width, f.height, 0, 0, f.width, f.height);
|
|
}
|
|
},
|
|
/**
|
|
* start sprite animation
|
|
* @name start
|
|
* @methodOf Kinetic.Sprite.prototype
|
|
*/
|
|
start: function() {
|
|
var that = this;
|
|
var layer = this.getLayer();
|
|
|
|
/*
|
|
* animation object has no executable function because
|
|
* the updates are done with a fixed FPS with the setInterval
|
|
* below. The anim object only needs the layer reference for
|
|
* redraw
|
|
*/
|
|
this.anim.node = layer;
|
|
|
|
this.interval = setInterval(function() {
|
|
var index = that.attrs.index;
|
|
that._updateIndex();
|
|
if(that.afterFrameFunc && index === that.afterFrameIndex) {
|
|
that.afterFrameFunc();
|
|
delete that.afterFrameFunc;
|
|
delete that.afterFrameIndex;
|
|
}
|
|
}, 1000 / this.attrs.frameRate);
|
|
|
|
this.anim.start();
|
|
},
|
|
/**
|
|
* stop sprite animation
|
|
* @name stop
|
|
* @methodOf Kinetic.Sprite.prototype
|
|
*/
|
|
stop: function() {
|
|
this.anim.stop();
|
|
clearInterval(this.interval);
|
|
},
|
|
/**
|
|
* set after frame event handler
|
|
* @name afterFrame
|
|
* @methodOf Kinetic.Sprite.prototype
|
|
* @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;
|
|
},
|
|
_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;
|
|
}
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Sprite, Kinetic.Shape);
|
|
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Sprite, ['animation', 'animations', 'index']);
|
|
|
|
/**
|
|
* set animation key
|
|
* @name setAnimation
|
|
* @methodOf Kinetic.Sprite.prototype
|
|
* @param {String} anim animation key
|
|
*/
|
|
|
|
/**
|
|
* set animations obect
|
|
* @name setAnimations
|
|
* @methodOf Kinetic.Sprite.prototype
|
|
* @param {Object} animations
|
|
*/
|
|
|
|
/**
|
|
* set animation frame index
|
|
* @name setIndex
|
|
* @methodOf Kinetic.Sprite.prototype
|
|
* @param {Integer} index frame index
|
|
*/
|
|
|
|
/**
|
|
* get animation key
|
|
* @name getAnimation
|
|
* @methodOf Kinetic.Sprite.prototype
|
|
*/
|
|
|
|
/**
|
|
* get animations object
|
|
* @name getAnimations
|
|
* @methodOf Kinetic.Sprite.prototype
|
|
*/
|
|
|
|
/**
|
|
* get animation frame index
|
|
* @name getIndex
|
|
* @methodOf Kinetic.Sprite.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Star
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Star constructor
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Star = function(config) {
|
|
this._initStar(config);
|
|
};
|
|
|
|
Kinetic.Star.prototype = {
|
|
_initStar: function(config) {
|
|
this.setDefaultAttrs({
|
|
numPoints: 0,
|
|
innerRadius: 0,
|
|
outerRadius: 0
|
|
});
|
|
|
|
this.shapeType = "Star";
|
|
config.drawFunc = this.drawFunc;
|
|
// call super constructor
|
|
Kinetic.Shape.call(this, config);
|
|
},
|
|
drawFunc: function(context) {
|
|
context.beginPath();
|
|
context.moveTo(0, 0 - this.attrs.outerRadius);
|
|
|
|
for(var n = 1; n < this.attrs.numPoints * 2; n++) {
|
|
var radius = n % 2 === 0 ? this.attrs.outerRadius : this.attrs.innerRadius;
|
|
var x = radius * Math.sin(n * Math.PI / this.attrs.numPoints);
|
|
var y = -1 * radius * Math.cos(n * Math.PI / this.attrs.numPoints);
|
|
context.lineTo(x, y);
|
|
}
|
|
context.closePath();
|
|
|
|
this.fill(context);
|
|
this.stroke(context);
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Star, Kinetic.Shape);
|
|
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Star, ['numPoints', 'innerRadius', 'outerRadius']);
|
|
|
|
/**
|
|
* set number of points
|
|
* @name setNumPoints
|
|
* @methodOf Kinetic.Star.prototype
|
|
* @param {Integer} points
|
|
*/
|
|
|
|
/**
|
|
* set outer radius
|
|
* @name setOuterRadius
|
|
* @methodOf Kinetic.Star.prototype
|
|
* @param {Number} radius
|
|
*/
|
|
|
|
/**
|
|
* set inner radius
|
|
* @name setInnerRadius
|
|
* @methodOf Kinetic.Star.prototype
|
|
* @param {Number} radius
|
|
*/
|
|
|
|
/**
|
|
* get number of points
|
|
* @name getNumPoints
|
|
* @methodOf Kinetic.Star.prototype
|
|
*/
|
|
|
|
/**
|
|
* get outer radius
|
|
* @name getOuterRadius
|
|
* @methodOf Kinetic.Star.prototype
|
|
*/
|
|
|
|
/**
|
|
* get inner radius
|
|
* @name getInnerRadius
|
|
* @methodOf Kinetic.Star.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// RegularPolygon
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* RegularPolygon constructor. Examples include triangles, squares, pentagons, hexagons, etc.
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.RegularPolygon = function(config) {
|
|
this._initRegularPolygon(config);
|
|
};
|
|
|
|
Kinetic.RegularPolygon.prototype = {
|
|
_initRegularPolygon: function(config) {
|
|
this.setDefaultAttrs({
|
|
radius: 0,
|
|
sides: 0
|
|
});
|
|
|
|
this.shapeType = "RegularPolygon";
|
|
config.drawFunc = this.drawFunc;
|
|
// call super constructor
|
|
Kinetic.Shape.call(this, config);
|
|
},
|
|
drawFunc: function(context) {
|
|
context.beginPath();
|
|
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.fill(context);
|
|
this.stroke(context);
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.RegularPolygon, Kinetic.Shape);
|
|
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.RegularPolygon, ['radius', 'sides']);
|
|
|
|
/**
|
|
* set radius
|
|
* @name setRadius
|
|
* @methodOf Kinetic.RegularPolygon.prototype
|
|
* @param {Number} radius
|
|
*/
|
|
|
|
/**
|
|
* set number of sides
|
|
* @name setSides
|
|
* @methodOf Kinetic.RegularPolygon.prototype
|
|
* @param {int} sides
|
|
*/
|
|
/**
|
|
* get radius
|
|
* @name getRadius
|
|
* @methodOf Kinetic.RegularPolygon.prototype
|
|
*/
|
|
|
|
/**
|
|
* get number of sides
|
|
* @name getSides
|
|
* @methodOf Kinetic.RegularPolygon.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// SVG Path
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Path constructor.
|
|
* @author Jason Follas
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.Path = function(config) {
|
|
this._initPath(config);
|
|
};
|
|
|
|
Kinetic.Path.prototype = {
|
|
_initPath: function(config) {
|
|
this.shapeType = "Path";
|
|
this.dataArray = [];
|
|
var that = this;
|
|
|
|
config.drawFunc = this.drawFunc;
|
|
// call super constructor
|
|
Kinetic.Shape.call(this, config);
|
|
this.dataArray = Kinetic.Path.parsePathData(this.attrs.data);
|
|
this.on('dataChange', function() {
|
|
that.dataArray = Kinetic.Path.parsePathData(that.attrs.data);
|
|
});
|
|
},
|
|
drawFunc: function(context) {
|
|
var ca = this.dataArray;
|
|
// context position
|
|
context.beginPath();
|
|
for(var n = 0; n < ca.length; n++) {
|
|
var c = ca[n].command;
|
|
var p = ca[n].points;
|
|
switch (c) {
|
|
case 'L':
|
|
context.lineTo(p[0], p[1]);
|
|
break;
|
|
case 'M':
|
|
context.moveTo(p[0], p[1]);
|
|
break;
|
|
case 'C':
|
|
context.bezierCurveTo(p[0], p[1], p[2], p[3], p[4], p[5]);
|
|
break;
|
|
case 'Q':
|
|
context.quadraticCurveTo(p[0], p[1], p[2], p[3]);
|
|
break;
|
|
case 'A':
|
|
var cx = p[0], cy = p[1], rx = p[2], ry = p[3], theta = p[4], dTheta = p[5], psi = p[6], fs = p[7];
|
|
|
|
var r = (rx > ry) ? rx : ry;
|
|
var scaleX = (rx > ry) ? 1 : rx / ry;
|
|
var scaleY = (rx > ry) ? ry / rx : 1;
|
|
|
|
context.translate(cx, cy);
|
|
context.rotate(psi);
|
|
context.scale(scaleX, scaleY);
|
|
context.arc(0, 0, r, theta, theta + dTheta, 1 - fs);
|
|
context.scale(1 / scaleX, 1 / scaleY);
|
|
context.rotate(-psi);
|
|
context.translate(-cx, -cy);
|
|
|
|
break;
|
|
case 'z':
|
|
context.closePath();
|
|
break;
|
|
}
|
|
}
|
|
this.fill(context);
|
|
this.stroke(context);
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.Path, Kinetic.Shape);
|
|
|
|
/*
|
|
* Utility methods written by jfollas to
|
|
* handle length and point measurements
|
|
*/
|
|
Kinetic.Path.getLineLength = function(x1, y1, x2, y2) {
|
|
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
|
|
};
|
|
Kinetic.Path.getPointOnLine = function(dist, P1x, P1y, P2x, P2y, fromX, fromY) {
|
|
if(fromX === undefined) {
|
|
fromX = P1x;
|
|
}
|
|
if(fromY === undefined) {
|
|
fromY = P1y;
|
|
}
|
|
|
|
var m = (P2y - P1y) / ((P2x - P1x) + 0.00000001);
|
|
var run = Math.sqrt(dist * dist / (1 + m * m));
|
|
var rise = m * run;
|
|
var pt;
|
|
|
|
if((fromY - P1y) / ((fromX - P1x) + 0.00000001) === m) {
|
|
pt = {
|
|
x: fromX + run,
|
|
y: fromY + rise
|
|
};
|
|
}
|
|
else {
|
|
var ix, iy;
|
|
|
|
var len = this.getLineLength(P1x, P1y, P2x, P2y);
|
|
if(len < 0.00000001) {
|
|
return undefined;
|
|
}
|
|
var u = (((fromX - P1x) * (P2x - P1x)) + ((fromY - P1y) * (P2y - P1y)));
|
|
u = u / (len * len);
|
|
ix = P1x + u * (P2x - P1x);
|
|
iy = P1y + u * (P2y - P1y);
|
|
|
|
var pRise = this.getLineLength(fromX, fromY, ix, iy);
|
|
var pRun = Math.sqrt(dist * dist - pRise * pRise);
|
|
run = Math.sqrt(pRun * pRun / (1 + m * m));
|
|
rise = m * run;
|
|
pt = {
|
|
x: ix + run,
|
|
y: iy + rise
|
|
};
|
|
}
|
|
|
|
return pt;
|
|
};
|
|
|
|
Kinetic.Path.getPointOnCubicBezier = function(pct, P1x, P1y, P2x, P2y, P3x, P3y, P4x, P4y) {
|
|
function CB1(t) {
|
|
return t * t * t;
|
|
}
|
|
function CB2(t) {
|
|
return 3 * t * t * (1 - t);
|
|
}
|
|
function CB3(t) {
|
|
return 3 * t * (1 - t) * (1 - t);
|
|
}
|
|
function CB4(t) {
|
|
return (1 - t) * (1 - t) * (1 - t);
|
|
}
|
|
var x = P4x * CB1(pct) + P3x * CB2(pct) + P2x * CB3(pct) + P1x * CB4(pct);
|
|
var y = P4y * CB1(pct) + P3y * CB2(pct) + P2y * CB3(pct) + P1y * CB4(pct);
|
|
|
|
return {
|
|
x: x,
|
|
y: y
|
|
};
|
|
};
|
|
Kinetic.Path.getPointOnQuadraticBezier = function(pct, P1x, P1y, P2x, P2y, P3x, P3y) {
|
|
function QB1(t) {
|
|
return t * t;
|
|
}
|
|
function QB2(t) {
|
|
return 2 * t * (1 - t);
|
|
}
|
|
function QB3(t) {
|
|
return (1 - t) * (1 - t);
|
|
}
|
|
var x = P3x * QB1(pct) + P2x * QB2(pct) + P1x * QB3(pct);
|
|
var y = P3y * QB1(pct) + P2y * QB2(pct) + P1y * QB3(pct);
|
|
|
|
return {
|
|
x: x,
|
|
y: y
|
|
};
|
|
};
|
|
Kinetic.Path.getPointOnEllipticalArc = function(cx, cy, rx, ry, theta, psi) {
|
|
var cosPsi = Math.cos(psi), sinPsi = Math.sin(psi);
|
|
var pt = {
|
|
x: rx * Math.cos(theta),
|
|
y: ry * Math.sin(theta)
|
|
};
|
|
return {
|
|
x: cx + (pt.x * cosPsi - pt.y * sinPsi),
|
|
y: cy + (pt.x * sinPsi + pt.y * cosPsi)
|
|
};
|
|
};
|
|
/**
|
|
* get parsed data array from the data
|
|
* string. V, v, H, h, and l data are converted to
|
|
* L data for the purpose of high performance Path
|
|
* rendering
|
|
*/
|
|
Kinetic.Path.parsePathData = function(data) {
|
|
// Path Data Segment must begin with a moveTo
|
|
//m (x y)+ Relative moveTo (subsequent points are treated as lineTo)
|
|
//M (x y)+ Absolute moveTo (subsequent points are treated as lineTo)
|
|
//l (x y)+ Relative lineTo
|
|
//L (x y)+ Absolute LineTo
|
|
//h (x)+ Relative horizontal lineTo
|
|
//H (x)+ Absolute horizontal lineTo
|
|
//v (y)+ Relative vertical lineTo
|
|
//V (y)+ Absolute vertical lineTo
|
|
//z (closepath)
|
|
//Z (closepath)
|
|
//c (x1 y1 x2 y2 x y)+ Relative Bezier curve
|
|
//C (x1 y1 x2 y2 x y)+ Absolute Bezier curve
|
|
//q (x1 y1 x y)+ Relative Quadratic Bezier
|
|
//Q (x1 y1 x y)+ Absolute Quadratic Bezier
|
|
//t (x y)+ Shorthand/Smooth Relative Quadratic Bezier
|
|
//T (x y)+ Shorthand/Smooth Absolute Quadratic Bezier
|
|
//s (x2 y2 x y)+ Shorthand/Smooth Relative Bezier curve
|
|
//S (x2 y2 x y)+ Shorthand/Smooth Absolute Bezier curve
|
|
//a (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Relative Elliptical Arc
|
|
//A (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Absolute Elliptical Arc
|
|
|
|
// return early if data is not defined
|
|
if(!data) {
|
|
return [];
|
|
}
|
|
|
|
// command string
|
|
var cs = data;
|
|
|
|
// command chars
|
|
var cc = ['m', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', 'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A'];
|
|
// convert white spaces to commas
|
|
cs = cs.replace(new RegExp(' ', 'g'), ',');
|
|
// create pipes so that we can split the data
|
|
for(var n = 0; n < cc.length; n++) {
|
|
cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]);
|
|
}
|
|
// create array
|
|
var arr = cs.split('|');
|
|
var ca = [];
|
|
// init context point
|
|
var cpx = 0;
|
|
var cpy = 0;
|
|
for(var n = 1; n < arr.length; n++) {
|
|
var str = arr[n];
|
|
var c = str.charAt(0);
|
|
str = str.slice(1);
|
|
// remove ,- for consistency
|
|
str = str.replace(new RegExp(',-', 'g'), '-');
|
|
// add commas so that it's easy to split
|
|
str = str.replace(new RegExp('-', 'g'), ',-');
|
|
str = str.replace(new RegExp('e,-', 'g'), 'e-');
|
|
var p = str.split(',');
|
|
if(p.length > 0 && p[0] === '') {
|
|
p.shift();
|
|
}
|
|
// convert strings to floats
|
|
for(var i = 0; i < p.length; i++) {
|
|
p[i] = parseFloat(p[i]);
|
|
}
|
|
while(p.length > 0) {
|
|
if(isNaN(p[0]))// case for a trailing comma before next command
|
|
break;
|
|
|
|
var cmd = null;
|
|
var points = [];
|
|
var startX = cpx, startY = cpy;
|
|
|
|
// convert l, H, h, V, and v to L
|
|
switch (c) {
|
|
|
|
// Note: Keep the lineTo's above the moveTo's in this switch
|
|
case 'l':
|
|
cpx += p.shift();
|
|
cpy += p.shift();
|
|
cmd = 'L';
|
|
points.push(cpx, cpy);
|
|
break;
|
|
case 'L':
|
|
cpx = p.shift();
|
|
cpy = p.shift();
|
|
points.push(cpx, cpy);
|
|
break;
|
|
|
|
// Note: lineTo handlers need to be above this point
|
|
case 'm':
|
|
cpx += p.shift();
|
|
cpy += p.shift();
|
|
cmd = 'M';
|
|
points.push(cpx, cpy);
|
|
c = 'l';
|
|
// subsequent points are treated as relative lineTo
|
|
break;
|
|
case 'M':
|
|
cpx = p.shift();
|
|
cpy = p.shift();
|
|
cmd = 'M';
|
|
points.push(cpx, cpy);
|
|
c = 'L';
|
|
// subsequent points are treated as absolute lineTo
|
|
break;
|
|
|
|
case 'h':
|
|
cpx += p.shift();
|
|
cmd = 'L';
|
|
points.push(cpx, cpy);
|
|
break;
|
|
case 'H':
|
|
cpx = p.shift();
|
|
cmd = 'L';
|
|
points.push(cpx, cpy);
|
|
break;
|
|
case 'v':
|
|
cpy += p.shift();
|
|
cmd = 'L';
|
|
points.push(cpx, cpy);
|
|
break;
|
|
case 'V':
|
|
cpy = p.shift();
|
|
cmd = 'L';
|
|
points.push(cpx, cpy);
|
|
break;
|
|
case 'C':
|
|
points.push(p.shift(), p.shift(), p.shift(), p.shift());
|
|
cpx = p.shift();
|
|
cpy = p.shift();
|
|
points.push(cpx, cpy);
|
|
break;
|
|
case 'c':
|
|
points.push(cpx + p.shift(), cpy + p.shift(), cpx + p.shift(), cpy + p.shift());
|
|
cpx += p.shift();
|
|
cpy += p.shift();
|
|
cmd = 'C';
|
|
points.push(cpx, cpy);
|
|
break;
|
|
case 'S':
|
|
var ctlPtx = cpx, ctlPty = cpy;
|
|
var prevCmd = ca[ca.length - 1];
|
|
if(prevCmd.command === 'C') {
|
|
ctlPtx = cpx + (cpx - prevCmd.points[2]);
|
|
ctlPty = cpy + (cpy - prevCmd.points[3]);
|
|
}
|
|
points.push(ctlPtx, ctlPty, p.shift(), p.shift());
|
|
cpx = p.shift();
|
|
cpy = p.shift();
|
|
cmd = 'C';
|
|
points.push(cpx, cpy);
|
|
break;
|
|
case 's':
|
|
var ctlPtx = cpx, ctlPty = cpy;
|
|
var prevCmd = ca[ca.length - 1];
|
|
if(prevCmd.command === 'C') {
|
|
ctlPtx = cpx + (cpx - prevCmd.points[2]);
|
|
ctlPty = cpy + (cpy - prevCmd.points[3]);
|
|
}
|
|
points.push(ctlPtx, ctlPty, cpx + p.shift(), cpy + p.shift());
|
|
cpx += p.shift();
|
|
cpy += p.shift();
|
|
cmd = 'C';
|
|
points.push(cpx, cpy);
|
|
break;
|
|
case 'Q':
|
|
points.push(p.shift(), p.shift());
|
|
cpx = p.shift();
|
|
cpy = p.shift();
|
|
points.push(cpx, cpy);
|
|
break;
|
|
case 'q':
|
|
points.push(cpx + p.shift(), cpy + p.shift());
|
|
cpx += p.shift();
|
|
cpy += p.shift();
|
|
cmd = 'Q';
|
|
points.push(cpx, cpy);
|
|
break;
|
|
case 'T':
|
|
var ctlPtx = cpx, ctlPty = cpy;
|
|
var prevCmd = ca[ca.length - 1];
|
|
if(prevCmd.command === 'Q') {
|
|
ctlPtx = cpx + (cpx - prevCmd.points[0]);
|
|
ctlPty = cpy + (cpy - prevCmd.points[1]);
|
|
}
|
|
cpx = p.shift();
|
|
cpy = p.shift();
|
|
cmd = 'Q';
|
|
points.push(ctlPtx, ctlPty, cpx, cpy);
|
|
break;
|
|
case 't':
|
|
var ctlPtx = cpx, ctlPty = cpy;
|
|
var prevCmd = ca[ca.length - 1];
|
|
if(prevCmd.command === 'Q') {
|
|
ctlPtx = cpx + (cpx - prevCmd.points[0]);
|
|
ctlPty = cpy + (cpy - prevCmd.points[1]);
|
|
}
|
|
cpx += p.shift();
|
|
cpy += p.shift();
|
|
cmd = 'Q';
|
|
points.push(ctlPtx, ctlPty, cpx, cpy);
|
|
break;
|
|
case 'A':
|
|
var rx = p.shift(), ry = p.shift(), psi = p.shift(), fa = p.shift(), fs = p.shift();
|
|
var x1 = cpx, y1 = cpy;
|
|
cpx = p.shift(), cpy = p.shift();
|
|
cmd = 'A';
|
|
points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi);
|
|
break;
|
|
case 'a':
|
|
var rx = p.shift(), ry = p.shift(), psi = p.shift(), fa = p.shift(), fs = p.shift();
|
|
var x1 = cpx, y1 = cpy;
|
|
cpx += p.shift(), cpy += p.shift();
|
|
cmd = 'A';
|
|
points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi);
|
|
break;
|
|
}
|
|
|
|
ca.push({
|
|
command: cmd || c,
|
|
points: points,
|
|
start: {
|
|
x: startX,
|
|
y: startY
|
|
},
|
|
pathLength: this.calcLength(startX, startY, cmd || c, points)
|
|
});
|
|
}
|
|
|
|
if(c === 'z' || c === 'Z') {
|
|
ca.push({
|
|
command: 'z',
|
|
points: [],
|
|
start: undefined,
|
|
pathLength: 0
|
|
});
|
|
}
|
|
}
|
|
|
|
return ca;
|
|
};
|
|
Kinetic.Path.calcLength = function(x, y, cmd, points) {
|
|
var len, p1, p2;
|
|
var path = Kinetic.Path;
|
|
|
|
switch (cmd) {
|
|
case 'L':
|
|
return path.getLineLength(x, y, points[0], points[1]);
|
|
case 'C':
|
|
// Approximates by breaking curve into 100 line segments
|
|
len = 0.0;
|
|
p1 = path.getPointOnCubicBezier(0, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
|
|
for( t = 0.01; t <= 1; t += 0.01) {
|
|
p2 = path.getPointOnCubicBezier(t, x, y, points[0], points[1], points[2], points[3], points[4], points[5]);
|
|
len += path.getLineLength(p1.x, p1.y, p2.x, p2.y);
|
|
p1 = p2;
|
|
}
|
|
return len;
|
|
case 'Q':
|
|
// Approximates by breaking curve into 100 line segments
|
|
len = 0.0;
|
|
p1 = path.getPointOnQuadraticBezier(0, x, y, points[0], points[1], points[2], points[3]);
|
|
for( t = 0.01; t <= 1; t += 0.01) {
|
|
p2 = path.getPointOnQuadraticBezier(t, x, y, points[0], points[1], points[2], points[3]);
|
|
len += path.getLineLength(p1.x, p1.y, p2.x, p2.y);
|
|
p1 = p2;
|
|
}
|
|
return len;
|
|
case 'A':
|
|
// Approximates by breaking curve into line segments
|
|
len = 0.0;
|
|
var start = points[4];
|
|
// 4 = theta
|
|
var dTheta = points[5];
|
|
// 5 = dTheta
|
|
var end = points[4] + dTheta;
|
|
var inc = Math.PI / 180.0;
|
|
// 1 degree resolution
|
|
if(Math.abs(start - end) < inc) {
|
|
inc = Math.abs(start - end);
|
|
}
|
|
// Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi
|
|
p1 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], start, 0);
|
|
if(dTheta < 0) {// clockwise
|
|
for( t = start - inc; t > end; t -= inc) {
|
|
p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
|
|
len += path.getLineLength(p1.x, p1.y, p2.x, p2.y);
|
|
p1 = p2;
|
|
}
|
|
}
|
|
else {// counter-clockwise
|
|
for( t = start + inc; t < end; t += inc) {
|
|
p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0);
|
|
len += path.getLineLength(p1.x, p1.y, p2.x, p2.y);
|
|
p1 = p2;
|
|
}
|
|
}
|
|
p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], end, 0);
|
|
len += path.getLineLength(p1.x, p1.y, p2.x, p2.y);
|
|
|
|
return len;
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
Kinetic.Path.convertEndpointToCenterParameterization = function(x1, y1, x2, y2, fa, fs, rx, ry, psiDeg) {
|
|
// Derived from: http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
|
var psi = psiDeg * (Math.PI / 180.0);
|
|
var xp = Math.cos(psi) * (x1 - x2) / 2.0 + Math.sin(psi) * (y1 - y2) / 2.0;
|
|
var yp = -1 * Math.sin(psi) * (x1 - x2) / 2.0 + Math.cos(psi) * (y1 - y2) / 2.0;
|
|
|
|
var lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry);
|
|
|
|
if(lambda > 1) {
|
|
rx *= Math.sqrt(lambda);
|
|
ry *= Math.sqrt(lambda);
|
|
}
|
|
|
|
var f = Math.sqrt((((rx * rx) * (ry * ry)) - ((rx * rx) * (yp * yp)) - ((ry * ry) * (xp * xp))) / ((rx * rx) * (yp * yp) + (ry * ry) * (xp * xp)));
|
|
|
|
if(fa == fs) {
|
|
f *= -1;
|
|
}
|
|
if(isNaN(f)) {
|
|
f = 0;
|
|
}
|
|
|
|
var cxp = f * rx * yp / ry;
|
|
var cyp = f * -ry * xp / rx;
|
|
|
|
var cx = (x1 + x2) / 2.0 + Math.cos(psi) * cxp - Math.sin(psi) * cyp;
|
|
var cy = (y1 + y2) / 2.0 + Math.sin(psi) * cxp + Math.cos(psi) * cyp;
|
|
|
|
var vMag = function(v) {
|
|
return Math.sqrt(v[0] * v[0] + v[1] * v[1]);
|
|
};
|
|
var vRatio = function(u, v) {
|
|
return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v));
|
|
};
|
|
var vAngle = function(u, v) {
|
|
return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vRatio(u, v));
|
|
};
|
|
var theta = vAngle([1, 0], [(xp - cxp) / rx, (yp - cyp) / ry]);
|
|
var u = [(xp - cxp) / rx, (yp - cyp) / ry];
|
|
var v = [(-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry];
|
|
var dTheta = vAngle(u, v);
|
|
|
|
if(vRatio(u, v) <= -1) {
|
|
dTheta = Math.PI;
|
|
}
|
|
if(vRatio(u, v) >= 1) {
|
|
dTheta = 0;
|
|
}
|
|
if(fs === 0 && dTheta > 0) {
|
|
dTheta = dTheta - 2 * Math.PI;
|
|
}
|
|
if(fs == 1 && dTheta < 0) {
|
|
dTheta = dTheta + 2 * Math.PI;
|
|
}
|
|
return [cx, cy, rx, ry, theta, dTheta, psi, fs];
|
|
};
|
|
|
|
// add getters setters
|
|
Kinetic.Node.addGettersSetters(Kinetic.Path, ['data']);
|
|
|
|
/**
|
|
* set SVG path data string. This method
|
|
* also automatically parses the data string
|
|
* into a data array. Currently supported SVG data:
|
|
* M, m, L, l, H, h, V, v, Q, q, T, t, C, c, S, s, A, a, Z, z
|
|
* @name setData
|
|
* @methodOf Kinetic.Path.prototype
|
|
* @param {String} SVG path command string
|
|
*/
|
|
|
|
/**
|
|
* get SVG path data string
|
|
* @name getData
|
|
* @methodOf Kinetic.Path.prototype
|
|
*/
|
|
///////////////////////////////////////////////////////////////////////
|
|
// Text Path
|
|
///////////////////////////////////////////////////////////////////////
|
|
/**
|
|
* Path constructor.
|
|
* @author Jason Follas
|
|
* @constructor
|
|
* @augments Kinetic.Shape
|
|
* @param {Object} config
|
|
*/
|
|
Kinetic.TextPath = function(config) {
|
|
this._initTextPath(config);
|
|
};
|
|
|
|
Kinetic.TextPath.prototype = {
|
|
_initTextPath: function(config) {
|
|
this.setDefaultAttrs({
|
|
fontFamily: 'Calibri',
|
|
fontSize: 12,
|
|
fontStyle: 'normal',
|
|
detectionType: 'path',
|
|
text: ''
|
|
});
|
|
|
|
this.dummyCanvas = document.createElement('canvas');
|
|
this.shapeType = "TextPath";
|
|
this.dataArray = [];
|
|
var that = this;
|
|
|
|
config.drawFunc = this.drawFunc;
|
|
// call super constructor
|
|
Kinetic.Shape.call(this, config);
|
|
this.dataArray = Kinetic.Path.parsePathData(this.attrs.data);
|
|
this.on('dataChange', function() {
|
|
that.dataArray = Kinetic.Path.parsePathData(this.attrs.data);
|
|
});
|
|
// update text data for certain attr changes
|
|
var attrs = ['text', 'textStroke', 'textStrokeWidth'];
|
|
for(var n = 0; n < attrs.length; n++) {
|
|
var attr = attrs[n];
|
|
this.on(attr + 'Change', that._setTextData);
|
|
}
|
|
that._setTextData();
|
|
},
|
|
drawFunc: function(context) {
|
|
var charArr = this.charArr;
|
|
|
|
context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily;
|
|
context.textBaseline = 'middle';
|
|
context.textAlign = 'left';
|
|
context.save();
|
|
|
|
var glyphInfo = this.glyphInfo;
|
|
|
|
for(var i = 0; i < glyphInfo.length; i++) {
|
|
context.save();
|
|
|
|
var p0 = glyphInfo[i].p0;
|
|
var p1 = glyphInfo[i].p1;
|
|
var ht = parseFloat(this.attrs.fontSize);
|
|
|
|
context.translate(p0.x, p0.y);
|
|
|
|
context.rotate(glyphInfo[i].rotation);
|
|
|
|
this.fillText(context, glyphInfo[i].text);
|
|
this.strokeText(context, glyphInfo[i].text);
|
|
|
|
context.restore();
|
|
|
|
//// To assist with debugging visually, uncomment following
|
|
// context.beginPath();
|
|
// if (i % 2)
|
|
// context.strokeStyle = 'cyan';
|
|
// else
|
|
// context.strokeStyle = 'green';
|
|
|
|
// context.moveTo(p0.x, p0.y);
|
|
// context.lineTo(p1.x, p1.y);
|
|
// context.stroke();
|
|
}
|
|
|
|
context.restore();
|
|
},
|
|
/**
|
|
* get text width in pixels
|
|
* @name getTextWidth
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
*/
|
|
getTextWidth: function() {
|
|
return this.textWidth;
|
|
},
|
|
/**
|
|
* get text height in pixels
|
|
* @name getTextHeight
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
*/
|
|
getTextHeight: function() {
|
|
return this.textHeight;
|
|
},
|
|
_getTextSize: function(text) {
|
|
var dummyCanvas = this.dummyCanvas;
|
|
var context = dummyCanvas.getContext('2d');
|
|
|
|
context.save();
|
|
|
|
context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily;
|
|
var metrics = context.measureText(text);
|
|
|
|
context.restore();
|
|
|
|
return {
|
|
width: metrics.width,
|
|
height: parseInt(this.attrs.fontSize, 10)
|
|
};
|
|
},
|
|
/**
|
|
* set text data.
|
|
*/
|
|
_setTextData: function() {
|
|
|
|
var that = this;
|
|
var size = this._getTextSize(this.attrs.text);
|
|
this.textWidth = size.width;
|
|
this.textHeight = size.height;
|
|
|
|
this.glyphInfo = [];
|
|
|
|
var charArr = this.attrs.text.split('');
|
|
|
|
var p0, p1, pathCmd;
|
|
|
|
var pIndex = -1;
|
|
var currentT = 0;
|
|
|
|
var getNextPathSegment = function() {
|
|
currentT = 0;
|
|
var pathData = that.dataArray;
|
|
|
|
for(var i = pIndex + 1; i < pathData.length; i++) {
|
|
if(pathData[i].pathLength > 0) {
|
|
pIndex = i;
|
|
|
|
return pathData[i];
|
|
}
|
|
else if(pathData[i].command == 'M') {
|
|
p0 = {
|
|
x: pathData[i].points[0],
|
|
y: pathData[i].points[1]
|
|
};
|
|
}
|
|
}
|
|
|
|
return {};
|
|
};
|
|
var findSegmentToFitCharacter = function(c, before) {
|
|
|
|
var glyphWidth = that._getTextSize(c).width;
|
|
|
|
var currLen = 0;
|
|
var attempts = 0;
|
|
var needNextSegment = false;
|
|
p1 = undefined;
|
|
while(Math.abs(glyphWidth - currLen) / glyphWidth > 0.01 && attempts < 25) {
|
|
attempts++;
|
|
var cumulativePathLength = currLen;
|
|
while(pathCmd === undefined) {
|
|
pathCmd = getNextPathSegment();
|
|
|
|
if(pathCmd && cumulativePathLength + pathCmd.pathLength < glyphWidth) {
|
|
cumulativePathLength += pathCmd.pathLength;
|
|
pathCmd = undefined;
|
|
}
|
|
}
|
|
|
|
if(pathCmd === {} || p0 === undefined)
|
|
return undefined;
|
|
|
|
var needNewSegment = false;
|
|
|
|
switch (pathCmd.command) {
|
|
case 'L':
|
|
if(Kinetic.Path.getLineLength(p0.x, p0.y, pathCmd.points[0], pathCmd.points[1]) > glyphWidth) {
|
|
p1 = Kinetic.Path.getPointOnLine(glyphWidth, p0.x, p0.y, pathCmd.points[0], pathCmd.points[1], p0.x, p0.y);
|
|
}
|
|
else
|
|
pathCmd = undefined;
|
|
break;
|
|
case 'A':
|
|
|
|
var start = pathCmd.points[4];
|
|
// 4 = theta
|
|
var dTheta = pathCmd.points[5];
|
|
// 5 = dTheta
|
|
var end = pathCmd.points[4] + dTheta;
|
|
|
|
if(currentT === 0)
|
|
currentT = start + 0.00000001;
|
|
// Just in case start is 0
|
|
else if(glyphWidth > currLen)
|
|
currentT += (Math.PI / 180.0) * dTheta / Math.abs(dTheta);
|
|
else
|
|
currentT -= Math.PI / 360.0 * dTheta / Math.abs(dTheta);
|
|
|
|
if(Math.abs(currentT) > Math.abs(end)) {
|
|
currentT = end;
|
|
needNewSegment = true;
|
|
}
|
|
p1 = Kinetic.Path.getPointOnEllipticalArc(pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], currentT, pathCmd.points[6]);
|
|
break;
|
|
case 'C':
|
|
if(currentT === 0) {
|
|
if(glyphWidth > pathCmd.pathLength)
|
|
currentT = 0.00000001;
|
|
else
|
|
currentT = glyphWidth / pathCmd.pathLength;
|
|
}
|
|
else if(glyphWidth > currLen)
|
|
currentT += (glyphWidth - currLen) / pathCmd.pathLength;
|
|
else
|
|
currentT -= (currLen - glyphWidth) / pathCmd.pathLength;
|
|
|
|
if(currentT > 1.0) {
|
|
currentT = 1.0;
|
|
needNewSegment = true;
|
|
}
|
|
p1 = Kinetic.Path.getPointOnCubicBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], pathCmd.points[4], pathCmd.points[5]);
|
|
break;
|
|
case 'Q':
|
|
if(currentT === 0)
|
|
currentT = glyphWidth / pathCmd.pathLength;
|
|
else if(glyphWidth > currLen)
|
|
currentT += (glyphWidth - currLen) / pathCmd.pathLength;
|
|
else
|
|
currentT -= (currLen - glyphWidth) / pathCmd.pathLength;
|
|
|
|
if(currentT > 1.0) {
|
|
currentT = 1.0;
|
|
needNewSegment = true;
|
|
}
|
|
p1 = Kinetic.Path.getPointOnQuadraticBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3]);
|
|
break;
|
|
|
|
}
|
|
|
|
if(p1 !== undefined) {
|
|
currLen = Kinetic.Path.getLineLength(p0.x, p0.y, p1.x, p1.y);
|
|
}
|
|
|
|
if(needNewSegment) {
|
|
needNewSegment = false;
|
|
pathCmd = undefined;
|
|
}
|
|
}
|
|
};
|
|
for(var i = 0; i < charArr.length; i++) {
|
|
|
|
// Find p1 such that line segment between p0 and p1 is approx. width of glyph
|
|
findSegmentToFitCharacter(charArr[i]);
|
|
|
|
if(p0 === undefined || p1 === undefined)
|
|
break;
|
|
|
|
var width = Kinetic.Path.getLineLength(p0.x, p0.y, p1.x, p1.y);
|
|
|
|
// Note: Since glyphs are rendered one at a time, any kerning pair data built into the font will not be used.
|
|
// Can foresee having a rough pair table built in that the developer can override as needed.
|
|
|
|
var kern = 0;
|
|
// placeholder for future implementation
|
|
|
|
var midpoint = Kinetic.Path.getPointOnLine(kern + width / 2.0, p0.x, p0.y, p1.x, p1.y);
|
|
|
|
var rotation = Math.atan2((p1.y - p0.y), (p1.x - p0.x));
|
|
this.glyphInfo.push({
|
|
transposeX: midpoint.x,
|
|
transposeY: midpoint.y,
|
|
text: charArr[i],
|
|
rotation: rotation,
|
|
p0: p0,
|
|
p1: p1
|
|
});
|
|
p0 = p1;
|
|
}
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Kinetic.TextPath, Kinetic.Shape);
|
|
|
|
// add setters and getters
|
|
Kinetic.Node.addGettersSetters(Kinetic.TextPath, ['fontFamily', 'fontSize', 'fontStyle', 'textFill', 'textStroke', 'textStrokeWidth', 'text']);
|
|
|
|
/**
|
|
* set font family
|
|
* @name setFontFamily
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
* @param {String} fontFamily
|
|
*/
|
|
|
|
/**
|
|
* set font size
|
|
* @name setFontSize
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
* @param {int} fontSize
|
|
*/
|
|
|
|
/**
|
|
* set font style. Can be "normal", "italic", or "bold". "normal" is the default.
|
|
* @name setFontStyle
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
* @param {String} fontStyle
|
|
*/
|
|
|
|
/**
|
|
* set text fill color
|
|
* @name setTextFill
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
* @param {String} textFill
|
|
*/
|
|
|
|
/**
|
|
* set text stroke color
|
|
* @name setFontStroke
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
* @param {String} textStroke
|
|
*/
|
|
|
|
/**
|
|
* set text stroke width
|
|
* @name setTextStrokeWidth
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
* @param {int} textStrokeWidth
|
|
*/
|
|
|
|
/**
|
|
* set text
|
|
* @name setText
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
* @param {String} text
|
|
*/
|
|
|
|
/**
|
|
* get font family
|
|
* @name getFontFamily
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
*/
|
|
|
|
/**
|
|
* get font size
|
|
* @name getFontSize
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
*/
|
|
|
|
/**
|
|
* get font style
|
|
* @name getFontStyle
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
*/
|
|
|
|
/**
|
|
* get text fill color
|
|
* @name getTextFill
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
*/
|
|
|
|
/**
|
|
* get text stroke color
|
|
* @name getTextStroke
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
*/
|
|
|
|
/**
|
|
* get text stroke width
|
|
* @name getTextStrokeWidth
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
*/
|
|
|
|
/**
|
|
* get text
|
|
* @name getText
|
|
* @methodOf Kinetic.TextPath.prototype
|
|
*/
|