diff --git a/src/Global.js b/src/Global.js index f3cc72c8..0b744347 100644 --- a/src/Global.js +++ b/src/Global.js @@ -93,29 +93,6 @@ var Kinetic = {}; (function() { // can remove the `root` use and the passing `this` as the first arg to // the top function. -( function(root, factory) { - if( typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like enviroments that support module.exports, - // like Node. - module.exports = factory(require('b')); - } - else if( typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['b'], factory); - } - else { - // Browser globals (root is window) - root.returnExports = factory(root.b); - } -}(this, function(b) { - //use b in some fashion. - - // Just return a value to define the module export. - // This example returns an object, but the module - // can return a function as the exported value. - return {}; -})); // if the module has no dependencies, the above pattern can be simplified to ( function(root, factory) { if( typeof exports === 'object') { diff --git a/tests/html/index.html b/tests/html/index.html index 1e243e8e..30d362b8 100644 --- a/tests/html/index.html +++ b/tests/html/index.html @@ -7,6 +7,7 @@
  • Functional Tests
  • Manual Tests
  • Performance Tests
  • +
  • Special Tests
  • \ No newline at end of file diff --git a/tests/html/special/index.html b/tests/html/special/index.html index c2049719..0b8988c0 100644 --- a/tests/html/special/index.html +++ b/tests/html/special/index.html @@ -1,9 +1,13 @@ -coreCustomBuild.html
    -dragAndDropCustomBuild.html
    -transitionCustomBuild.html
    -stageBelowFold.html
    -tallStage.html
    +

    Special Tests

    + \ No newline at end of file diff --git a/tests/html/special/requirejs.html b/tests/html/special/requirejs.html new file mode 100644 index 00000000..48544ae2 --- /dev/null +++ b/tests/html/special/requirejs.html @@ -0,0 +1,13 @@ + + + + My Sample Project + + + + +

    My Sample Project

    +
    + + \ No newline at end of file diff --git a/tests/html/special/scripts/kinetic-vcurrent.js b/tests/html/special/scripts/kinetic-vcurrent.js new file mode 100644 index 00000000..0729af31 --- /dev/null +++ b/tests/html/special/scripts/kinetic-vcurrent.js @@ -0,0 +1,6976 @@ +/** + * KineticJS JavaScript Library vcurrent + * http://www.kineticjs.com/ + * Copyright 2012, Eric Rowell + * Licensed under the MIT or GPL Version 2 licenses. + * Date: Dec 01 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. + */ +/** + * @namespace + */ +var Kinetic = {}; (function() { + Kinetic.version = 'current'; + /** + * @namespace + */ + Kinetic.Filters = {}; + Kinetic.Plugins = {}; + Kinetic.Global = { + stages: [], + idCounter: 0, + tempNodes: {}, + //shapes hash. rgb keys and shape values + shapes: {}, + warn: function(str) { + /* + * IE9 on Windows7 64bit will throw a JS error + * if we don't use window.console in the conditional + */ + if(window.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]; + } + }; +})(); + +// Uses Node, AMD or browser globals to create a module. + +// If you want something that will work in other stricter CommonJS environments, +// or if you need to create a circular dependency, see commonJsStrict.js + +// Defines a module "returnExports" that depends another module called "b". +// Note that the name of the module is implied by the file name. It is best +// if the file name and the exported global have matching names. + +// If the 'b' module also uses this type of boilerplate, then +// in the browser, it will create a global .b that is used below. + +// If you do not want to support the browser global path, then you +// can remove the `root` use and the passing `this` as the first arg to +// the top function. + +// if the module has no dependencies, the above pattern can be simplified to +( function(root, factory) { + if( typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like enviroments that support module.exports, + // like Node. + module.exports = factory(); + } + else if( typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory); + } + else { + // Browser globals (root is window) + root.returnExports = factory(); + } +}(this, function() { + + // Just return a value to define the module export. + // This example returns an object, but the module + // can return a function as the exported value. + return Kinetic; +})); + +(function() { + /* + * utilities that handle data type detection, conversion, and manipulation + */ + 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(); + /** @ignore */ + 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(); + /** @ignore */ + 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); + }, + // o1 takes precedence over o2 + _merge: function(o1, o2) { + var retObj = this._clone(o2); + for(var key in o1) { + if(this._isObject(o1[key])) { + retObj[key] = this._merge(o1[key], retObj[key]); + } + else { + retObj[key] = o1[key]; + } + } + return retObj; + }, + // deep clone + _clone: function(obj) { + var retObj = {}; + for(var key in obj) { + if(this._isObject(obj[key])) { + retObj[key] = this._clone(obj[key]); + } + else { + retObj[key] = obj[key]; + } + } + return retObj; + }, + _degToRad: function(deg) { + return deg * Math.PI / 180; + }, + _radToDeg: function(rad) { + return rad * 180 / Math.PI; + } + }; +})(); + +(function() { + /** + * Canvas wrapper constructor + * @constructor + * @param {Number} width + * @param {Number} height + */ + Kinetic.Canvas = function(width, height, isHit) { + this.element = document.createElement('canvas'); + this.context = this.element.getContext('2d'); + + // set dimensions + this.element.width = width || 0; + this.element.height = height || 0; + + this.context.renderer = isHit ? new Kinetic.HitRenderer(this.context) : new Kinetic.SceneRenderer(this.context); + }; + + 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) { + try { + return this.element.toDataURL(); + } + catch(e) { + Kinetic.Global.warn('Unable to get data URL. ' + e.message) + return ''; + } + } + } + }; + + Kinetic.SceneRenderer = function(context) { + this.context = context; + }; + + Kinetic.SceneRenderer.prototype = { + _fill: function(shape, skipShadow) { + var context = this.context, fill = shape.getFill(), fillType = shape._getFillType(fill), shadow = shape.getShadow(); + if(fill) { + context.save(); + + if(!skipShadow && shadow) { + this._applyShadow(shape); + } + var s = fill.start; + var e = fill.end; + + // color fill + switch(fillType) { + case 'COLOR': + context.fillStyle = fill; + context.fill(context); + break; + case 'PATTERN': + 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); + } file:///C:/Users/Eric/Documents/Eric/workspaces/KineticJS/dist/kinetic-current.js + + context.fillStyle = context.createPattern(fill.image, repeat); + context.fill(context); + break; + case 'LINEAR_GRADIENT': + 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); + + break; + case 'RADIAL_GRADIENT': + 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); + break; + default: + context.fillStyle = 'black'; + context.fill(context); + break; + } + + context.restore(); + + if(!skipShadow && shadow && shadow.opacity) { + this._fill(shape, true); + } + } + }, + _stroke: function(shape, skipShadow) { + var context = this.context, stroke = shape.getStroke(), strokeWidth = shape.getStrokeWidth(), shadow = shape.getShadow(); + if(stroke || strokeWidth) { + context.save(); + if(!skipShadow && shadow) { + this._applyShadow(shape); + } + context.lineWidth = strokeWidth || 2; + context.strokeStyle = stroke || 'black'; + context.stroke(context); + context.restore(); + + if(!skipShadow && shadow && shadow.opacity) { + this._stroke(shape, true); + } + } + }, + _applyShadow: function(shape) { + var context = this.context, shadow = shape.getShadow(); + if(shadow) { + var aa = shape.getAbsoluteOpacity(); + // defaults + var color = shadow.color || 'black'; + var blur = shadow.blur || 5; + var offset = shadow.offset || { + x: 0, + y: 0 + }; + + if(shadow.opacity) { + context.globalAlpha = shadow.opacity * aa; + } + context.shadowColor = color; + context.shadowBlur = blur; + context.shadowOffsetX = offset.x; + context.shadowOffsetY = offset.y; + } + } + }; + + Kinetic.HitRenderer = function(context) { + this.context = context; + }; + + Kinetic.HitRenderer.prototype = { + _fill: function(shape) { + var context = this.context; + context.save(); + context.fillStyle = '#' + shape.colorKey; + context.fill(context); + context.restore(); + }, + _stroke: function(shape) { + var context = this.context, stroke = shape.getStroke(), strokeWidth = shape.getStrokeWidth(); + if(stroke || strokeWidth) { + context.save(); + context.lineWidth = strokeWidth || 2; + context.strokeStyle = '#' + shape.colorKey; + context.stroke(context); + context.restore(); + } + } + }; +})(); + +(function() { + /* + * 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; + } + }; +})(); + +(function() { + /* + * 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. + */ + + /** + * Transform constructor + * @constructor + */ + 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; + } + }; +})(); + +(function() { + /** + * Collection constructor. Collection extends + * Array. This class is used in conjunction with get() + * @constructor + */ + Kinetic.Collection = function() { + var args = [].slice.call(arguments), length = args.length, i = 0; + + this.length = length; + for(; i < length; i++) { + this[i] = args[i]; + } + return this; + } + Kinetic.Collection.prototype = new Array(); + /** + * apply a method to all nodes in the array + * @name apply + * @methodOf Kinetic.Collection.prototype + * @param {String} method + * @param val + */ + Kinetic.Collection.prototype.apply = function(method) { + args = [].slice.call(arguments); + args.shift(); + for(var n = 0; n < this.length; n++) { + if(Kinetic.Type._isFunction(this[n][method])) { + this[n][method].apply(this[n], args); + } + } + }; + /** + * iterate through node array + * @name each + * @methodOf Kinetic.Collection.prototype + * @param {Function} func + */ + Kinetic.Collection.prototype.each = function(func) { + for(var n = 0; n < this.length; n++) { + func.call(this[n], n, this[n]); + } + }; +})(); + +(function() { + /** + * Grayscale Filter + * @function + * @memberOf Kinetic.Filters + * @param {Object} imageData + * @param {Object} config + */ + Kinetic.Filters.Grayscale = function(imageData, config) { + 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; + } + }; +})(); + +(function() { + /** + * Brighten Filter + * @function + * @memberOf Kinetic.Filters + * @param {Object} imageData + * @param {Object} config + * @param {Integer} config.val brightness number from -255 to 255.  Positive values increase the brightness and negative values decrease the brightness, making the image darker + */ + Kinetic.Filters.Brighten = function(imageData, config) { + var brightness = config.val || 0; + var data = imageData.data; + for(var i = 0; i < data.length; i += 4) { + // red + data[i] += brightness; + // green + data[i + 1] += brightness; + // blue + data[i + 2] += brightness; + } + }; +})(); + +(function() { + /** + * Invert Filter + * @function + * @memberOf Kinetic.Filters + * @param {Object} imageData + * @param {Object} config + */ + Kinetic.Filters.Invert = function(imageData, config) { + var data = imageData.data; + for(var i = 0; i < data.length; i += 4) { + // red + data[i] = 255 - data[i]; + // green + data[i + 1] = 255 - data[i + 1]; + // blue + data[i + 2] = 255 - data[i + 2]; + } + }; +})(); + +(function() { + /** + * Stage constructor. A stage is used to contain multiple layers and handle + * animations + * @constructor + * @augments Kinetic.Container + * @param {Function} func function executed on each animation frame + * @param {Kinetic.Node} [node] node to be redrawn.  Specifying a node will improve + * draw performance.  This can be a shape, a group, a layer, or the stage. + */ + Kinetic.Animation = function(func, node) { + this.func = func; + this.node = node; + this.id = Kinetic.Animation.animIdCounter++; + }; + /* + * Animation methods + */ + Kinetic.Animation.prototype = { + /** + * start animation + * @name start + * @methodOf Kinetic.Animation.prototype + */ + start: function() { + this.stop(); + 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.frame = { + time: 0, + timeDiff: 0, + lastTime: new Date().getTime(), + frameRate: 0 + }; + + Kinetic.Animation.fixedRequestAnimFrame = function(callback) { + window.setTimeout(callback, 1000 / 60); + }; + + 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); + break; + } + } + }; + Kinetic.Animation._updateFrameObject = function() { + var time = new Date().getTime(); + this.frame.timeDiff = time - this.frame.lastTime; + this.frame.lastTime = time; + this.frame.time += this.frame.timeDiff; + this.frame.frameRate = 1000 / this.frame.timeDiff; + }; + Kinetic.Animation._runFrames = function() { + this._updateFrameObject(); + 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]; + 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(this.frame); + } + } + + for(var key in nodes) { + nodes[key].draw(); + } + }; + Kinetic.Animation._animationLoop = function() { + if(this.animations.length > 0) { + this._runFrames(); + var that = this; + Kinetic.Animation.requestAnimFrame(function() { + that._animationLoop(); + }); + } + else { + this.animRunning = false; + } + }; + Kinetic.Animation._handleAnimation = function() { + var that = this; + if(!this.animRunning) { + this.animRunning = true; + that._animationLoop(); + } + }; + Kinetic.Animation.requestAnimFrame = function(callback) { + var raf = Kinetic.DD && Kinetic.DD.moving ? this.fixedRequestAnimFrame : window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || Kinetic.Animation.fixedRequestAnimFrame; + + raf(callback); + }; +})(); + +(function() { + /** + * Node constructor. Nodes are entities that can be transformed, layered, + * and have bound events. The stage, layers, groups, and shapes all extend Node. + * @constructor + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @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] offset from center point and rotation point + * @param {Number} [config.offset.x] + * @param {Number} [config.offset.y] + * @param {Boolean} [config.draggable] + * @param {Function} [config.dragBoundFunc] + */ + 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 + }, + draggable: false + }; + + this.setDefaultAttrs(this.defaultNodeAttrs); + this.eventListeners = {}; + this.setAttrs(config); + + // bind events + 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); + } + }); + }, + /** + * bind events to the node. KineticJS supports mouseover, mousemove, + * mouseout, mouseenter, mouseleave, mousedown, mouseup, click, dblclick, touchstart, touchmove, + * touchend, tap, dbltap, dragstart, dragmove, and dragend events. Pass in a string + * of events 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 e.g. 'click', 'mousedown touchstart', 'mousedown.foo touchstart.foo' + * @param {Function} handler The handler function is passed an event object + */ + 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 + */ + var len = types.length; + for(var n = 0; n < len; 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'. If you only give a name like '.foobar', + * all events in that namespace will be removed. + * @name off + * @methodOf Kinetic.Node.prototype + * @param {String} typesStr e.g. 'click', 'mousedown touchstart', '.foobar' + */ + off: function(typesStr) { + var types = typesStr.split(' '); + var len = types.length; + for(var n = 0; n < len; 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(parts.length > 1) { + if(baseEvent) { + if(this.eventListeners[baseEvent]) { + this._off(baseEvent, parts[1]); + } + } + else { + for(var type in this.eventListeners) { + this._off(type, parts[1]); + } + } + } + else { + delete this.eventListeners[baseEvent]; + } + } + }, + /** + * remove child from container + * @name remove + * @methodOf Kinetic.Container.prototype + */ + remove: function() { + var parent = this.getParent(); + if(parent && this.index !== undefined && parent.children[this.index]._id == this._id) { + var stage = parent.getStage(); + /* + * remove event listeners and references to the node + * from the ids and names hashes + */ + if(stage) { + stage._removeId(this.getId()); + stage._removeName(this.getName(), this._id); + } + + Kinetic.Global._removeTempNode(this); + parent.children.splice(this.index, 1); + parent._setChildrenIndices(); + + // remove children + while(this.children && this.children.length > 0) { + this.children[0].remove(); + } + delete this.parent; + } + }, + /** + * 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 object containing key value pairs + */ + setAttrs: function(config) { + if(config) { + for(var key in config) { + var method = 'set' + key.charAt(0).toUpperCase() + key.slice(1); + // use setter if available + if(Kinetic.Type._isFunction(this[method])) { + this[method](config[key]); + } + // otherwise set directly + else { + this.setAttr(key, config[key]); + } + } + } + }, + /** + * determine if node is visible or not. Node is visible only + * if it's visible and all of its ancestors are visible. If an ancestor + * is invisible, this means that the node is also invisible + * @name getVisible + * @methodOf Kinetic.Node.prototype + */ + getVisible: function() { + var visible = this.attrs.visible, parent = this.getParent(); + if(visible && parent && !parent.getVisible()) { + return false; + } + return visible; + }, + /** + * determine if node is listening or not. Node is listening only + * if it's listening and all of its ancestors are listening. If an ancestor + * is not listening, this means that the node is also not listening + * @name getListening + * @methodOf Kinetic.Node.prototype + */ + getListening: function() { + var listening = this.attrs.listening, parent = this.getParent(); + if(listening && parent && !parent.getListening()) { + return false; + } + return listening; + }, + /** + * show node + * @name show + * @methodOf Kinetic.Node.prototype + */ + show: function() { + this.setVisible(true); + }, + /** + * hide node. Hidden nodes are no longer detectable + * @name hide + * @methodOf Kinetic.Node.prototype + */ + hide: function() { + this.setVisible(false); + }, + /** + * get zIndex relative to the node's siblings who share the same parent + * @name getZIndex + * @methodOf Kinetic.Node.prototype + */ + getZIndex: function() { + return this.index; + }, + /** + * get absolute z-index which takes into account sibling + * and ancestor 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 = []; + var len = children.length; + for(var n = 0; n < len; n++) { + var child = children[n]; + index++; + + if(child.nodeType !== 'Shape') { + nodes = nodes.concat(child.getChildren()); + } + + if(child._id === that._id) { + n = len; + } + } + + 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. Returns an integer.

    + * e.g. Stage level will always be 0. Layers will always be 1. Groups and Shapes will always + * be >= 2 + * @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 relative to parent + * @name setPosition + * @methodOf Kinetic.Node.prototype + * @param {Number} x + * @param {Number} y + */ + setPosition: function() { + var pos = Kinetic.Type._getXY([].slice.call(arguments)); + this.setAttr('x', pos.x); + this.setAttr('y', pos.y); + }, + /** + * get node position relative to parent + * @name getPosition + * @methodOf Kinetic.Node.prototype + */ + getPosition: function() { + var attrs = this.attrs; + return { + x: attrs.x, + y: attrs.y + }; + }, + /** + * get absolute position relative to the top left corner of the stage container div + * @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 {Number} x + * @param {Number} y + */ + setAbsolutePosition: function() { + var pos = Kinetic.Type._getXY([].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 relative to its current position + * @name move + * @methodOf Kinetic.Node.prototype + * @param {Number} x + * @param {Number} y + */ + move: function() { + var pos = Kinetic.Type._getXY([].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.setPosition(x, y); + }, + /** + * get rotation in degrees + * @name getRotationDeg + * @methodOf Kinetic.Node.prototype + */ + getRotationDeg: function() { + return Kinetic.Type._radToDeg(this.getRotation()); + }, + /** + * set rotation in degrees + * @name setRotationDeg + * @methodOf Kinetic.Node.prototype + * @param {Number} deg + */ + setRotationDeg: function(deg) { + this.setRotation(Kinetic.Type._degToRad(deg)); + }, + /** + * rotate node by an amount in radians relative to its current rotation + * @name rotate + * @methodOf Kinetic.Node.prototype + * @param {Number} theta + */ + rotate: function(theta) { + this.setRotation(this.getRotation() + theta); + }, + /** + * rotate node by an amount in degrees relative to its current rotation + * @name rotateDeg + * @methodOf Kinetic.Node.prototype + * @param {Number} deg + */ + rotateDeg: function(deg) { + this.setRotation(this.getRotation() + Kinetic.Type._degToRad(deg)); + }, + /** + * 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(); + return true; + }, + /** + * move node up + * @name moveUp + * @methodOf Kinetic.Node.prototype + */ + moveUp: function() { + var index = this.index; + var len = this.parent.getChildren().length; + if(index < len - 1) { + this.parent.children.splice(index, 1); + this.parent.children.splice(index + 1, 0, this); + this.parent._setChildrenIndices(); + return true; + } + }, + /** + * 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(); + return true; + } + }, + /** + * move node to the bottom of its siblings + * @name moveToBottom + * @methodOf Kinetic.Node.prototype + */ + moveToBottom: function() { + var index = this.index; + if(index > 0) { + this.parent.children.splice(index, 1); + this.parent.children.unshift(this); + this.parent._setChildrenIndices(); + return true; + } + }, + /** + * set zIndex relative to siblings + * @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 = this.getOpacity(); + if(this.getParent()) { + absOpacity *= this.getParent().getAbsoluteOpacity(); + } + return absOpacity; + }, + /** + * 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(); + }, + /** + * convert Node into an object for serialization. Returns an object. + * @name toObject + * @methodOf Kinetic.Node.prototype + */ + toObject: function() { + var type = Kinetic.Type, obj = {}, attrs = this.attrs; + + obj.attrs = {}; + + // serialize only attributes that are not function, image, DOM, or objects with methods + for(var key in attrs) { + var val = attrs[key]; + if(!type._isFunction(val) && !type._isElement(val) && !(type._isObject(val) && type._hasMethods(val))) { + obj.attrs[key] = val; + } + } + + obj.nodeType = this.nodeType; + obj.shapeType = this.shapeType; + + return obj; + }, + /** + * convert Node into a JSON string. Returns a JSON string. + * @name toJSON + * @methodOf Kinetic.Node.prototype + */ + toJSON: function() { + return JSON.stringify(this.toObject()); + }, + /** + * get parent container + * @name getParent + * @methodOf Kinetic.Node.prototype + */ + getParent: function() { + return this.parent; + }, + /** + * get layer ancestor + * @name getLayer + * @methodOf Kinetic.Node.prototype + */ + getLayer: function() { + return this.getParent().getLayer(); + }, + /** + * get stage ancestor + * @name getStage + * @methodOf Kinetic.Node.prototype + */ + getStage: function() { + if(this.getParent()) { + return this.getParent().getStage(); + } + else { + return undefined; + } + }, + /** + * simulate event with event bubbling + * @name simulate + * @methodOf Kinetic.Node.prototype + * @param {String} eventType + * @param {EventObject} evt event object + */ + simulate: function(eventType, evt) { + this._handleEvent(eventType, evt || {}); + }, + /** + * synthetically fire an event. The event object will not bubble up the Node tree. You can also pass in custom properties + * @name fire + * @methodOf Kinetic.Node.prototype + * @param {String} eventType + * @param {Object} obj optional object which can be used to pass parameters + */ + fire: function(eventType, obj) { + this._executeHandlers(eventType, obj || {}); + }, + /** + * get absolute transform of the node which takes into + * account its ancestor 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; + } + + var len = family.length; + for(var n = 0; n < len; 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(), attrs = this.attrs, x = attrs.x, y = attrs.y, rotation = attrs.rotation, scale = attrs.scale, scaleX = scale.x, scaleY = scale.y, offset = attrs.offset, offsetX = offset.x, offsetY = offset.y; + + if(x !== 0 || y !== 0) { + m.translate(x, y); + } + if(rotation !== 0) { + m.rotate(rotation); + } + if(scaleX !== 1 || scaleY !== 1) { + m.scale(scaleX, scaleY); + } + if(offsetX !== 0 || offsetY !== 0) { + m.translate(-1 * offsetX, -1 * offsetY); + } + + return m; + }, + /** + * clone node. Returns a new Node instance with identical attributes + * @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]; + var len = allListeners.length; + for(var n = 0; n < len; 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 width and height are defined, create new canvas to draw on, else reuse stage hit canvas + if(config && config.width && config.height) { + canvas = new Kinetic.Canvas(config.width, config.height); + } + else { + canvas = this.getStage().bufferCanvas; + canvas.clear(); + } + + this.drawBuffer(canvas); + return canvas.toDataURL(mimeType, quality); + }, + /** + * converts node into an image. Since the toImage + * method is asynchronous, a callback is required. toImage is most commonly used + * to cache complex drawings as an image so that they don't have to constantly be redrawn + * @name toImage + * @methodOf Kinetic.Node.prototype + * @param {Object} config + * @param {Function} config.callback function that is passed the image object + * @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); + }); + }, + /** + * set offset. A node's offset defines the position and rotation point + * @name setOffset + * @methodOf Kinetic.Node.prototype + * @param {Number} x + * @param {Number} y + */ + setOffset: function() { + var pos = Kinetic.Type._getXY([].slice.call(arguments)); + if(pos.x === undefined) { + pos.x = this.getOffset().x; + } + if(pos.y === undefined) { + pos.y = this.getOffset().y; + } + this.setAttr('offset', pos); + }, + /** + * set scale. + * @name setScale + * @param {Number} x + * @param {Number} y + * @methodOf Kinetic.Node.prototype + */ + setScale: function() { + var pos = Kinetic.Type._getXY([].slice.call(arguments)); + + if(pos.x === undefined) { + pos.x = this.getScale().x; + } + if(pos.y === undefined) { + pos.y = this.getScale().y; + } + this.setAttr('scale', pos); + + }, + /** + * set size + * @name setSize + * @methodOf Kinetic.Node.prototype + * @param {Number} width + * @param {Number} height + */ + setSize: function() { + // set stage dimensions + var size = Kinetic.Type._getSize(Array.prototype.slice.call(arguments)); + this.setWidth(size.width); + this.setHeight(size.height); + }, + /** + * get size + * @name getSize + * @methodOf Kinetic.Node.prototype + */ + getSize: function() { + return { + width: this.getWidth(), + height: this.getHeight() + }; + }, + /** + * get width + * @name getWidth + * @methodOf Kinetic.Node.prototype + */ + getWidth: function() { + return this.attrs.width || 0; + }, + /** + * get height + * @name getHeight + * @methodOf Kinetic.Node.prototype + */ + getHeight: function() { + return this.attrs.height || 0; + }, + _get: function(selector) { + return this.nodeType === selector ? [this] : []; + }, + _off: function(type, name) { + for(var i = 0; i < this.eventListeners[type].length; i++) { + if(this.eventListeners[type][i].name === name) { + this.eventListeners[type].splice(i, 1); + if(this.eventListeners[type].length === 0) { + delete this.eventListeners[type]; + break; + } + i--; + } + } + }, + _clearTransform: function() { + var attrs = this.attrs, scale = attrs.scale, offset = attrs.offset; + var trans = { + x: attrs.x, + y: attrs.y, + rotation: attrs.rotation, + scale: { + x: scale.x, + y: scale.y + }, + offset: { + x: offset.x, + y: 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(key, val) { + if(val !== undefined) { + var oldVal = this.attrs[key]; + this._fireBeforeChangeEvent(key, oldVal, val); + this.attrs[key] = val; + this._fireChangeEvent(key, oldVal, val); + } + }, + _handleEvent: function(eventType, evt, compareShape) { + if(evt && this.nodeType === 'Shape') { + evt.shape = this; + } + var stage = this.getStage(); + var el = this.eventListeners; + var okayToRun = true; + + if(eventType === 'mouseenter' && compareShape && this._id === compareShape._id) { + okayToRun = false; + } + else if(eventType === 'mouseleave' && compareShape && this._id === compareShape._id) { + okayToRun = false; + } + + if(okayToRun) { + if(el[eventType]) { + this.fire(eventType, evt); + } + + // simulate event bubbling + if(evt && !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); + } + } + } + }, + _executeHandlers: function(eventType, evt) { + var events = this.eventListeners[eventType]; + var len = events.length; + for(var i = 0; i < len; i++) { + events[i].handler.apply(this, [evt]); + } + } + }; + + // add getter and setter methods + Kinetic.Node.addSetters = function(constructor, arr) { + var len = arr.length; + for(var n = 0; n < len; n++) { + var attr = arr[n]; + this._addSetter(constructor, attr); + } + }; + Kinetic.Node.addGetters = function(constructor, arr) { + var len = arr.length; + for(var n = 0; n < len; 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(val) { + this.setAttr(attr, val); + }; + }; + 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]; + }; + }; + /** + * create node 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() methods + * @name create + * @methodOf Kinetic.Node + * @param {String} JSON string + * @param {DomElement} [container] optional container dom element used only if you're + * creating a stage node + */ + Kinetic.Node.create = function(json, container) { + return this._createNode(JSON.parse(json), container); + }; + Kinetic.Node._createNode = function(obj, container) { + var type; + + // determine type + if(obj.nodeType === 'Shape') { + // add custom shape + if(obj.shapeType === undefined) { + type = 'Shape'; + } + // add standard shape + else { + type = obj.shapeType; + } + } + else { + type = obj.nodeType; + } + + // if container was passed in, add it to attrs + if(container) { + obj.attrs.container = container; + } + + var no = new Kinetic[type](obj.attrs); + if(obj.children) { + var len = obj.children.length; + for(var n = 0; n < len; n++) { + no.add(this._createNode(obj.children[n])); + } + } + + return no; + }; + // add getters setters + Kinetic.Node.addGettersSetters(Kinetic.Node, ['x', 'y', 'rotation', 'opacity', 'name', 'id']); + Kinetic.Node.addGetters(Kinetic.Node, ['scale', 'offset']); + Kinetic.Node.addSetters(Kinetic.Node, ['width', 'height', 'listening', 'visible']); + + // aliases + /** + * Alias of getListening() + * @name isListening + * @methodOf Kinetic.Node.prototype + */ + Kinetic.Node.prototype.isListening = Kinetic.Node.prototype.getListening; + /** + * Alias of getVisible() + * @name isVisible + * @methodOf Kinetic.Node.prototype + */ + Kinetic.Node.prototype.isVisible = Kinetic.Node.prototype.getVisible; + + // collection mappings + var collectionMappings = ['on', 'off']; + for(var n = 0; n < 2; n++) { + // induce scope + (function(i) { + var method = collectionMappings[i]; + Kinetic.Collection.prototype[method] = function() { + var args = [].slice.call(arguments); + args.unshift(method); + this.apply.apply(this, args); + }; + })(n); + } + + /** + * set x position + * @name setX + * @methodOf Kinetic.Node.prototype + * @param {Number} x + */ + + /** + * set y position + * @name setY + * @methodOf Kinetic.Node.prototype + * @param {Number} y + */ + + /** + * set 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 name + * @name setName + * @methodOf Kinetic.Node.prototype + * @param {String} name + */ + + /** + * set id + * @name setId + * @methodOf Kinetic.Node.prototype + * @param {String} id + */ + + /** + * set width + * @name setWidth + * @methodOf Kinetic.Node.prototype + * @param {Number} width + */ + + /** + * set height + * @name setHeight + * @methodOf Kinetic.Node.prototype + * @param {Number} height + */ + + /** + * listen or don't listen to events + * @name setListening + * @methodOf Kinetic.Node.prototype + * @param {Boolean} listening + */ + + /** + * set visible + * @name setVisible + * @methodOf Kinetic.Node.prototype + * @param {Boolean} visible + */ + + /** + * get x position + * @name getX + * @methodOf Kinetic.Node.prototype + */ + + /** + * get 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 scale + * @name getScale + * @methodOf Kinetic.Node.prototype + */ + + /** + * get offset + * @name getOffset + * @methodOf Kinetic.Node.prototype + */ +})(); + +(function() { + Kinetic.DD = { + anim: new Kinetic.Animation(), + moving: false, + offset: { + x: 0, + y: 0 + } + }; + + Kinetic.DD._startDrag = function(evt) { + var dd = Kinetic.DD; + var node = dd.node; + + if(node) { + var pos = node.getStage().getUserPosition(); + var dbf = node.attrs.dragBoundFunc; + + var newNodePos = { + x: pos.x - dd.offset.x, + y: pos.y - dd.offset.y + }; + + if(dbf !== undefined) { + newNodePos = dbf.call(node, newNodePos, evt); + } + + node.setAbsolutePosition(newNodePos); + + if(!dd.moving) { + dd.moving = true; + node.setListening(false); + + // execute dragstart events if defined + node._handleEvent('dragstart', evt); + } + + // execute user defined ondragmove if defined + node._handleEvent('dragmove', evt); + } + }; + Kinetic.DD._endDrag = function(evt) { + var dd = Kinetic.DD; + var node = dd.node; + if(node) { + node.setListening(true); + if(node.nodeType === 'Stage') { + node.draw(); + } + else { + node.getLayer().draw(); + } + + // handle dragend + if(dd.moving) { + dd.moving = false; + node._handleEvent('dragend', evt); + } + } + dd.node = null; + dd.anim.stop(); + }; + /** + * set draggable + * @name setDraggable + * @methodOf Kinetic.Node.prototype + * @param {String} draggable + */ + Kinetic.Node.prototype.setDraggable = function(draggable) { + this.setAttr('draggable', draggable); + this._dragChange(); + }; + /** + * get draggable + * @name getDraggable + * @methodOf Kinetic.Node.prototype + */ + Kinetic.Node.prototype.getDraggable = function() { + return this.attrs.draggable; + }; + /** + * determine if node is currently in drag and drop mode + * @name isDragging + * @methodOf Kinetic.Node.prototype + */ + Kinetic.Node.prototype.isDragging = function() { + var dd = Kinetic.DD; + return dd.node && dd.node._id === this._id && dd.moving; + }; + + Kinetic.Node.prototype._listenDrag = function() { + this._dragCleanup(); + var that = this; + this.on('mousedown.kinetic touchstart.kinetic', function(evt) { + that._initDrag(); + }); + }; + Kinetic.Node.prototype._initDrag = function() { + var dd = Kinetic.DD; + var stage = this.getStage(); + var pos = stage.getUserPosition(); + + if(pos) { + var m = this.getTransform().getTranslation(); + var am = this.getAbsoluteTransform().getTranslation(); + var ap = this.getAbsolutePosition(); + dd.node = this; + dd.offset.x = pos.x - ap.x; + dd.offset.y = pos.y - ap.y; + + /* + * if dragging and dropping the stage, + * draw all of the layers + */ + if(this.nodeType === 'Stage') { + dd.anim.node = this; + } + else { + dd.anim.node = this.getLayer(); + } + dd.anim.start(); + } + }; + Kinetic.Node.prototype._dragChange = 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 dd = Kinetic.DD; + if(stage && dd.node && dd.node._id === this._id) { + dd._endDrag(); + } + } + }; + Kinetic.Node.prototype._dragCleanup = function() { + this.off('mousedown.kinetic'); + this.off('touchstart.kinetic'); + }; + /** + * get draggable. Alias of getDraggable() + * @name isDraggable + * @methodOf Kinetic.Node.prototype + */ + Kinetic.Node.prototype.isDraggable = Kinetic.Node.prototype.getDraggable; + + Kinetic.Node.addGettersSetters(Kinetic.Node, ['dragBoundFunc']); + + /** + * set drag bound function. This is used to override the default + * drag and drop position + * @name setDragBoundFunc + * @methodOf Kinetic.Node.prototype + * @param {Function} dragBoundFunc + */ + + /** + * get dragBoundFunc + * @name getDragBoundFunc + * @methodOf Kinetic.Node.prototype + */ +})(); + +(function() { + /** + * 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; + } + }; + + /** + * 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 + */ + Kinetic.Node.prototype.transitionTo = function(config) { + if(!this.transAnim) { + this.transAnim = new Kinetic.Animation(); + } + /* + * 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(); + + // callback + if(config.callback) { + config.callback(); + } + }; + // auto start + trans.start(); + this.transAnim.start(); + return trans; + }; +})(); + +(function() { + /** + * Container constructor.  Containers are used to contain nodes or other containers + * @constructor + * @augments Kinetic.Node + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.alpha] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] + * @param {Number} [config.scale.x] + * @param {Number} [config.scale.y] + * @param {Number} [config.rotation] rotation in radians + * @param {Number} [config.rotationDeg] rotation in degrees + * @param {Object} [config.offset] offsets default position point and rotation point + * @param {Number} [config.offset.x] + * @param {Number} [config.offset.y] + * @param {Boolean} [config.draggable] + * @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default + * is none + * @param {Object} [config.dragBounds] + * @param {Number} [config.dragBounds.top] + * @param {Number} [config.dragBounds.right] + * @param {Number} [config.dragBounds.bottom] + * @param {Number} [config.dragBounds.left] + */ + Kinetic.Container = function(config) { + this._containerInit(config); + }; + + Kinetic.Container.prototype = { + _containerInit: function(config) { + this.children = []; + Kinetic.Node.call(this, config); + }, + /** + * get children + * @name getChildren + * @methodOf Kinetic.Container.prototype + */ + getChildren: function() { + return this.children; + }, + /** + * remove all children + * @name removeChildren + * @methodOf Kinetic.Container.prototype + */ + removeChildren: function() { + while(this.children.length > 0) { + this.children[0].remove(); + } + }, + /** + * add node to container + * @name add + * @methodOf Kinetic.Container.prototype + * @param {Node} child + */ + add: function(child) { + var go = Kinetic.Global, children = this.children; + + child._id = Kinetic.Global.idCounter++; + child.index = children.length; + child.parent = this; + children.push(child); + var stage = child.getStage(); + + if(!stage) { + go._addTempNode(child); + } + else { + stage._addId(child); + stage._addName(child); + + /* + * pull in other nodes that are now linked + * to a stage + */ + go._pullNodes(stage); + } + + // 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 collection = new Kinetic.Collection(); + // ID selector + if(selector.charAt(0) === '#') { + var node = this._getNodeById(selector.slice(1)); + if(node) { + collection.push(node); + } + } + // name selector + else if(selector.charAt(0) === '.') { + var nodeList = this._getNodesByName(selector.slice(1)); + Kinetic.Collection.apply(collection, nodeList); + } + // unrecognized selector, pass to children + else { + var retArr = []; + var children = this.getChildren(); + var len = children.length; + for(var n = 0; n < len; n++) { + retArr = retArr.concat(children[n]._get(selector)); + } + Kinetic.Collection.apply(collection, retArr); + } + return collection; + }, + _getNodeById: function(key) { + var stage = this.getStage(); + if(stage.ids[key] !== undefined && this.isAncestorOf(stage.ids[key])) { + return stage.ids[key]; + } + return null; + }, + _getNodesByName: function(key) { + var arr = this.getStage().names[key] || []; + return this._getDescendants(arr); + }, + _get: function(selector) { + var retArr = Kinetic.Node.prototype._get.call(this, selector); + var children = this.getChildren(); + var len = children.length; + for(var n = 0; n < len; n++) { + retArr = retArr.concat(children[n]._get(selector)); + } + return retArr; + }, + // extenders + toObject: function() { + var obj = Kinetic.Node.prototype.toObject.call(this); + + obj.children = []; + + var children = this.getChildren(); + var len = children.length; + for(var n = 0; n < len; n++) { + var child = children[n]; + obj.children.push(child.toObject()); + } + + return obj; + }, + _getDescendants: function(arr) { + var retArr = []; + var len = arr.length; + for(var n = 0; n < len; 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) { + var parent = node.getParent(); + while(parent) { + if(parent._id === this._id) { + return true; + } + parent = parent.getParent(); + } + + return false; + }, + /** + * clone node + * @name clone + * @methodOf Kinetic.Container.prototype + * @param {Object} attrs override attrs + */ + clone: function(obj) { + // call super method + var node = Kinetic.Node.prototype.clone.call(this, obj) + + // perform deep clone on containers + for(var key in this.children) { + node.add(this.children[key].clone()); + } + return node; + }, + /** + * get shapes that intersect a point + * @name getIntersections + * @methodOf Kinetic.Container.prototype + * @param {Object} point + */ + getIntersections: function() { + var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments)); + var arr = []; + var shapes = this.get('Shape'); + + var len = shapes.length; + for(var n = 0; n < len; n++) { + var shape = shapes[n]; + if(shape.isVisible() && shape.intersects(pos)) { + arr.push(shape); + } + } + + return arr; + }, + /** + * set children indices + */ + _setChildrenIndices: function() { + var children = this.children, len = children.length; + for(var n = 0; n < len; n++) { + children[n].index = n; + } + }, + /* + * draw both scene and hit graphs + */ + draw: function() { + this.drawScene(); + this.drawHit(); + }, + drawScene: function() { + if(this.isVisible()) { + var children = this.children, len = children.length; + for(var n = 0; n < len; n++) { + children[n].drawScene(); + } + } + }, + drawHit: function() { + if(this.isVisible() && this.isListening()) { + var children = this.children, len = children.length; + for(var n = 0; n < len; n++) { + children[n].drawHit(); + } + } + }, + drawBuffer: function(canvas) { + if(this.isVisible()) { + var children = this.children, len = children.length; + for(var n = 0; n < len; n++) { + children[n].drawBuffer(canvas); + } + } + } + }; + Kinetic.Global.extend(Kinetic.Container, Kinetic.Node); +})(); + +(function() { + /** + * 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 {Function} [config.dragBoundFunc] dragBoundFunc(pos, evt) should return new position + */ + Kinetic.Stage = function(config) { + this._initStage(config); + }; + + Kinetic.Stage.prototype = { + _initStage: function(config) { + this.setDefaultAttrs({ + width: 400, + height: 200 + }); + + // call super constructor + Kinetic.Container.call(this, config); + + this._setStageDefaultProperties(); + this._id = Kinetic.Global.idCounter++; + this._buildDOM(); + this._bindContentEvents(); + + var go = Kinetic.Global; + go.stages.push(this); + this._addId(this); + this._addName(this); + + }, + setContainer: function(container) { + /* + * if container is a string, assume it's an id for + * a DOM element + */ + if( typeof container === 'string') { + container = document.getElementById(container); + } + this.setAttr('container', container); + }, + /** + * draw layer scenes + * @name draw + * @methodOf Kinetic.Stage.prototype + */ + + /** + * draw layer hits + * @name drawHit + * @methodOf Kinetic.Stage.prototype + */ + + /** + * set height + * @name setHeight + * @methodOf Kinetic.Stage.prototype + * @param {Number} height + */ + setHeight: function(height) { + Kinetic.Node.prototype.setHeight.call(this, height); + this._resizeDOM(); + }, + /** + * set width + * @name setWidth + * @methodOf Kinetic.Stage.prototype + * @param {Number} width + */ + setWidth: function(width) { + Kinetic.Node.prototype.setWidth.call(this, width); + this._resizeDOM(); + }, + /** + * 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(); + } + }, + /** + * reset stage to default state + * @name reset + * @methodOf Kinetic.Stage.prototype + */ + reset: function() { + // remove children + this.removeChildren(); + + // defaults + this._setStageDefaultProperties(); + this.setAttrs(this.defaultNodeAttrs); + }, + /** + * 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 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 hit canvas + * because this method is asynchonous which means that other parts of the + * code could modify the hit 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.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]; + if(layer.isVisible() && layer.isListening()) { + var p = layer.hitCanvas.context.getImageData(Math.round(pos.x), Math.round(pos.y), 1, 1).data; + // this indicates that a hit 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; + }, + _getNodeById: function(key) { + return this.ids[key] || null; + }, + _getNodesByName: function(key) { + return this.names[key] || []; + }, + _resizeDOM: function() { + if(this.content) { + 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); + this.hitCanvas.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.hitCanvas.setSize(width, height); + layer.draw(); + } + } + }, + /** + * add layer to stage + * @param {Layer} layer + */ + add: function(layer) { + Kinetic.Container.prototype.add.call(this, layer); + layer.canvas.setSize(this.attrs.width, this.attrs.height); + layer.hitCanvas.setSize(this.attrs.width, this.attrs.height); + + // draw layer and append canvas to container + layer.draw(); + this.content.appendChild(layer.canvas.element); + + // chainable + return this; + }, + _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 dd = Kinetic.DD; + // if there's a current target shape, run mouseout handlers + var targetShape = this.targetShape; + if(targetShape && (!dd || !dd.moving)) { + targetShape._handleEvent('mouseout', evt); + targetShape._handleEvent('mouseleave', evt); + this.targetShape = null; + } + this.mousePos = undefined; + + // end drag and drop + if(dd) { + dd._endDrag(evt); + } + }, + _mousemove: function(evt) { + this._setUserPosition(evt); + var dd = Kinetic.DD; + var obj = this.getIntersection(this.getUserPosition()); + + if(obj) { + var shape = obj.shape; + if(shape) { + if((!dd || !dd.moving) && obj.pixel[3] === 255 && (!this.targetShape || this.targetShape._id !== shape._id)) { + if(this.targetShape) { + this.targetShape._handleEvent('mouseout', evt, shape); + this.targetShape._handleEvent('mouseleave', evt, shape); + } + shape._handleEvent('mouseover', evt, this.targetShape); + shape._handleEvent('mouseenter', 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 && (!dd || !dd.moving)) { + this.targetShape._handleEvent('mouseout', evt); + this.targetShape._handleEvent('mouseleave', evt); + this.targetShape = null; + } + + // start drag and drop + if(dd) { + dd._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(Kinetic.DD && this.attrs.draggable) { + this._initDrag(); + } + }, + _mouseup: function(evt) { + this._setUserPosition(evt); + var dd = Kinetic.DD; + 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(!dd || !dd.moving || !dd.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 + if(dd) { + dd._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(Kinetic.DD && this.attrs.draggable) { + this._initDrag(); + } + }, + _touchend: function(evt) { + this._setUserPosition(evt); + var dd = Kinetic.DD; + 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(!dd || !dd.moving || !dd.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 + if(dd) { + dd._endDrag(evt); + } + }, + _touchmove: function(evt) { + this._setUserPosition(evt); + var dd = Kinetic.DD; + evt.preventDefault(); + var obj = this.getIntersection(this.getUserPosition()); + if(obj && obj.shape) { + var shape = obj.shape; + shape._handleEvent('touchmove', evt); + } + + // start drag and drop + if(dd) { + dd._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 + }; + }, + /** + * 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(); + this.hitCanvas = new Kinetic.Canvas(0, 0, true); + + 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 = {}; + } + }; + Kinetic.Global.extend(Kinetic.Stage, Kinetic.Container); + + // add getters and setters + Kinetic.Node.addGetters(Kinetic.Stage, ['container']); + + /** + * get container DOM element + * @name getContainer + * @methodOf Kinetic.Stage.prototype + */ +})(); + +(function() { + /** + * 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 {Function} [config.dragBoundFunc] dragBoundFunc(pos, evt) should return new position + */ + 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.hitCanvas = new Kinetic.Canvas(0, 0, true); + + // 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() { + // before draw handler + if(this.beforeDrawFunc !== undefined) { + this.beforeDrawFunc.call(this); + } + + Kinetic.Container.prototype.draw.call(this); + + // after draw handler + if(this.afterDrawFunc !== undefined) { + this.afterDrawFunc.call(this); + } + }, + /** + * draw children nodes on hit. this includes any groups + * or shapes + * @name drawHit + * @methodOf Kinetic.Layer.prototype + */ + drawHit: function() { + this.hitCanvas.clear(); + Kinetic.Container.prototype.drawHit.call(this); + }, + /** + * draw children nodes on scene. this includes any groups + * or shapes + * @name drawScene + * @methodOf Kinetic.Layer.prototype + * @param {Kinetic.Canvas} [canvas] + */ + drawScene: function() { + if(this.attrs.clearBeforeDraw) { + this.getCanvas().clear(); + } + Kinetic.Container.prototype.drawScene.call(this); + }, + /** + * 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(); + }, + // extenders + setVisible: function(visible) { + Kinetic.Node.prototype.setVisible.call(this, visible); + if(visible) { + this.canvas.element.style.display = 'block'; + this.hitCanvas.element.style.display = 'block'; + } + else { + this.canvas.element.style.display = 'none'; + this.hitCanvas.element.style.display = 'none'; + } + }, + setZIndex: function(index) { + Kinetic.Node.prototype.setZIndex.call(this, index); + var stage = this.getStage(); + if(stage) { + stage.content.removeChild(this.canvas.element); + + if(index < stage.getChildren().length - 1) { + stage.content.insertBefore(this.canvas.element, stage.getChildren()[index + 1].canvas.element); + } + else { + stage.content.appendChild(this.canvas.element); + } + } + }, + moveToTop: function() { + Kinetic.Node.prototype.moveToTop.call(this); + var stage = this.getStage(); + if(stage) { + stage.content.removeChild(this.canvas.element); + stage.content.appendChild(this.canvas.element); + } + }, + moveUp: function() { + if(Kinetic.Node.prototype.moveUp.call(this)) { + 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); + } + } + } + }, + moveDown: function() { + if(Kinetic.Node.prototype.moveDown.call(this)) { + var stage = this.getStage(); + if(stage) { + var children = stage.getChildren(); + stage.content.removeChild(this.canvas.element); + stage.content.insertBefore(this.canvas.element, children[this.index + 1].canvas.element); + } + } + }, + moveToBottom: function() { + if(Kinetic.Node.prototype.moveToBottom.call(this)) { + var stage = this.getStage(); + if(stage) { + var children = stage.getChildren(); + stage.content.removeChild(this.canvas.element); + stage.content.insertBefore(this.canvas.element, children[1].canvas.element); + } + } + }, + getLayer: function() { + return this; + }, + /** + * 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 currently drawn 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 layer is hidden, return blank canvas + * else if width and height are defined, create blank canvas and draw onto it + * else return canvas as is + */ + if(!this.isVisible()) { + var stage = this.getStage(); + canvas = new Kinetic.Canvas(stage.getWidth(), stage.getHeight()); + } + else if(config && config.width && config.height) { + canvas = new Kinetic.Canvas(config.width, config.height); + this.draw(canvas); + } + else { + canvas = this.getCanvas(); + } + return canvas.toDataURL(mimeType, quality); + }, + /** + * remove layer from stage + */ + remove: function() { + var stage = this.getStage(); + Kinetic.Node.prototype.remove.call(this); + /* + * remove canvas DOM from the document if + * it exists + */ + try { + stage.content.removeChild(this.canvas.element); + } + catch(e) { + Kinetic.Global.warn('unable to remove layer scene canvas element from the document'); + } + } + }; + 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 + */ +})(); + +(function() { + /** + * 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 {Function} [config.dragBoundFunc] dragBoundFunc(pos, evt) should return new position + */ + 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); +})(); + +(function() { + /** + * 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'; + + // 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(); + }, + _getFillType: function(fill) { + var type = Kinetic.Type; + if(!fill) { + return undefined; + } + else if(type._isString(fill)) { + return 'COLOR'; + } + else if(fill.image) { + return 'PATTERN'; + } + else if(fill.start && fill.end && !fill.start.radius && !fill.end.radius) { + return 'LINEAR_GRADIENT'; + } + else if(fill.start && fill.end && type._isNumber(fill.start.radius) && type._isNumber(fill.end.radius)) { + return 'RADIAL_GRADIENT'; + } + else { + return 'UNKNOWN'; + } + }, + /** + * fill current path + * @name fill + * @methodOf Kinetic.Shape.prototype + */ + fill: function(context) { + context.renderer._fill(this); + }, + /** + * stroke current path + * @name stroke + * @methodOf Kinetic.Shape.prototype + */ + stroke: function(context) { + context.renderer._stroke(this); + }, + /** + * fill and stroke current path.  Aside from being a convenience method + * which fills and strokes the current path with a single method, its main purpose is + * to ensure that the shadow object is not applied to both the fill and stroke.  A shadow + * will only be applied to either the fill or stroke.  Fill + * is given priority over stroke. + * @name fillStroke + * @param {CanvasContext} context + * @methodOf Kinetic.Shape.prototype + */ + fillStroke: function(context) { + context.renderer._fill(this); + context.renderer._stroke(this, this.getShadow() && this.getFill()); + }, + /** + * apply shadow + * @name applyShadow + * @param {CanvasContext} context + * @param {Function} func draw function + * @methodOf Kinetic.Shape.prototype + */ + applyShadow: function(context, func) { + context.save(); + context.renderer._applyShadow(this); + func(); + context.restore(); + func(); + }, + /** + * draw an image + * @name drawImage + * @methodOf Kinetic.Shape.prototype + * @param {CanvasContext} context + */ + drawImage: function() { + var context = arguments[0]; + context.save(); + var a = Array.prototype.slice.call(arguments); + + if(a.length === 6) { + context.drawImage(a[1], a[2], a[3], a[4], a[5]); + } + else if(a.length === 10) { + context.drawImage(a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9]); + } + + context.restore(); + }, + _applyOpacity: function(context) { + var absOpacity = this.getAbsoluteOpacity(); + if(absOpacity !== 1) { + context.globalAlpha = absOpacity; + } + }, + _applyLineJoin: function(context) { + var lineJoin = this.getLineJoin(); + if(lineJoin) { + context.lineJoin = lineJoin; + } + }, + _applyLineCap: function(context) { + var lineCap = this.getLineCap(); + if(lineCap) { + context.lineCap = lineCap; + } + }, + /** + * set shadow object + * @name setShadow + * @methodOf Kinetic.Shape.prototype + * @param {Object} config + * @param {String} config.color + * @param {Number} config.blur + * @param {Array|Object|Number} config.offset + * @param {Number} config.opacity + */ + setShadow: function(config) { + var type = Kinetic.Type; + if(config.offset !== undefined) { + config.offset = type._getXY(config.offset); + } + this.setAttr('shadow', type._merge(config, this.getShadow())); + }, + /** + * 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 + */ + setFill: function(fill) { + var type = Kinetic.Type; + var oldFill = this.getFill(); + var fillType = this._getFillType(fill); + var oldFillType = this._getFillType(oldFill); + var newOrOldFillIsColor = fillType === 'COLOR' || oldFillType === 'COLOR'; + var changedFillType = fillType === oldFillType || fillType === 'UNKNOWN'; + + // if fill.offset is defined, normalize the xy value + if(fill.offset !== undefined) { + fill.offset = type._getXY(fill.offset); + } + + /* + * merge fill objects if neither the new or old fill + * is type is COLOR, and if if the fill type has not changed. Otherwise, + * overwrite the fill entirely + */ + if(!newOrOldFillIsColor && changedFillType) { + fill = type._merge(fill, oldFill); + } + + this.setAttr('fill', fill); + }, + /** + * set width and height + * @name setSize + * @methodOf Kinetic.Shape.prototype + */ + setSize: function() { + var size = Kinetic.Type._getSize(Array.prototype.slice.call(arguments)); + this.setWidth(size.width); + this.setHeight(size.height); + }, + /** + * return shape size + * @name getSize + * @methodOf Kinetic.Shape.prototype + */ + getSize: function() { + return { + width: this.getWidth(), + height: this.getHeight() + }; + }, + _get: function(selector) { + return this.nodeType === selector || this.shapeType === selector ? [this] : []; + }, + /** + * 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 hitCanvas = stage.hitCanvas; + hitCanvas.clear(); + this.drawBuffer(hitCanvas); + var p = hitCanvas.context.getImageData(Math.round(pos.x), Math.round(pos.y), 1, 1).data; + return p[3] > 0; + }, + remove: function() { + Kinetic.Node.prototype.remove.call(this); + delete Kinetic.Global.shapes[this.colorKey]; + }, + drawBuffer: function(canvas) { + var attrs = this.attrs, drawFunc = attrs.drawFunc, context = canvas.getContext(); + + if(drawFunc && this.isVisible()) { + var stage = this.getStage(), family = [], parent = this.parent; + + family.unshift(this); + while(parent) { + family.unshift(parent); + parent = parent.parent; + } + + context.save(); + this._applyOpacity(context); + this._applyLineJoin(context); + this._applyLineCap(context); + var len = family.length; + for(var n = 0; n < len; n++) { + var node = family[n], t = node.getTransform(), m = t.getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + drawFunc.call(this, context); + context.restore(); + } + }, + drawScene: function() { + var attrs = this.attrs, drawFunc = attrs.drawFunc, context = this.getLayer().getCanvas().getContext(); + + if(drawFunc && this.isVisible()) { + var stage = this.getStage(), family = [], parent = this.parent; + + family.unshift(this); + while(parent) { + family.unshift(parent); + parent = parent.parent; + } + + context.save(); + this._applyOpacity(context); + this._applyLineJoin(context); + this._applyLineCap(context); + var len = family.length; + for(var n = 0; n < len; n++) { + var node = family[n], t = node.getTransform(), m = t.getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + drawFunc.call(this, context); + context.restore(); + } + }, + drawHit: function() { + var attrs = this.attrs, drawFunc = attrs.drawHitFunc || attrs.drawFunc, context = this.getLayer().hitCanvas.getContext(); + + if(drawFunc && this.isVisible() && this.isListening()) { + var stage = this.getStage(), family = [], parent = this.parent; + + family.unshift(this); + while(parent) { + family.unshift(parent); + parent = parent.parent; + } + + context.save(); + this._applyLineJoin(context); + this._applyLineCap(context); + var len = family.length; + for(var n = 0; n < len; n++) { + var node = family[n], t = node.getTransform(), m = t.getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + drawFunc.call(this, context); + context.restore(); + } + }, + _setDrawFuncs: function() { + if(!this.attrs.drawFunc && this.drawFunc) { + this.setDrawFunc(this.drawFunc); + } + if(!this.attrs.drawHitFunc && this.drawHitFunc) { + this.setDrawHitFunc(this.drawHitFunc); + } + } + }; + Kinetic.Global.extend(Kinetic.Shape, Kinetic.Node); + + // add getters and setters + Kinetic.Node.addGettersSetters(Kinetic.Shape, ['stroke', 'lineJoin', 'lineCap', 'strokeWidth', 'drawFunc', 'drawHitFunc', 'cornerRadius']); + Kinetic.Node.addGetters(Kinetic.Shape, ['shadow', '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 draw function + * @name setDrawFunc + * @methodOf Kinetic.Shape.prototype + * @param {Function} drawFunc drawing function + */ + + /** + * set draw hit function used for hit detection + * @name setDrawHitFunc + * @methodOf Kinetic.Shape.prototype + * @param {Function} drawHitFunc drawing function used for hit detection + */ + + /** + * set corner radius + * @name setCornerRadius + * @methodOf Kinetic.Shape.prototype + * @param {Number} corner radius + */ + + /** + * set line cap. Can be butt, round, or square + * @name setLineCap + * @methodOf Kinetic.Shape.prototype + * @param {String} lineCap + */ + + /** + * 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 corner radius + * @name getCornerRadius + * @methodOf Kinetic.Shape.prototype + */ + + /** + * get draw function + * @name getDrawFunc + * @methodOf Kinetic.Shape.prototype + */ + + /** + * get draw hit function + * @name getDrawHitFunc + * @methodOf Kinetic.Shape.prototype + */ + + /** + * get shadow object + * @name getShadow + * @methodOf Kinetic.Shape.prototype + */ + + /** + * get fill + * @name getFill + * @methodOf Kinetic.Shape.prototype + */ + + /** + * get line cap + * @name getLineCap + * @methodOf Kinetic.Shape.prototype + */ +})(); + +(function() { + /** + * 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"; + + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + }, + drawFunc: function(context) { + context.beginPath(); + var cornerRadius = this.getCornerRadius(), width = this.getWidth(), height = this.getHeight(); + if(cornerRadius === 0) { + // simple rect - don't bother doing all that complicated maths stuff. + context.rect(0, 0, width, height); + } + else { + // arcTo would be nicer, but browser support is patchy (Opera) + context.moveTo(cornerRadius, 0); + context.lineTo(width - cornerRadius, 0); + context.arc(width - cornerRadius, cornerRadius, cornerRadius, Math.PI * 3 / 2, 0, false); + context.lineTo(width, height - cornerRadius); + context.arc(width - cornerRadius, height - cornerRadius, cornerRadius, 0, Math.PI / 2, false); + context.lineTo(cornerRadius, height); + context.arc(cornerRadius, height - cornerRadius, cornerRadius, Math.PI / 2, Math.PI, false); + context.lineTo(0, cornerRadius); + context.arc(cornerRadius, cornerRadius, cornerRadius, Math.PI, Math.PI * 3 / 2, false); + } + context.closePath(); + this.fillStroke(context); + } + }; + + Kinetic.Global.extend(Kinetic.Rect, Kinetic.Shape); + +})(); + +(function() { + /** + * 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'; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + }, + drawFunc: function(context) { + context.beginPath(); + context.arc(0, 0, this.getRadius(), 0, Math.PI * 2, true); + context.closePath(); + this.fillStroke(context); + }, + getWidth: function() { + return this.getRadius() * 2; + }, + getHeight: function() { + return this.getRadius() * 2; + }, + setWidth: function(width) { + Kinetic.Node.prototype.setWidth.call(this, width); + this.setRadius(width / 2); + }, + setHeight: function(height) { + Kinetic.Node.prototype.setHeight.call(this, height); + this.setRadius(height / 2); + } + }; + 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 + */ +})(); + +(function() { + /** + * Wedge constructor + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ + Kinetic.Wedge = function(config) { + this._initWedge(config); + }; + + Kinetic.Wedge.prototype = { + _initWedge: function(config) { + this.setDefaultAttrs({ + radius: 0, + angle: 0, + clickwise: true + }); + + this.shapeType = 'Wedge'; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + }, + drawFunc: function(context) { + context.beginPath(); + context.arc(0, 0, this.getRadius(), 0, this.getAngle(), this.getClockwise()); + context.lineTo(0, 0); + context.closePath(); + this.fillStroke(context); + }, + setAngleDeg: function(deg) { + this.setAngle(Kinetic.Type._degToRad(deg)); + }, + getAngleDeg: function() { + return Kinetic.Type._radToDeg(this.getAngle()); + } + }; + Kinetic.Global.extend(Kinetic.Wedge, Kinetic.Shape); + + // add getters setters + Kinetic.Node.addGettersSetters(Kinetic.Wedge, ['radius', 'angle', 'clockwise']); + + /** + * set radius + * @name setRadius + * @methodOf Kinetic.Wedge.prototype + * @param {Number} radius + */ + + /** + * set angle + * @name setAngle + * @methodOf Kinetic.Wedge.prototype + * @param {Number} angle + */ + + /** + * set clockwise draw direction. If set to true, the wedge will be drawn clockwise + * If set to false, the wedge will be drawn anti-clockwise. The default is true. + * @name setClockwise + * @methodOf Kinetic.Wedge.prototype + * @param {Boolean} clockwise + */ + + /** + * get radius + * @name getRadius + * @methodOf Kinetic.Wedge.prototype + */ + + /** + * get angle + * @name getAngle + * @methodOf Kinetic.Wedge.prototype + */ + + /** + * get clockwise + * @name getClockwise + * @methodOf Kinetic.Wedge.prototype + */ +})(); + +(function() { + /** + * 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"; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + }, + 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.fillStroke(context); + }, + /** + * 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 + */ + setRadius: function() { + var pos = Kinetic.Type._getXY([].slice.call(arguments)); + this.setAttr('radius', Kinetic.Type._merge(pos, this.getRadius())); + }, + getWidth: function() { + return this.getRadius().x * 2; + }, + getHeight: function() { + return this.getRadius().y * 2; + }, + setWidth: function(width) { + Kinetic.Node.prototype.setWidth.call(this, width); + this.setRadius({ + x: width / 2 + }); + }, + setHeight: function(height) { + Kinetic.Node.prototype.setHeight.call(this, height); + this.setRadius({ + y: height / 2 + }); + } + }; + Kinetic.Global.extend(Kinetic.Ellipse, Kinetic.Shape); + + // add getters setters + Kinetic.Node.addGetters(Kinetic.Ellipse, ['radius']); + + /** + * get radius + * @name getRadius + * @methodOf Kinetic.Ellipse.prototype + */ +})(); + +(function() { + /** + * 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"; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + + var that = this; + this.on('imageChange', function(evt) { + that._syncSize(); + }); + + this._syncSize(); + }, + drawFunc: function(context) { + var width = this.getWidth(), height = this.getHeight(), params, that = this; + + context.beginPath(); + context.rect(0, 0, width, height); + context.closePath(); + this.fillStroke(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 || 0; + var cropY = this.attrs.crop.y || 0; + var cropWidth = this.attrs.crop.width; + var cropHeight = this.attrs.crop.height; + params = [context, this.attrs.image, cropX, cropY, cropWidth, cropHeight, 0, 0, width, height]; + } + // no cropping + else { + params = [context, this.attrs.image, 0, 0, width, height]; + } + + if(this.getShadow()) { + this.applyShadow(context, function() { + that.drawImage.apply(that, params); + }); + } + else { + this.drawImage.apply(this, params); + } + + } + + }, + drawHitFunc: function(context) { + var width = this.getWidth(), height = this.getHeight(), imageHitRegion = this.imageHitRegion, appliedShadow = false; + + if(imageHitRegion) { + this.drawImage(context, imageHitRegion, 0, 0, width, height); + + context.beginPath(); + context.rect(0, 0, width, height); + context.closePath(); + this.stroke(context); + } + else { + context.beginPath(); + context.rect(0, 0, width, height); + context.closePath(); + this.fillStroke(context); + } + }, + /** + * apply filter + * @name applyFilter + * @methodOf Kinetic.Image.prototype + * @param {Object} config + * @param {Function} filter filter function + * @param {Object} [config] optional config object used to configure filter + * @param {Function} [callback] callback function to be called once + * filter has been applied + */ + applyFilter: function(filter, config, callback) { + 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()); + filter(imageData, config); + var that = this; + Kinetic.Type._getImage(imageData, function(imageObj) { + that.setImage(imageObj); + + if(callback) { + callback(); + } + }); + } + catch(e) { + Kinetic.Global.warn('Unable to apply filter. ' + e.message); + } + }, + /** + * set crop + * @name setCrop + * @methodOf Kinetic.Image.prototype + * @param {Object|Array} config + * @param {Number} config.x + * @param {Number} config.y + * @param {Number} config.width + * @param {Number} config.height + */ + setCrop: function() { + var config = [].slice.call(arguments); + var pos = Kinetic.Type._getXY(config); + var size = Kinetic.Type._getSize(config); + var both = Kinetic.Type._merge(pos, size); + this.setAttr('crop', Kinetic.Type._merge(both, this.getCrop())); + }, + /** + * create image hit region which enables more accurate hit detection mapping of the image + * by avoiding event detections for transparent pixels + * @name createImageHitRegion + * @methodOf Kinetic.Image.prototype + * @param {Function} [callback] callback function to be called once + * the image hit region has been created + */ + createImageHitRegion: 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.imageHitRegion = imageObj; + if(callback) { + callback(); + } + }); + } + catch(e) { + Kinetic.Global.warn('Unable to create image hit region. ' + e.message); + } + }, + /** + * clear image hit region + * @name clearImageHitRegion + * @methodOf Kinetic.Image.prototype + */ + clearImageHitRegion: function() { + delete this.imageHitRegion; + }, + _syncSize: function() { + if(this.attrs.image) { + if(!this.attrs.width) { + this.setWidth(this.attrs.image.width); + } + if(!this.attrs.height) { + this.setHeight(this.attrs.image.height); + } + } + } + }; + Kinetic.Global.extend(Kinetic.Image, Kinetic.Shape); + + // add getters setters + Kinetic.Node.addGettersSetters(Kinetic.Image, ['image']); + Kinetic.Node.addGetters(Kinetic.Image, ['crop']); + + /** + * set image + * @name setImage + * @methodOf Kinetic.Image.prototype + * @param {ImageObject} image + */ + + /** + * get crop + * @name getCrop + * @methodOf Kinetic.Image.prototype + */ + + /** + * get image + * @name getImage + * @methodOf Kinetic.Image.prototype + */ +})(); + +(function() { + /** + * Polygon constructor.  Polygons are defined by an array of points + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ + Kinetic.Polygon = function(config) { + this._initPolygon(config); + }; + + Kinetic.Polygon.prototype = { + _initPolygon: function(config) { + this.setDefaultAttrs({ + points: [] + }); + + this.shapeType = "Polygon"; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + }, + 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.fillStroke(context); + }, + /** + * set points array + * @name setPoints + * @methodOf Kinetic.Polygon.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] + */ + setPoints: function(val) { + this.setAttr('points', Kinetic.Type._getPoints(val)); + } + }; + Kinetic.Global.extend(Kinetic.Polygon, Kinetic.Shape); + + // add getters setters + Kinetic.Node.addGetters(Kinetic.Polygon, ['points']); + + /** + * get points array + * @name getPoints + * @methodOf Kinetic.Polygon.prototype + */ +})(); + +(function() { + /** + * 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"; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + + // 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 + Kinetic.Rect.prototype.drawFunc.call(this, 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 + for(var n = 0; n < textArr.length; n++) { + var text = textArr[n]; + + // horizontal alignment + context.save(); + if(this.attrs.align === 'right') { + context.translate(this.getWidth() - this._getTextSize(text).width - p * 2, 0); + } + else if(this.attrs.align === 'center') { + context.translate((this.getWidth() - this._getTextSize(text).width - p * 2) / 2, 0); + } + + this.fillStrokeText(context, text); + context.restore(); + context.translate(0, lineHeightPx); + } + context.restore(); + }, + drawHitFunc: Kinetic.Rect.prototype.drawFunc, + /** + * set text + * @name setText + * @methodOf Kinetic.Text.prototype + * @param {String} text + */ + setText: function(text) { + var str = Kinetic.Type._isString(text) ? text : text.toString(); + this.setAttr('text', str); + }, + /** + * get width + * @name getWidth + * @methodOf Kinetic.Text.prototype + */ + getWidth: function() { + return this.attrs.width === 'auto' ? this.getTextWidth() + this.attrs.padding * 2 : this.attrs.width; + }, + /** + * get height + * @name getHeight + * @methodOf Kinetic.Text.prototype + */ + getHeight: function() { + return this.attrs.height === 'auto' ? (this.getTextHeight() * this.textArr.length * this.attrs.lineHeight) + this.attrs.padding * 2 : this.attrs.height; + }, + /** + * get text width + * @name getTextWidth + * @methodOf Kinetic.Text.prototype + */ + getTextWidth: function() { + return this.textWidth; + }, + /** + * get text height + * @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) + }; + }, + fillText: function(context, text, skipShadow) { + var textFill = this.getTextFill(), textShadow = this.getTextShadow(); + if(textFill) { + context.save(); + if(!skipShadow && textShadow) { + this._applyTextShadow(context); + } + context.fillStyle = textFill; + context.fillText(text, 0, 0); + context.restore(); + + if(!skipShadow && textShadow && textShadow.opacity) { + this.fillText(context, text, true); + } + } + }, + strokeText: function(context, text, skipShadow) { + var textStroke = this.getTextStroke(), textStrokeWidth = this.getTextStrokeWidth(), textShadow = this.getTextShadow(); + if(textStroke || textStrokeWidth) { + context.save(); + if(!skipShadow && textShadow) { + this._applyTextShadow(context); + } + + context.lineWidth = textStrokeWidth || 2; + context.strokeStyle = textStroke || 'black'; + context.strokeText(text, 0, 0); + context.restore(); + + if(!skipShadow && textShadow && textShadow.opacity) { + this.strokeText(context, text, true); + } + } + }, + fillStrokeText: function(context, text) { + this.fillText(context, text); + this.strokeText(context, text, this.getTextShadow() && this.getTextFill()); + }, + /** + * set text shadow object + * @name setTextShadow + * @methodOf Kinetic.Text.prototype + * @param {Object} config + * @param {String} config.color + * @param {Number} config.blur + * @param {Array|Object|Number} config.offset + * @param {Number} config.opacity + */ + setTextShadow: function(config) { + var type = Kinetic.Type; + if(config.offset !== undefined) { + config.offset = type._getXY(config.offset); + } + this.setAttr('textShadow', type._merge(config, this.getTextShadow())); + }, + /** + * 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; + }, + _applyTextShadow: function(context) { + var textShadow = this.getTextShadow(); + if(textShadow) { + var aa = this.getAbsoluteOpacity(); + // defaults + var color = textShadow.color || 'black'; + var blur = textShadow.blur || 5; + var offset = textShadow.offset || { + x: 0, + y: 0 + }; + + if(textShadow.opacity) { + context.globalAlpha = textShadow.opacity * aa; + } + context.shadowColor = color; + context.shadowBlur = blur; + context.shadowOffsetX = offset.x; + context.shadowOffsetY = offset.y; + } + } + }; + Kinetic.Global.extend(Kinetic.Text, Kinetic.Shape); + + // add getters setters + Kinetic.Node.addGettersSetters(Kinetic.Text, ['fontFamily', 'fontSize', 'fontStyle', 'textFill', 'textStroke', 'textStrokeWidth', 'padding', 'align', 'lineHeight']); + Kinetic.Node.addGetters(Kinetic.Text, ['text', 'textShadow']); + /** + * 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 + */ + + /** + * 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 + */ +})(); + +(function() { + /** + * Line constructor.  Lines are defined by an array of points + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ + Kinetic.Line = function(config) { + this._initLine(config); + }; + + Kinetic.Line.prototype = { + _initLine: function(config) { + this.setDefaultAttrs({ + points: [], + lineCap: 'butt', + dashArray: [], + detectionType: 'pixel' + }); + + this.shapeType = "Line"; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + }, + drawFunc: function(context) { + var lastPos = {}, points = this.getPoints(), length = points.length, dashArray = this.getDashArray(), dashLength = dashArray.length; + context.beginPath(); + + context.moveTo(points[0].x, points[0].y); + + for(var n = 1; n < length; n++) { + var x = points[n].x; + var y = points[n].y; + if(dashLength > 0) { + // draw dashed line + var lastX = points[n - 1].x; + var lastY = points[n - 1].y; + this._dashedLine(context, lastX, lastY, x, y, dashArray); + } + else { + // draw normal line + context.lineTo(x, y); + } + } + + this.stroke(context); + }, + /** + * 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] + */ + setPoints: function(val) { + this.setAttr('points', Kinetic.Type._getPoints(val)); + }, + /** + * 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']); + Kinetic.Node.addGetters(Kinetic.Line, ['points']); + + /** + * set dash array. + * @name setDashArray + * @methodOf Kinetic.Line.prototype + * @param {Array} dashArray + * examples:
    + * [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 + */ + + /** + * get dash array + * @name getDashArray + * @methodOf Kinetic.Line.prototype + */ + + /** + * get points array + * @name getPoints + * @methodOf Kinetic.Line.prototype + */ +})(); + +(function() { + /** + * 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 + }); + this.shapeType = "Sprite"; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + + this.anim = new Kinetic.Animation(); + var that = this; + this.on('animationChange', 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]; + + 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); + } + }, + drawHitFunc: 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.fillStroke(context); + }, + /** + * 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 object + * @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 + */ +})(); + +(function() { + /** + * 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"; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + }, + 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.fillStroke(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 + */ +})(); + +(function() { + /** + * RegularPolygon constructor.  Examples include triangles, squares, pentagons, hexagons, etc. + * @constructor + * @augments Kinetic.Shape + * @param {Object} config + */ + Kinetic.RegularPolygon = function(config) { + this._initRegularPolygon(config); + }; + + Kinetic.RegularPolygon.prototype = { + _initRegularPolygon: function(config) { + this.setDefaultAttrs({ + radius: 0, + sides: 0 + }); + + this.shapeType = "RegularPolygon"; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + }, + 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.fillStroke(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 + */ +})(); + +(function() { + /** + * 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; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + + 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.fillStroke(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)); + if(P2x < P1x) + run *= -1; + 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)); + if(P2x < P1x) + run *= -1; + 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 + */ +})(); + +(function() { + /** + * 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; + + // call super constructor + Kinetic.Shape.call(this, config); + this._setDrawFuncs(); + + 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; + + var appliedShadow = this.appliedShadow; + for(var i = 0; i < glyphInfo.length; i++) { + /* + * need to reset appliedShadow flag so that shadows + * are appropriately applied to each line of text + */ + this.appliedShadow = appliedShadow; + + 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.fillStrokeText(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; + }, + /** + * set text + * @name setText + * @methodOf Kinetic.TextPath.prototype + * @param {String} text + */ + setText: function(text) { + Kinetic.Text.prototype.setText.call(this, text); + }, + _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']); + Kinetic.Node.addGetters(Kinetic.TextPath, ['text', 'textShadow']); + + // reference Text methods + Kinetic.TextPath.prototype.setTextShadow = Kinetic.Text.prototype.setTextShadow; + Kinetic.TextPath.prototype.fillText = Kinetic.Text.prototype.fillText; + Kinetic.TextPath.prototype.strokeText = Kinetic.Text.prototype.strokeText; + Kinetic.TextPath.prototype.fillStrokeText = Kinetic.Text.prototype.strokeText; + + /** + * 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 + */ + + /** + * 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 + */ +})(); + diff --git a/tests/html/special/scripts/main.js b/tests/html/special/scripts/main.js new file mode 100644 index 00000000..bfb1e773 --- /dev/null +++ b/tests/html/special/scripts/main.js @@ -0,0 +1,34 @@ +require(['kinetic-vcurrent'], function(Kinetic) { + + var stage = new Kinetic.Stage({ + container: 'container', + width: 578, + height: 200 + }); + var layer = new Kinetic.Layer(); + var rectX = stage.getWidth() / 2 - 50; + var rectY = stage.getHeight() / 2 - 25; + + var box = new Kinetic.Rect({ + x: rectX, + y: rectY, + width: 100, + height: 50, + fill: '#00D2FF', + stroke: 'black', + strokeWidth: 4, + draggable: true + }); + + // add cursor styling + box.on('mouseover', function() { + document.body.style.cursor = 'pointer'; + }); + box.on('mouseout', function() { + document.body.style.cursor = 'default'; + }); + + layer.add(box); + stage.add(layer); + +}); diff --git a/tests/html/special/scripts/require.js b/tests/html/special/scripts/require.js new file mode 100644 index 00000000..5f088080 --- /dev/null +++ b/tests/html/special/scripts/require.js @@ -0,0 +1,35 @@ +/* + RequireJS 2.1.2 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. + Available via the MIT or new BSD license. + see: http://github.com/jrburke/requirejs for details +*/ +var requirejs,require,define; +(function(Y){function H(b){return"[object Function]"===L.call(b)}function I(b){return"[object Array]"===L.call(b)}function x(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(H(n)){if(this.events.error)try{e=j.execCb(c,n,b,e)}catch(d){a=d}else e=j.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!==this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=[this.map.id],a.requireType="define",C(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&& +!this.ignore&&(p[c]=e,l.onResourceLoad))l.onResourceLoad(j,this.map,this.depMaps);delete k[c];this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=h(a.prefix);this.depMaps.push(d);s(d,"defined",t(this,function(e){var n,d;d=this.map.name;var v=this.map.parentMap?this.map.parentMap.name:null,f=j.makeRequire(a.parentMap,{enableBuildCallback:!0, +skipMap:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,v,!0)})||""),e=h(a.prefix+"!"+d,this.map.parentMap),s(e,"defined",t(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=i(k,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",t(this,function(a){this.emit("error",a)}));d.enable()}}else n=t(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=t(this,function(a){this.inited=!0;this.error= +a;a.requireModules=[b];E(k,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&delete k[a.map.id]});C(a)}),n.fromText=t(this,function(e,c){var d=a.name,u=h(d),v=O;c&&(e=c);v&&(O=!1);q(u);r(m.config,b)&&(m.config[d]=m.config[b]);try{l.exec(e)}catch(k){throw Error("fromText eval for "+d+" failed: "+k);}v&&(O=!0);this.depMaps.push(u);j.completeLoad(d);f([d],n)}),e.load(a.name,f,n,m)}));j.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){this.enabling=this.enabled=!0;x(this.depMaps,t(this,function(a, +b){var c,e;if("string"===typeof a){a=h(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=i(N,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;s(a,"defined",t(this,function(a){this.defineDep(b,a);this.check()}));this.errback&&s(a,"error",this.errback)}c=a.id;e=k[c];!r(N,c)&&(e&&!e.enabled)&&j.enable(a,this)}));E(this.pluginMaps,t(this,function(a){var b=i(k,a.id);b&&!b.enabled&&j.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c= +this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){x(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};j={config:m,contextName:b,registry:k,defined:p,urlFetched:S,defQueue:F,Module:W,makeModuleMap:h,nextTick:l.nextTick,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=m.pkgs,c=m.shim,e={paths:!0,config:!0,map:!0};E(a,function(a,b){e[b]?"map"===b?Q(m[b],a,!0,!0):Q(m[b],a,!0):m[b]=a});a.shim&&(E(a.shim,function(a, +b){I(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=j.makeShimExports(a);c[b]=a}),m.shim=c);a.packages&&(x(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name,location:a.location||a.name,main:(a.main||"main").replace(ga,"").replace(aa,"")}}),m.pkgs=b);E(k,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=h(b))});if(a.deps||a.callback)j.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(Y,arguments)); +return b||a.exports&&Z(a.exports)}},makeRequire:function(a,d){function f(e,c,u){var i,m;d.enableBuildCallback&&(c&&H(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(H(c))return C(J("requireargs","Invalid require call"),u);if(a&&r(N,e))return N[e](k[a.id]);if(l.get)return l.get(j,e,a);i=h(e,a,!1,!0);i=i.id;return!r(p,i)?C(J("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):p[i]}K();j.nextTick(function(){K();m=q(h(null,a));m.skipMap=d.skipMap; +m.init(e,c,u,{enabled:!0});B()});return f}d=d||{};Q(f,{isBrowser:z,toUrl:function(b){var d=b.lastIndexOf("."),g=null;-1!==d&&(g=b.substring(d,b.length),b=b.substring(0,d));return j.nameToUrl(c(b,a&&a.id,!0),g)},defined:function(b){return r(p,h(b,a,!1,!0).id)},specified:function(b){b=h(b,a,!1,!0).id;return r(p,b)||r(k,b)}});a||(f.undef=function(b){w();var c=h(b,a,!0),d=i(k,b);delete p[b];delete S[c.url];delete X[b];d&&(d.events.defined&&(X[b]=d.events),delete k[b])});return f},enable:function(a){i(k, +a.id)&&q(a).enable()},completeLoad:function(a){var b,c,d=i(m.shim,a)||{},h=d.exports;for(w();F.length;){c=F.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);D(c)}c=i(k,a);if(!b&&!r(p,a)&&c&&!c.inited){if(m.enforceDefine&&(!h||!Z(h)))return y(a)?void 0:C(J("nodefine","No define call for "+a,null,[a]));D([a,d.deps||[],d.exportsFn])}B()},nameToUrl:function(a,b){var c,d,h,f,j,k;if(l.jsExtRegExp.test(a))f=a+(b||"");else{c=m.paths;d=m.pkgs;f=a.split("/");for(j=f.length;0f.attachEvent.toString().indexOf("[native code"))&&!V?(O=!0,f.attachEvent("onreadystatechange", +b.onScriptLoad)):(f.addEventListener("load",b.onScriptLoad,!1),f.addEventListener("error",b.onScriptError,!1)),f.src=d,K=f,D?A.insertBefore(f,D):A.appendChild(f),K=null,f;$&&(importScripts(d),b.completeLoad(c))};z&&M(document.getElementsByTagName("script"),function(b){A||(A=b.parentNode);if(s=b.getAttribute("data-main"))return q.baseUrl||(G=s.split("/"),ba=G.pop(),ca=G.length?G.join("/")+"/":"./",q.baseUrl=ca,s=ba),s=s.replace(aa,""),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var i, +f;"string"!==typeof b&&(d=c,c=b,b=null);I(c)||(d=c,c=[]);!c.length&&H(d)&&d.length&&(d.toString().replace(ia,"").replace(ja,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c));if(O){if(!(i=K))P&&"interactive"===P.readyState||M(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return P=b}),i=P;i&&(b||(b=i.getAttribute("data-requiremodule")),f=B[i.getAttribute("data-requirecontext")])}(f?f.defQueue:R).push([b,c,d])};define.amd= +{jQuery:!0};l.exec=function(b){return eval(b)};l(q)}})(this); \ No newline at end of file