PHP Classes

File: kinetic/js/libs/kinetic.js

Recommend this page to a friend!
  Classes of Vitalij Mik   PHP Tiled to CraftyJS   kinetic/js/libs/kinetic.js   Download  
File: kinetic/js/libs/kinetic.js
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: PHP Tiled to CraftyJS
Convert game level tiled maps CraftyJS components
Author: By
Last change:
Date: 2 years ago
Size: 198,448 bytes
 

Contents

Class file image Download
/** * KineticJS JavaScript Library v4.0.1 * http://www.kineticjs.com/ * Copyright 2012, Eric Rowell * Licensed under the MIT or GPL Version 2 licenses. * Date: Aug 26 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; } }; /////////////////////////////////////////////////////////////////////// // 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.&nbsp; 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) { stage.content.removeChild(this.canvas.element); stage.content.insertBefore(this.canvas.element, stage.getChildren()[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; this.parent.children.splice(index, 1); this.parent.children.unshift(this); this.parent._setChildrenIndices(); if(this.nodeType === 'Layer') { var stage = this.getStage(); if(stage) { stage.content.removeChild(this.canvas.element); stage.content.insertBefore(this.canvas.element, stage.getChildren()[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.&nbsp; 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 = []; 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; }, /** * 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 = []; 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(); }, /** * 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.&nbsp; 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.&nbsp; 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.&nbsp; 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 */