/** * KineticJS JavaScript Library core * http://www.kineticjs.com/ * Copyright 2012, Eric Rowell * Licensed under the MIT or GPL Version 2 licenses. * Date: Apr 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. */ /////////////////////////////////////////////////////////////////////// // Global Object /////////////////////////////////////////////////////////////////////// /** * Kinetic Namespace * @namespace */ var Kinetic = {}; /** * Kinetic Global Object * @property {Object} GlobalObjet */ Kinetic.GlobalObject = { stages: [], idCounter: 0, frame: { time: 0, timeDiff: 0, lastTime: 0 }, drag: { moving: false, node: undefined, offset: { x: 0, y: 0 } }, extend: function(obj1, obj2) { for(var key in obj2.prototype) { if(obj2.prototype.hasOwnProperty(key) && obj1.prototype[key] === undefined) { obj1.prototype[key] = obj2.prototype[key]; } } }, _isaCanvasAnimating: function() { for(var n = 0; n < this.stages.length; n++) { var stage = this.stages[n]; if(stage.isAnimating) { return true; } for(var i = 0; i < stage.children.length; i++) { var layer = stage.children[i]; if(layer.transitions.length > 0) { return true; } } } this.frame.lastTime = 0; return false; }, _endTransition: function() { var config = this.config; for(var key in config) { if(config.hasOwnProperty(key)) { if(config[key].x !== undefined || config[key].y !== undefined) { var propArray = ['x', 'y']; for(var n = 0; n < propArray.length; n++) { var prop = propArray[n]; if(config[key][prop] !== undefined) { this.node[key][prop] = config[key][prop]; } } } else { this.node[key] = config[key]; } } } }, _transitionPow: function(transition, key, prop, powFunc) { var pow = powFunc(); var config = transition.config; var timeDiff = this.frame.timeDiff; if(prop !== undefined) { var start = transition.starts[key][prop]; var change = config[key][prop] - start; var b = change / (Math.pow(config.duration * 1000, pow)); transition.node[key][prop] = b * Math.pow(transition.time, pow) + start; } else { var start = transition.starts[key]; var change = config[key] - start; var b = change / (Math.pow(config.duration * 1000, pow)); transition.node[key] = b * Math.pow(transition.time, pow) + start; } }, _chooseTransition: function(transition, key, prop) { var config = transition.config; var that = this; switch(config.easing) { case 'ease-in': this._transitionPow(transition, key, prop, function() { return 2.5; }); break; case 'ease-out': this._transitionPow(transition, key, prop, function() { return 0.4; }); break; case 'ease-in-out': this._transitionPow(transition, key, prop, function() { var change = -2.1; var b = change / (config.duration * 1000); return 2.5 + b * transition.time; }); break; // none is default default: this._transitionPow(transition, key, prop, function() { return 1; }); break; } }, _runTransition: function(transition) { var config = transition.config; for(var key in config) { if(config.hasOwnProperty(key) && key !== 'duration' && key !== 'easing' && key !== 'callback') { if(config[key].x !== undefined || config[key].y !== undefined) { var propArray = ['x', 'y']; for(var n = 0; n < propArray.length; n++) { var prop = propArray[n]; if(config[key][prop] !== undefined) { this._chooseTransition(transition, key, prop); } } } else { this._chooseTransition(transition, key); } } } }, _clearTransition: function(node) { var layer = node.getLayer(); for(var n = 0; n < layer.transitions.length; n++) { if(layer.transitions[n].node.id === node.id) { layer.transitions.splice(n, 1); } } }, _runFrames: function() { for(var n = 0; n < this.stages.length; n++) { var stage = this.stages[n]; // run animation if available if(stage.isAnimating && stage.onFrameFunc !== undefined) { stage.onFrameFunc(this.frame); } // loop through layers var layers = stage.getChildren(); for(var k = 0; k < layers.length; k++) { var layer = layers[k]; var didTransition = false; // loop through transitions for(var i = 0; i < layer.transitions.length; i++) { didTransition = true; var transition = layer.transitions[i]; transition.time += this.frame.timeDiff; if(transition.time >= transition.config.duration * 1000) { this._endTransition.apply(transition); this._clearTransition(transition.node); if(transition.config.callback !== undefined) { transition.config.callback(); } } else { this._runTransition(transition); } } if(didTransition) { layer.draw(); } } } }, _updateFrameObject: function() { var date = new Date(); var time = date.getTime(); if(this.frame.lastTime === 0) { this.frame.lastTime = time; } else { this.frame.timeDiff = time - this.frame.lastTime; this.frame.lastTime = time; this.frame.time += this.frame.timeDiff; } }, _animationLoop: function() { if(this._isaCanvasAnimating()) { this._updateFrameObject(); this._runFrames(); var that = this; requestAnimFrame(function() { that._animationLoop(); }); } }, _handleAnimation: function() { var that = this; if(this._isaCanvasAnimating()) { that._animationLoop(); } } }; window.requestAnimFrame = (function(callback) { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60); }; })(); /////////////////////////////////////////////////////////////////////// // Node /////////////////////////////////////////////////////////////////////// /** * Node constructor.  Nodes are entities that can move around * and have events bound to them. They are the building blocks of a KineticJS * application * @constructor * @param {Object} config */ Kinetic.Node = function(config) { this.visible = true; this.isListening = true; this.name = undefined; this.alpha = 1; this.x = 0; this.y = 0; this.scale = { x: 1, y: 1 }; this.rotation = 0; this.centerOffset = { x: 0, y: 0 }; this.eventListeners = {}; this.dragConstraint = 'none'; this.dragBounds = {}; this._draggable = false; // set properties from config if(config) { for(var key in config) { // handle special keys switch (key) { case 'draggable': this.draggable(config[key]); break; case 'listen': this.listen(config[key]); break; case 'rotationDeg': this.rotation = config[key] * Math.PI / 180; break; default: this[key] = config[key]; break; } } } // overrides if(this.centerOffset.x === undefined) { this.centerOffset.x = 0; } if(this.centerOffset.y === undefined) { this.centerOffset.y = 0; } }; /* * Node methods */ Kinetic.Node.prototype = { /** * bind events to the node. KineticJS supports mouseover, mousemove, * mouseout, mousedown, mouseup, click, dblclick, touchstart, touchmove, * touchend, dbltap, dragstart, dragmove, and dragend. Pass in a string * of event types delimmited by a space to bind multiple events at once * such as 'mousedown mouseup mousemove'. include a namespace to bind an * event by name such as 'click.foobar'. * @param {String} typesStr * @param {function} handler */ on: function(typesStr, handler) { var types = typesStr.split(' '); /* * loop through types and attach event listeners to * each one. eg. 'click mouseover.namespace mouseout' * will create three event bindings */ for(var n = 0; n < types.length; n++) { var type = types[n]; var event = (type.indexOf('touch') === -1) ? 'on' + type : type; var parts = event.split('.'); var baseEvent = parts[0]; var name = parts.length > 1 ? parts[1] : ''; if(!this.eventListeners[baseEvent]) { this.eventListeners[baseEvent] = []; } this.eventListeners[baseEvent].push({ name: name, handler: handler }); } }, /** * remove event bindings from the node. Pass in a string of * event types delimmited by a space to remove multiple event * bindings at once such as 'mousedown mouseup mousemove'. * include a namespace to remove an event binding by name * such as 'click.foobar'. * @param {String} typesStr */ off: function(typesStr) { var types = typesStr.split(' '); for(var n = 0; n < types.length; n++) { var type = types[n]; var event = (type.indexOf('touch') === -1) ? 'on' + type : type; var parts = event.split('.'); var baseEvent = parts[0]; if(this.eventListeners[baseEvent] && parts.length > 1) { var name = parts[1]; for(var i = 0; i < this.eventListeners[baseEvent].length; i++) { if(this.eventListeners[baseEvent][i].name === name) { this.eventListeners[baseEvent].splice(i, 1); if(this.eventListeners[baseEvent].length === 0) { this.eventListeners[baseEvent] = undefined; } break; } } } else { this.eventListeners[baseEvent] = undefined; } } }, /** * show node */ show: function() { this.visible = true; }, /** * hide node */ hide: function() { this.visible = false; }, /** * get zIndex */ getZIndex: function() { return this.index; }, /** * get absolute z-index by taking into account * all parent and sibling indices */ getAbsoluteZIndex: function() { var level = this.getLevel(); var stage = this.getStage(); var that = this; var index = 0; function addChildren(children) { var nodes = []; for(var n = 0; n < children.length; n++) { var child = children[n]; index++; if(child.className !== 'Shape') { nodes = nodes.concat(child.getChildren()); } if(child.id === that.id) { n = children.length; } } if(nodes.length > 0 && nodes[0].getLevel() <= level) { addChildren(nodes); } } if(that.className !== 'Stage') { addChildren(that.getStage().getChildren()); } return index; }, /** * get node level in node tree */ getLevel: function() { var level = 0; var parent = this.parent; while(parent) { level++; parent = parent.parent; } return level; }, /** * set node scale. If only one parameter is passed in, * then both scaleX and scaleY are set with that parameter * @param {Number} scaleX * @param {Number} scaleY */ setScale: function(scaleX, scaleY) { if(scaleY) { this.scale.x = scaleX; this.scale.y = scaleY; } else { this.scale.x = scaleX; this.scale.y = scaleX; } }, /** * get scale */ getScale: function() { return this.scale; }, /** * set node position * @param {Number} x * @param {Number} y */ setPosition: function(x, y) { this.x = x; this.y = y; }, /** * get node position relative to container */ getPosition: function() { return { x: this.x, y: this.y }; }, /** * get absolute position relative to stage */ getAbsolutePosition: function() { return this.getAbsoluteTransform().getTranslation(); }, /** * move node by an amount * @param {Number} x * @param {Number} y */ move: function(x, y) { this.x += x; this.y += y; }, /** * set node rotation in radians * @param {Number} theta */ setRotation: function(theta) { this.rotation = theta; }, /** * set node rotation in degrees * @param {Number} deg */ setRotationDeg: function(deg) { this.rotation = (deg * Math.PI / 180); }, /** * get rotation in radians */ getRotation: function() { return this.rotation; }, /** * get rotation in degrees */ getRotationDeg: function() { return this.rotation * 180 / Math.PI; }, /** * rotate node by an amount in radians * @param {Number} theta */ rotate: function(theta) { this.rotation += theta; }, /** * rotate node by an amount in degrees * @param {Number} deg */ rotateDeg: function(deg) { this.rotation += (deg * Math.PI / 180); }, /** * listen or don't listen to events * @param {Boolean} isListening */ listen: function(isListening) { this.isListening = isListening; }, /** * move node to top */ moveToTop: function() { var index = this.index; this.parent.children.splice(index, 1); this.parent.children.push(this); this.parent._setChildrenIndices(); }, /** * move node up */ moveUp: function() { var index = this.index; this.parent.children.splice(index, 1); this.parent.children.splice(index + 1, 0, this); this.parent._setChildrenIndices(); }, /** * move node down */ moveDown: function() { var index = this.index; if(index > 0) { this.parent.children.splice(index, 1); this.parent.children.splice(index - 1, 0, this); this.parent._setChildrenIndices(); } }, /** * move node to bottom */ moveToBottom: function() { var index = this.index; this.parent.children.splice(index, 1); this.parent.children.unshift(this); this.parent._setChildrenIndices(); }, /** * set zIndex * @param {int} zIndex */ setZIndex: function(zIndex) { var index = this.index; this.parent.children.splice(index, 1); this.parent.children.splice(zIndex, 0, this); this.parent._setChildrenIndices(); }, /** * set alpha. Alpha values range from 0 to 1. * A node with an alpha of 0 is fully transparent, and a node * with an alpha of 1 is fully opaque * @param {Object} alpha */ setAlpha: function(alpha) { this.alpha = alpha; }, /** * get alpha. Alpha values range from 0 to 1. * A node with an alpha of 0 is fully transparent, and a node * with an alpha of 1 is fully opaque */ getAlpha: function() { return this.alpha; }, /** * get absolute alpha */ getAbsoluteAlpha: function() { var absAlpha = 1; var node = this; // traverse upwards while(node.className !== 'Stage') { absAlpha *= node.alpha; node = node.parent; } return absAlpha; }, /** * enable or disable drag and drop * @param {Boolean} isDraggable */ draggable: function(isDraggable) { if(this.draggable !== isDraggable) { if(isDraggable) { this._initDrag(); } else { this._dragCleanup(); } this._draggable = isDraggable; } }, /** * determine if node is currently in drag and drop mode */ isDragging: function() { var go = Kinetic.GlobalObject; return go.drag.node !== undefined && go.drag.node.id === this.id && go.drag.moving; }, /** * move node to another container * @param {Container} newContainer */ moveTo: function(newContainer) { var parent = this.parent; // remove from parent's children parent.children.splice(this.index, 1); parent._setChildrenIndices(); // add to new parent newContainer.children.push(this); this.index = newContainer.children.length - 1; this.parent = newContainer; newContainer._setChildrenIndices(); // update children hashes if(this.name) { parent.childrenNames[this.name] = undefined; newContainer.childrenNames[this.name] = this; } }, /** * get parent container */ getParent: function() { return this.parent; }, /** * get layer associated to node */ getLayer: function() { if(this.className === 'Layer') { return this; } else { return this.getParent().getLayer(); } }, /** * get stage associated to node */ getStage: function() { if(this.className === 'Stage') { return this; } else { return this.getParent().getStage(); } }, /** * get name */ getName: function() { return this.name; }, /** * set center offset * @param {Number} x * @param {Number} y */ setCenterOffset: function(x, y) { this.centerOffset.x = x; this.centerOffset.y = y; }, /** * get center offset */ getCenterOffset: function() { return this.centerOffset; }, /** * transition node to another state. Any property that can accept a real * number can be transitioned, including x, y, rotation, alpha, strokeWidth, * radius, scale.x, scale.y, centerOffset.x, centerOffset.y, etc. * @param {Object} config * @config {Number} [duration] duration that the transition runs in seconds * @config {String} [easing] easing function. can be none, ease-in, ease-out, or ease-in-out. * none is the default * @config {Function} [callback] callback function to be executed when * transition completes */ transitionTo: function(config) { var layer = this.getLayer(); var that = this; var duration = config.duration * 1000; var starts = {}; /* * clear transition if one is currenlty running. * This make it easy to start new transitions without * having to explicitly cancel old ones */ Kinetic.GlobalObject._clearTransition(this); for(var key in config) { if(config.hasOwnProperty(key) && key !== 'duration' && key !== 'easing' && key !== 'callback') { if(config[key].x !== undefined || config[key].y !== undefined) { starts[key] = {}; var propArray = ['x', 'y']; for(var n = 0; n < propArray.length; n++) { var prop = propArray[n]; if(config[key][prop] !== undefined) { starts[key][prop] = this[key][prop]; } } } else { starts[key] = this[key]; } } } layer.transitions.push({ id: layer.transitionIdCounter++, time: 0, config: config, node: this, starts: starts }); Kinetic.GlobalObject._handleAnimation(); }, /** * set drag constraint * @param {String} constraint */ setDragConstraint: function(constraint) { this.dragConstraint = constraint; }, /** * get drag constraint */ getDragConstraint: function() { return this.dragConstraint; }, /** * set drag bounds * @param {Object} bounds * @config {Number} [left] left bounds position * @config {Number} [top] top bounds position * @config {Number} [right] right bounds position * @config {Number} [bottom] bottom bounds position */ setDragBounds: function(bounds) { this.dragBounds = bounds; }, /** * get drag bounds */ getDragBounds: function() { return this.dragBounds; }, /** * get transform of the node while taking into * account the transforms of its parents */ getAbsoluteTransform: function() { // absolute transform var am = new Kinetic.Transform(); var family = []; var parent = this.parent; family.unshift(this); while(parent) { family.unshift(parent); parent = parent.parent; } for(var n = 0; n < family.length; n++) { var node = family[n]; var m = node.getTransform(); am.multiply(m); } return am; }, /** * get transform of the node while not taking * into account the transforms of its parents */ getTransform: function() { var m = new Kinetic.Transform(); if(this.x !== 0 || this.y !== 0) { m.translate(this.x, this.y); } if(this.rotation !== 0) { m.rotate(this.rotation); } if(this.scale.x !== 1 || this.scale.y !== 1) { m.scale(this.scale.x, this.scale.y); } if(this.centerOffset.x !== 0 || this.centerOffset.y !== 0) { m.translate(-1 * this.centerOffset.x, -1 * this.centerOffset.y); } return m; }, /** * initialize drag and drop */ _initDrag: function() { var go = Kinetic.GlobalObject; var that = this; this.on('mousedown.initdrag touchstart.initdrag', function(evt) { var stage = that.getStage(); var pos = stage.getUserPosition(); if(pos) { var m = that.getTransform().getTranslation(); var am = that.getAbsoluteTransform().getTranslation(); go.drag.node = that; go.drag.offset.x = pos.x - that.getAbsoluteTransform().getTranslation().x; go.drag.offset.y = pos.y - that.getAbsoluteTransform().getTranslation().y; } }); }, /** * remove drag and drop event listener */ _dragCleanup: function() { this.off('mousedown.initdrag'); this.off('touchstart.initdrag'); }, /** * handle node events * @param {String} eventType * @param {Event} evt */ _handleEvents: function(eventType, evt) { if(this.className === 'Shape') { evt.shape = this; } var stage = this.getStage(); this._handleEvent(this, stage.mouseoverShape, stage.mouseoutShape, eventType, evt); }, /** * handle node event */ _handleEvent: function(node, mouseoverNode, mouseoutNode, eventType, evt) { var el = node.eventListeners; var okayToRun = true; /* * determine if event handler should be skipped by comparing * parent nodes */ if(eventType === 'onmouseover' && mouseoutNode && mouseoutNode.id === node.id) { okayToRun = false; } else if(eventType === 'onmouseout' && mouseoverNode && mouseoverNode.id === node.id) { okayToRun = false; } if(el[eventType] && okayToRun) { var events = el[eventType]; for(var i = 0; i < events.length; i++) { events[i].handler.apply(node, [evt]); } } var mouseoverParent = mouseoverNode ? mouseoverNode.parent : undefined; var mouseoutParent = mouseoutNode ? mouseoutNode.parent : undefined; // simulate event bubbling if(!evt.cancelBubble && node.parent.className !== 'Stage') { this._handleEvent(node.parent, mouseoverParent, mouseoutParent, eventType, evt); } } }; /////////////////////////////////////////////////////////////////////// // Container /////////////////////////////////////////////////////////////////////// /** * Container constructor.  Containers are used to contain nodes or other containers * @constructor */ Kinetic.Container = function() { this.children = []; this.childrenNames = {}; }; /* * Container methods */ Kinetic.Container.prototype = { /** * get children */ getChildren: function() { return this.children; }, /** * get child node by name * @param {String} name */ getChild: function(name) { return this.childrenNames[name]; }, /** * remove all children */ removeChildren: function() { while(this.children.length > 0) { this.remove(this.children[0]); } }, /** * remove child from container * @param {Node} child */ _remove: function(child) { if(child.name !== undefined) { this.childrenNames[child.name] = undefined; } this.children.splice(child.index, 1); this._setChildrenIndices(); child = undefined; }, /** * draw children */ _drawChildren: function() { var children = this.children; for(var n = 0; n < children.length; n++) { var child = children[n]; if(child.className === 'Shape') { child._draw(child.getLayer()); } else { child._draw(); } } }, /** * add node to container * @param {Node} child */ _add: function(child) { if(child.name) { this.childrenNames[child.name] = child; } child.id = Kinetic.GlobalObject.idCounter++; child.index = this.children.length; child.parent = this; this.children.push(child); }, /** * set children indices */ _setChildrenIndices: function() { /* * if reordering Layers, remove all canvas elements * from the container except the buffer and backstage canvases * and then readd all the layers */ if(this.className === 'Stage') { var canvases = this.content.children; var bufferCanvas = canvases[0]; var backstageCanvas = canvases[1]; this.content.innerHTML = ''; this.content.appendChild(bufferCanvas); this.content.appendChild(backstageCanvas); } for(var n = 0; n < this.children.length; n++) { this.children[n].index = n; if(this.className === 'Stage') { this.content.appendChild(this.children[n].canvas); } } } }; /////////////////////////////////////////////////////////////////////// // Stage /////////////////////////////////////////////////////////////////////// /** * Stage constructor. A stage is used to contain multiple layers and handle * animations * @constructor * @augments Kinetic.Container * @augments Kinetic.Node * @param {String|DomElement} cont Container id or DOM element * @param {int} width * @param {int} height */ Kinetic.Stage = function(config) { /* * if container is a string, assume it's an id for * a DOM element */ if( typeof config.container === 'string') { config.container = document.getElementById(config.container); } this.className = 'Stage'; this.container = config.container; this.content = document.createElement('div'); this.width = config.width; this.height = config.height; this.scale = { x: 1, y: 1 }; this.dblClickWindow = 400; this.clickStart = false; this.targetShape = undefined; this.targetFound = false; this.mouseoverShape = undefined; this.mouseoutShape = undefined; // desktop flags this.mousePos = undefined; this.mouseDown = false; this.mouseUp = false; // mobile flags this.touchPos = undefined; this.touchStart = false; this.touchEnd = false; // set stage id this.id = Kinetic.GlobalObject.idCounter++; // animation support this.isAnimating = false; this.onFrameFunc = undefined; this._buildDOM(); this._listen(); this._prepareDrag(); // add stage to global object Kinetic.GlobalObject.stages.push(this); // call super constructors Kinetic.Container.apply(this, []); Kinetic.Node.apply(this, [config]); }; /* * Stage methods */ Kinetic.Stage.prototype = { /** * sets onFrameFunc for animation * @param {function} func */ onFrame: function(func) { this.onFrameFunc = func; }, /** * start animation */ start: function() { this.isAnimating = true; Kinetic.GlobalObject._handleAnimation(); }, /** * stop animation */ stop: function() { this.isAnimating = false; Kinetic.GlobalObject._handleAnimation(); }, /** * draw children */ draw: function() { this._drawChildren(); }, /** * set stage size * @param {int} width * @param {int} height */ setSize: function(width, height) { var layers = this.children; for(var n = 0; n < layers.length; n++) { var layer = layers[n]; layer.getCanvas().width = width; layer.getCanvas().height = height; layer.draw(); } // set stage dimensions this.width = width; this.height = height; // set buffer layer and path layer sizes this.bufferLayer.getCanvas().width = width; this.bufferLayer.getCanvas().height = height; this.pathLayer.getCanvas().width = width; this.pathLayer.getCanvas().height = height; }, /** * return stage size */ getSize: function() { return { width: this.width, height: this.height }; }, /** * clear all layers */ clear: function() { var layers = this.children; for(var n = 0; n < layers.length; n++) { layers[n].clear(); } }, /** * Creates a composite data URL and passes it to a callback. If MIME type is not * specified, then "image/png" will result. For "image/jpeg", specify a quality * level as quality (range 0.0 - 1.0) * @param {function} callback * @param {String} mimeType (optional) * @param {Number} quality (optional) */ toDataURL: function(callback, mimeType, quality) { var bufferLayer = this.bufferLayer; var bufferContext = bufferLayer.getContext(); var layers = this.children; function addLayer(n) { var dataURL = layers[n].getCanvas().toDataURL(); var imageObj = new Image(); imageObj.onload = function() { bufferContext.drawImage(this, 0, 0); n++; if(n < layers.length) { addLayer(n); } else { callback(bufferLayer.getCanvas().toDataURL(mimeType, quality)); } }; imageObj.src = dataURL; } bufferLayer.clear(); addLayer(0); }, /** * remove layer from stage * @param {Layer} layer */ remove: function(layer) { // remove layer canvas from dom this.content.removeChild(layer.canvas); this._remove(layer); }, /** * bind event listener to container DOM element * @param {String} typesStr * @param {function} handler */ onContainer: function(typesStr, handler) { var types = typesStr.split(' '); for(var n = 0; n < types.length; n++) { var baseEvent = types[n]; this.container.addEventListener(baseEvent, handler, false); } }, /** * add layer to stage * @param {Layer} layer */ add: function(layer) { if(layer.name) { this.childrenNames[layer.name] = layer; } layer.canvas.width = this.width; layer.canvas.height = this.height; this._add(layer); // draw layer and append canvas to container layer.draw(); this.content.appendChild(layer.canvas); }, /** * get mouse position for desktop apps * @param {Event} evt */ getMousePosition: function(evt) { return this.mousePos; }, /** * get touch position for mobile apps * @param {Event} evt */ getTouchPosition: function(evt) { return this.touchPos; }, /** * get user position (mouse position or touch position) * @param {Event} evt */ getUserPosition: function(evt) { return this.getTouchPosition() || this.getMousePosition(); }, /** * get container DOM element */ getContainer: function() { return this.container; }, /** * get stage */ getStage: function() { return this; }, /** * detect event * @param {Shape} shape */ _detectEvent: function(shape, evt) { var isDragging = Kinetic.GlobalObject.drag.moving; var go = Kinetic.GlobalObject; var pos = this.getUserPosition(); var el = shape.eventListeners; if(this.targetShape && shape.id === this.targetShape.id) { this.targetFound = true; } if(shape.visible && pos !== undefined && shape._isPointInShape(pos)) { // handle onmousedown if(!isDragging && this.mouseDown) { this.mouseDown = false; this.clickStart = true; shape._handleEvents('onmousedown', evt); return true; } // handle onmouseup & onclick else if(this.mouseUp) { this.mouseUp = false; shape._handleEvents('onmouseup', evt); // detect if click or double click occurred if(this.clickStart) { /* * if dragging and dropping, don't fire click or dbl click * event */ if((!go.drag.moving) || !go.drag.node) { shape._handleEvents('onclick', evt); if(shape.inDoubleClickWindow) { shape._handleEvents('ondblclick', evt); } shape.inDoubleClickWindow = true; setTimeout(function() { shape.inDoubleClickWindow = false; }, this.dblClickWindow); } } return true; } // handle touchstart else if(this.touchStart) { this.touchStart = false; shape._handleEvents('touchstart', evt); if(el.ondbltap && shape.inDoubleClickWindow) { var events = el.ondbltap; for(var i = 0; i < events.length; i++) { events[i].handler.apply(shape, [evt]); } } shape.inDoubleClickWindow = true; setTimeout(function() { shape.inDoubleClickWindow = false; }, this.dblClickWindow); return true; } // handle touchend else if(this.touchEnd) { this.touchEnd = false; shape._handleEvents('touchend', evt); return true; } /* * NOTE: these event handlers require target shape * handling */ // handle onmouseover else if(!isDragging && this._isNewTarget(shape, evt)) { /* * check to see if there are stored mouseout events first. * if there are, run those before running the onmouseover * events */ if(this.mouseoutShape) { this.mouseoverShape = shape; this.mouseoutShape._handleEvents('onmouseout', evt); this.mouseoverShape = undefined; } shape._handleEvents('onmouseover', evt); this._setTarget(shape); return true; } // handle mousemove and touchmove else if(!isDragging) { shape._handleEvents('onmousemove', evt); shape._handleEvents('touchmove', evt); return true; } } // handle mouseout condition else if(!isDragging && this.targetShape && this.targetShape.id === shape.id) { this._setTarget(undefined); this.mouseoutShape = shape; //shape._handleEvents('onmouseout', evt); return true; } return false; }, /** * set new target */ _setTarget: function(shape) { this.targetShape = shape; this.targetFound = true; }, /** * check if shape should be a new target */ _isNewTarget: function(shape, evt) { if(!this.targetShape || (!this.targetFound && shape.id !== this.targetShape.id)) { /* * check if old target has an onmouseout event listener */ if(this.targetShape) { var oldEl = this.targetShape.eventListeners; if(oldEl) { this.mouseoutShape = this.targetShape; //this.targetShape._handleEvents('onmouseout', evt); } } return true; } else { return false; } }, /** * traverse container children * @param {Container} obj */ _traverseChildren: function(obj, evt) { var children = obj.children; // propapgate backwards through children for(var i = children.length - 1; i >= 0; i--) { var child = children[i]; if(child.isListening) { if(child.className === 'Shape') { var exit = this._detectEvent(child, evt); if(exit) { return true; } } else { var exit = this._traverseChildren(child, evt); if(exit) { return true; } } } } return false; }, /** * handle incoming event * @param {Event} evt */ _handleStageEvent: function(evt) { var go = Kinetic.GlobalObject; if(!evt) { evt = window.event; } this._setMousePosition(evt); this._setTouchPosition(evt); this._clearDefaultLayers(); /* * loop through layers. If at any point an event * is triggered, n is set to -1 which will break out of the * three nested loops */ this.targetFound = false; var shapeDetected = false; for(var n = this.children.length - 1; n >= 0; n--) { var layer = this.children[n]; if(layer.visible && n >= 0 && layer.isListening) { if(this._traverseChildren(layer, evt)) { n = -1; shapeDetected = true; } } } /* * if no shape was detected and a mouseout shape has been stored, * then run the onmouseout event handlers */ if(!shapeDetected && this.mouseoutShape) { this.mouseoutShape._handleEvents('onmouseout', evt); this.mouseoutShape = undefined; } }, /** * clear default layers */ _clearDefaultLayers: function() { this.bufferLayer.clear(); this.pathLayer.clear(); }, /** * begin listening for events by adding event handlers * to the container */ _listen: function() { var that = this; // desktop events this.container.addEventListener('mousedown', function(evt) { that.mouseDown = true; that._handleStageEvent(evt); }, false); this.container.addEventListener('mousemove', function(evt) { that.mouseUp = false; that.mouseDown = false; that._handleStageEvent(evt); }, false); this.container.addEventListener('mouseup', function(evt) { that.mouseUp = true; that.mouseDown = false; that._handleStageEvent(evt); that.clickStart = false; }, false); this.container.addEventListener('mouseover', function(evt) { that._handleStageEvent(evt); }, false); this.container.addEventListener('mouseout', function(evt) { that.mousePos = undefined; }, false); // mobile events this.container.addEventListener('touchstart', function(evt) { evt.preventDefault(); that.touchStart = true; that._handleStageEvent(evt); }, false); this.container.addEventListener('touchmove', function(evt) { evt.preventDefault(); that._handleStageEvent(evt); }, false); this.container.addEventListener('touchend', function(evt) { evt.preventDefault(); that.touchEnd = true; that._handleStageEvent(evt); }, false); }, /** * set mouse positon for desktop apps * @param {Event} evt */ _setMousePosition: function(evt) { var mouseX = evt.offsetX || (evt.clientX - this._getContainerPosition().left + window.pageXOffset); var mouseY = evt.offsetY || (evt.clientY - this._getContainerPosition().top + window.pageYOffset); this.mousePos = { x: mouseX, y: mouseY }; }, /** * set touch position for mobile apps * @param {Event} evt */ _setTouchPosition: function(evt) { if(evt.touches !== undefined && evt.touches.length === 1) {// Only deal with // one finger var touch = evt.touches[0]; // Get the information for finger #1 var touchX = touch.clientX - this._getContainerPosition().left + window.pageXOffset; var touchY = touch.clientY - this._getContainerPosition().top + window.pageYOffset; this.touchPos = { x: touchX, y: touchY }; } }, /** * get container position */ _getContainerPosition: function() { var obj = this.container; var top = 0; var left = 0; while(obj && obj.tagName !== 'BODY') { top += obj.offsetTop - obj.scrollTop; left += obj.offsetLeft - obj.scrollLeft; obj = obj.offsetParent; } return { top: top, left: left }; }, /** * modify path context * @param {CanvasContext} context */ _modifyPathContext: function(context) { context.stroke = function() { }; context.fill = function() { }; context.fillRect = function(x, y, width, height) { layer.context.rect(x, y, width, height); }; context.strokeRect = function(x, y, width, height) { layer.context.rect(x, y, width, height); }; context.drawImage = function() { }; context.fillText = function() { }; context.strokeText = function() { }; }, /** * end drag and drop */ _endDrag: function(evt) { var go = Kinetic.GlobalObject; if(go.drag.node) { if(go.drag.moving) { go.drag.moving = false; go.drag.node._handleEvents('ondragend', evt); } } go.drag.node = undefined; }, /** * prepare drag and drop */ _prepareDrag: function() { var that = this; this.onContainer('mousemove touchmove', function(evt) { var go = Kinetic.GlobalObject; var node = go.drag.node; if(node) { var pos = that.getUserPosition(); var dc = node.dragConstraint; var db = node.dragBounds; // default var newNodePos = { x: pos.x - go.drag.offset.x, y: pos.y - go.drag.offset.y }; // bounds overrides if(db.left !== undefined && newNodePos.x < db.left) { newNodePos.x = db.left; } if(db.right !== undefined && newNodePos.x > db.right) { newNodePos.x = db.right; } if(db.top !== undefined && newNodePos.y < db.top) { newNodePos.y = db.top; } if(db.bottom !== undefined && newNodePos.y > db.bottom) { newNodePos.y = db.bottom; } // constraint overrides if(dc === 'horizontal') { newNodePos.y = node.y; } else if(dc === 'vertical') { newNodePos.x = node.x; } /* * save rotation and scale and then * remove them from the transform */ var rot = node.rotation; var scale = { x: node.scale.x, y: node.scale.y }; node.rotation = 0; node.scale = { x: 1, y: 1 }; // unravel transform var it = node.getAbsoluteTransform(); it.invert(); it.translate(newNodePos.x, newNodePos.y); node.x += it.getTranslation().x; node.y += it.getTranslation().y; // restore rotation and scale node.rotate(rot); node.scale = { x: scale.x, y: scale.y }; go.drag.node.getLayer().draw(); if(!go.drag.moving) { go.drag.moving = true; // execute dragstart events if defined go.drag.node._handleEvents('ondragstart', evt); } // execute user defined ondragmove if defined go.drag.node._handleEvents('ondragmove', evt); } }, false); this.onContainer('mouseup touchend mouseout', function(evt) { that._endDrag(evt); }); }, /** * build dom */ _buildDOM: function() { // content this.content.style.width = this.width + 'px'; this.content.style.height = this.height + 'px'; this.content.style.position = 'relative'; this.content.style.display = 'inline-block'; this.content.className = 'kineticjs-content'; this.container.appendChild(this.content); // default layers this.bufferLayer = new Kinetic.Layer(); this.pathLayer = new Kinetic.Layer(); // set parents this.bufferLayer.parent = this; this.pathLayer.parent = this; // customize back stage context this._modifyPathContext(this.pathLayer.context); // hide canvases this.bufferLayer.getCanvas().style.display = 'none'; this.pathLayer.getCanvas().style.display = 'none'; // add buffer layer this.bufferLayer.canvas.width = this.width; this.bufferLayer.canvas.height = this.height; this.bufferLayer.canvas.className = 'kineticjs-buffer-layer'; this.content.appendChild(this.bufferLayer.canvas); // add path layer this.pathLayer.canvas.width = this.width; this.pathLayer.canvas.height = this.height; this.pathLayer.canvas.className = 'kineticjs-path-layer'; this.content.appendChild(this.pathLayer.canvas); } }; // Extend Container and Node Kinetic.GlobalObject.extend(Kinetic.Stage, Kinetic.Container); Kinetic.GlobalObject.extend(Kinetic.Stage, Kinetic.Node); /////////////////////////////////////////////////////////////////////// // Layer /////////////////////////////////////////////////////////////////////// /** * Layer constructor. Layers are tied to their own canvas element and are used * to contain groups or shapes * @constructor * @augments Kinetic.Container * @augments Kinetic.Node * @param {Object} config */ Kinetic.Layer = function(config) { this.className = 'Layer'; this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext('2d'); this.canvas.style.position = 'absolute'; this.transitions = []; this.transitionIdCounter = 0; // call super constructors Kinetic.Container.apply(this, []); Kinetic.Node.apply(this, [config]); }; /* * Layer methods */ Kinetic.Layer.prototype = { /** * draw children nodes. this includes any groups * or shapes */ draw: function() { this._draw(); }, /** * clears the canvas context tied to the layer. Clearing * a layer does not remove its children. The nodes within * the layer will be redrawn whenever the .draw() method * is used again. */ clear: function() { var context = this.getContext(); var canvas = this.getCanvas(); context.clearRect(0, 0, canvas.width, canvas.height); }, /** * get layer canvas */ getCanvas: function() { return this.canvas; }, /** * get layer context */ getContext: function() { return this.context; }, /** * add a node to the layer. New nodes are always * placed at the top. * @param {Node} node */ add: function(child) { this._add(child); }, /** * remove a child from the layer * @param {Node} child */ remove: function(child) { this._remove(child); }, /** * private draw children */ _draw: function() { this.clear(); if(this.visible) { this._drawChildren(); } } }; // Extend Container and Node Kinetic.GlobalObject.extend(Kinetic.Layer, Kinetic.Container); Kinetic.GlobalObject.extend(Kinetic.Layer, Kinetic.Node); /////////////////////////////////////////////////////////////////////// // Group /////////////////////////////////////////////////////////////////////// /** * Group constructor. Groups are used to contain shapes or other groups. * @constructor * @augments Kinetic.Container * @augments Kinetic.Node * @param {Object} config */ Kinetic.Group = function(config) { this.className = 'Group'; // call super constructors Kinetic.Container.apply(this, []); Kinetic.Node.apply(this, [config]); }; /* * Group methods */ Kinetic.Group.prototype = { /** * add node to group * @param {Node} child */ add: function(child) { this._add(child); }, /** * remove a child node from the group * @param {Node} child */ remove: function(child) { this._remove(child); }, /** * draw children */ _draw: function() { if(this.visible) { this._drawChildren(); } } }; // Extend Container and Node Kinetic.GlobalObject.extend(Kinetic.Group, Kinetic.Container); Kinetic.GlobalObject.extend(Kinetic.Group, Kinetic.Node); /////////////////////////////////////////////////////////////////////// // Shape /////////////////////////////////////////////////////////////////////// /** * Shape constructor. Shapes are used to objectify drawing bits of a KineticJS * application * @constructor * @augments Kinetic.Node * @param {Object} config * @config {String|CanvasGradient|CanvasPattern} [fill] fill * @config {String} [stroke] stroke color * @config {Number} [strokeWidth] stroke width * @config {String} [lineJoin] line join. Can be "miter", "round", or "bevel". The default * is "miter" * @config {String} [detectionType] shape detection type. Can be "path" or "pixel". * The default is "path" because it performs better */ Kinetic.Shape = function(config) { this.className = 'Shape'; // defaults if(config.stroke !== undefined || config.strokeWidth !== undefined) { if(config.stroke === undefined) { config.stroke = 'black'; } else if(config.strokeWidth === undefined) { config.strokeWidth = 2; } } if(config.detectionType === undefined) { config.detectionType = 'path'; } // required this.drawFunc = config.drawFunc; // call super constructor Kinetic.Node.apply(this, [config]); }; /* * Shape methods */ Kinetic.Shape.prototype = { /** * get layer context where the shape is being drawn. When * the shape is being rendered, .getContext() returns the context of the * user created layer that contains the shape. When the event detection * engine is determining whether or not an event has occured on that shape, * .getContext() returns the context of the invisible path layer. */ getContext: function() { return this.tempLayer.getContext(); }, /** * get shape temp layer canvas */ getCanvas: function() { return this.tempLayer.getCanvas(); }, /** * helper method to fill and stroke a shape * based on its fill, stroke, and strokeWidth, properties */ fillStroke: function() { var context = this.getContext(); if(this.fill !== undefined) { context.fillStyle = this.fill; context.fill(); } if(this.stroke !== undefined) { context.lineWidth = this.strokeWidth === undefined ? 1 : this.strokeWidth; context.strokeStyle = this.stroke; context.stroke(); } }, /** * helper method to set the line join of a shape * based on the lineJoin property */ applyLineJoin: function() { var context = this.getContext(); if(this.lineJoin !== undefined) { context.lineJoin = this.lineJoin; } }, /** * set fill which can be a color, gradient object, * or pattern object * @param {String|CanvasGradient|CanvasPattern} fill */ setFill: function(fill) { this.fill = fill; }, /** * get fill */ getFill: function() { return this.fill; }, /** * set stroke color * @param {String} stroke */ setStroke: function(stroke) { this.stroke = stroke; }, /** * get stroke color */ getStroke: function() { return this.stroke; }, /** * set line join * @param {String} lineJoin. Can be "miter", "round", or "bevel". The * default is "miter" */ setLineJoin: function(lineJoin) { this.lineJoin = lineJoin; }, /** * get line join */ getLineJoin: function() { return this.lineJoin; }, /** * set stroke width * @param {Number} strokeWidth */ setStrokeWidth: function(strokeWidth) { this.strokeWidth = strokeWidth; }, /** * get stroke width */ getStrokeWidth: function() { return this.strokeWidth; }, /** * set draw function * @param {Function} func drawing function */ setDrawFunc: function(func) { this.drawFunc = func; }, /** * draw shape * @param {Layer} layer Layer that the shape will be drawn on */ _draw: function(layer) { if(this.visible) { var stage = layer.getStage(); var context = layer.getContext(); var family = []; var parent = this.parent; family.unshift(this); while(parent) { family.unshift(parent); parent = parent.parent; } context.save(); for(var n = 0; n < family.length; n++) { var node = family[n]; var m = node.getTransform().getMatrix(); context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); if(node.getAbsoluteAlpha() !== 1) { context.globalAlpha = node.getAbsoluteAlpha(); } } this.tempLayer = layer; this.drawFunc.call(this); context.restore(); } }, /** * custom isPointInPath method which can use path detection * or pixel detection */ _isPointInShape: function(pos) { var stage = this.getStage(); if(this.detectionType === 'path') { var pathLayer = stage.pathLayer; var pathLayerContext = pathLayer.getContext(); this._draw(pathLayer); return pathLayerContext.isPointInPath(pos.x, pos.y); } else { var bufferLayer = stage.bufferLayer; var bufferLayerContext = bufferLayer.getContext(); this._draw(bufferLayer); var w = stage.width; var h = stage.height; var x = pos.x; var y = pos.y; var imageData = bufferLayerContext.getImageData(0, 0, w, h); var data = imageData.data; var alpha = data[((w * y) + x) * 4 + 3]; return (alpha !== undefined && alpha !== 0); } } }; // extend Node Kinetic.GlobalObject.extend(Kinetic.Shape, Kinetic.Node); /////////////////////////////////////////////////////////////////////// // Rect /////////////////////////////////////////////////////////////////////// /** * Rect constructor * @constructor * @augments Kinetic.Shape * @param {Object} config */ Kinetic.Rect = function(config) { config.drawFunc = function() { var context = this.getContext(); context.beginPath(); this.applyLineJoin(); context.rect(0, 0, this.width, this.height); context.closePath(); this.fillStroke(); }; // call super constructor Kinetic.Shape.apply(this, [config]); }; /* * Rect methods */ Kinetic.Rect.prototype = { /** * set width * @param {Number} width */ setWidth: function(width) { this.width = width; }, /** * get width */ getWidth: function() { return this.width; }, /** * set height * @param {Number} height */ setHeight: function(height) { this.height = height; }, /** * get height */ getHeight: function() { return this.height; }, /** * set width and height * @param {Number} width * @param {Number} height */ setSize: function(width, height) { this.width = width; this.height = height; }, /** * return rect size */ getSize: function() { return { width: this.width, height: this.height }; } }; // extend Shape Kinetic.GlobalObject.extend(Kinetic.Rect, Kinetic.Shape); /////////////////////////////////////////////////////////////////////// // Circle /////////////////////////////////////////////////////////////////////// /** * Circle constructor * @constructor * @augments Kinetic.Shape * @param {Object} config */ Kinetic.Circle = function(config) { config.drawFunc = function() { var canvas = this.getCanvas(); var context = this.getContext(); context.beginPath(); this.applyLineJoin(); context.arc(0, 0, this.radius, 0, Math.PI * 2, true); context.closePath(); this.fillStroke(); }; // call super constructor Kinetic.Shape.apply(this, [config]); }; /* * Circle methods */ Kinetic.Circle.prototype = { /** * set radius * @param {Number} radius */ setRadius: function(radius) { this.radius = radius; }, /** * get radius */ getRadius: function() { return this.radius; } }; // extend Shape Kinetic.GlobalObject.extend(Kinetic.Circle, Kinetic.Shape); /////////////////////////////////////////////////////////////////////// // Image /////////////////////////////////////////////////////////////////////// /** * Image constructor * @constructor * @augments Kinetic.Shape * @param {Object} config */ Kinetic.Image = function(config) { // defaults if(config.width === undefined) { config.width = config.image.width; } if(config.height === undefined) { config.height = config.image.height; } config.drawFunc = function() { var canvas = this.getCanvas(); var context = this.getContext(); context.beginPath(); this.applyLineJoin(); context.rect(0, 0, this.width, this.height); context.closePath(); this.fillStroke(); context.drawImage(this.image, 0, 0, this.width, this.height); }; // call super constructor Kinetic.Shape.apply(this, [config]); }; /* * Image methods */ Kinetic.Image.prototype = { /** * set image * @param {ImageObject} image */ setImage: function(image) { this.image = image; }, /** * get image */ getImage: function() { return this.image; }, /** * set width * @param {Number} width */ setWidth: function(width) { this.width = width; }, /** * get width */ getWidth: function() { return this.width; }, /** * set height * @param {Number} height */ setHeight: function(height) { this.height = height; }, /** * get height */ getHeight: function() { return this.height; }, /** * set width and height * @param {Number} width * @param {Number} height */ setSize: function(width, height) { this.width = width; this.height = height; }, /** * return image size */ getSize: function() { return { width: this.width, height: this.height }; } }; // extend Shape Kinetic.GlobalObject.extend(Kinetic.Image, Kinetic.Shape); /////////////////////////////////////////////////////////////////////// // Polygon /////////////////////////////////////////////////////////////////////// /** * Polygon constructor.  Polygons are defined by an array of points * @constructor * @augments Kinetic.Shape * @param {Object} config */ Kinetic.Polygon = function(config) { config.drawFunc = function() { var context = this.getContext(); context.beginPath(); this.applyLineJoin(); context.moveTo(this.points[0].x, this.points[0].y); for(var n = 1; n < this.points.length; n++) { context.lineTo(this.points[n].x, this.points[n].y); } context.closePath(); this.fillStroke(); }; // call super constructor Kinetic.Shape.apply(this, [config]); }; /* * Polygon methods */ Kinetic.Polygon.prototype = { /** * set points array * @param {Array} points */ setPoints: function(points) { this.points = points; }, /** * get points array */ getPoints: function() { return this.points; } }; // extend Shape Kinetic.GlobalObject.extend(Kinetic.Polygon, Kinetic.Shape); /////////////////////////////////////////////////////////////////////// // RegularPolygon /////////////////////////////////////////////////////////////////////// /** * RegularPolygon constructor.  Examples include triangles, squares, pentagons, hexagons, etc. * @constructor * @augments Kinetic.Shape * @param {Object} config */ Kinetic.RegularPolygon = function(config) { config.drawFunc = function() { var context = this.getContext(); context.beginPath(); this.applyLineJoin(); context.moveTo(0, 0 - this.radius); for(var n = 1; n < this.sides; n++) { var x = this.radius * Math.sin(n * 2 * Math.PI / this.sides); var y = -1 * this.radius * Math.cos(n * 2 * Math.PI / this.sides); context.lineTo(x, y); } context.closePath(); this.fillStroke(); }; // call super constructor Kinetic.Shape.apply(this, [config]); }; /* * RegularPolygon methods */ Kinetic.RegularPolygon.prototype = { /** * set number of points * @param {int} points */ setPoints: function(points) { this.points = points; }, /** * get number of points */ getPoints: function() { return this.points; }, /** * set radius * @param {Number} radius */ setRadius: function(radius) { this.radius = radius; }, /** * get radius */ getRadius: function() { return this.radius; }, /** * set number of sides * @param {int} sides */ setSides: function(sides) { this.sides = sides; }, /** * get number of sides */ getSides: function() { return this.sides; } }; // extend Shape Kinetic.GlobalObject.extend(Kinetic.RegularPolygon, Kinetic.Shape); /////////////////////////////////////////////////////////////////////// // Star /////////////////////////////////////////////////////////////////////// /** * Star constructor * @constructor * @augments Kinetic.Shape * @param {Object} config */ Kinetic.Star = function(config) { config.drawFunc = function() { var context = this.getContext(); context.beginPath(); this.applyLineJoin(); context.moveTo(0, 0 - this.outerRadius); for(var n = 1; n < this.points * 2; n++) { var radius = n % 2 === 0 ? this.outerRadius : this.innerRadius; var x = radius * Math.sin(n * Math.PI / this.points); var y = -1 * radius * Math.cos(n * Math.PI / this.points); context.lineTo(x, y); } context.closePath(); this.fillStroke(); }; // call super constructor Kinetic.Shape.apply(this, [config]); }; /* * Star methods */ Kinetic.Star.prototype = { /** * set points array * @param {Array} points */ setPoints: function(points) { this.points = points; }, /** * get points array */ getPoints: function() { return this.points; }, /** * set outer radius * @param {Number} radius */ setOuterRadius: function(radius) { this.outerRadius = radius; }, /** * get outer radius */ getOuterRadius: function() { return this.outerRadius; }, /** * set inner radius * @param {Number} radius */ setInnerRadius: function(radius) { this.innerRadius = radius; }, /** * get inner radius */ getInnerRadius: function() { return this.innerRadius; } }; // extend Shape Kinetic.GlobalObject.extend(Kinetic.Star, Kinetic.Shape); /////////////////////////////////////////////////////////////////////// // Text /////////////////////////////////////////////////////////////////////// /** * Text constructor * @constructor * @augments Kinetic.Shape * @param {Object} config */ Kinetic.Text = function(config) { /* * defaults */ if(config.textStroke !== undefined || config.textStrokeWidth !== undefined) { if(config.textStroke === undefined) { config.textStroke = 'black'; } else if(config.textStrokeWidth === undefined) { config.textStrokeWidth = 2; } } if(config.align === undefined) { config.align = 'left'; } if(config.verticalAlign === undefined) { config.verticalAlign = 'top'; } if(config.padding === undefined) { config.padding = 0; } if(config.fontStyle === undefined) { config.fontStyle = 'normal'; } config.drawFunc = function() { var context = this.getContext(); context.font = this.fontStyle + ' ' + this.fontSize + 'pt ' + this.fontFamily; context.textBaseline = 'middle'; var textHeight = this.getTextHeight(); var textWidth = this.getTextWidth(); var p = this.padding; var x = 0; var y = 0; switch (this.align) { case 'center': x = textWidth / -2 - p; break; case 'right': x = -1 * textWidth - p; break; } switch (this.verticalAlign) { case 'middle': y = textHeight / -2 - p; break; case 'bottom': y = -1 * textHeight - p; break; } // draw path context.save(); context.beginPath(); this.applyLineJoin(); context.rect(x, y, textWidth + p * 2, textHeight + p * 2); context.closePath(); this.fillStroke(); context.restore(); var tx = p + x; var ty = textHeight / 2 + p + y; // draw text if(this.textFill !== undefined) { context.fillStyle = this.textFill; context.fillText(this.text, tx, ty); } if(this.textStroke !== undefined || this.textStrokeWidth !== undefined) { // defaults if(this.textStroke === undefined) { this.textStroke = 'black'; } else if(this.textStrokeWidth === undefined) { this.textStrokeWidth = 2; } context.lineWidth = this.textStrokeWidth; context.strokeStyle = this.textStroke; context.strokeText(this.text, tx, ty); } }; // call super constructor Kinetic.Shape.apply(this, [config]); }; /* * Text methods */ Kinetic.Text.prototype = { /** * set font family * @param {String} fontFamily */ setFontFamily: function(fontFamily) { this.fontFamily = fontFamily; }, /** * get font family */ getFontFamily: function() { return this.fontFamily; }, /** * set font size * @param {int} fontSize */ setFontSize: function(fontSize) { this.fontSize = fontSize; }, /** * get font size */ getFontSize: function() { return this.fontSize; }, /** * set font style. Can be "normal", "italic", or "bold". "normal" is the default. * @param {String} fontStyle */ setFontStyle: function(fontStyle) { this.fontStyle = fontStyle; }, /** * get font style */ getFontStyle: function() { return this.fontStyle; }, /** * set text fill color * @param {String} textFill */ setTextFill: function(textFill) { this.textFill = textFill; }, /** * get text fill color */ getTextFill: function() { return this.textFill; }, /** * set text stroke color * @param {String} textStroke */ setTextStroke: function(textStroke) { this.textStroke = textStroke; }, /** * get text stroke color */ getTextStroke: function() { return this.textStroke; }, /** * set text stroke width * @param {int} textStrokeWidth */ setTextStrokeWidth: function(textStrokeWidth) { this.textStrokeWidth = textStrokeWidth; }, /** * get text stroke width */ getTextStrokeWidth: function() { return this.textStrokeWidth; }, /** * set padding * @param {int} padding */ setPadding: function(padding) { this.padding = padding; }, /** * get padding */ getPadding: function() { return this.padding; }, /** * set horizontal align of text * @param {String} align align can be 'left', 'center', or 'right' */ setAlign: function(align) { this.align = align; }, /** * get horizontal align */ getAlign: function() { return this.align; }, /** * set vertical align of text * @param {String} verticalAlign verticalAlign can be "top", "middle", or "bottom" */ setVerticalAlign: function(verticalAlign) { this.verticalAlign = verticalAlign; }, /** * get vertical align */ getVerticalAlign: function() { return this.verticalAlign; }, /** * set text * @param {String} text */ setText: function(text) { this.text = text; }, /** * get text */ getText: function() { return this.text; }, /** * get text width in pixels */ getTextWidth: function() { return this.getTextSize().width; }, /** * get text height in pixels */ getTextHeight: function() { return this.getTextSize().height; }, /** * get text size in pixels */ getTextSize: function() { var context = this.getContext(); context.save(); context.font = this.fontStyle + ' ' + this.fontSize + 'pt ' + this.fontFamily; var metrics = context.measureText(this.text); context.restore(); return { width: metrics.width, height: parseInt(this.fontSize, 10) }; } }; // extend Shape Kinetic.GlobalObject.extend(Kinetic.Text, Kinetic.Shape); /* * Last updated November 2011 * By Simon Sarris * www.simonsarris.com * sarris@acm.org * * Free to use and distribute at will * So long as you are nice to people, etc */ /* * The usage of this class was inspired by some of the work done by a forked * project, KineticJS-Ext by Wappworks, which is based on Simon's Transform * class. */ /** * Matrix object */ Kinetic.Transform = function() { this.m = [1, 0, 0, 1, 0, 0]; } Kinetic.Transform.prototype = { /** * Apply translation * @param {Number} x * @param {Number} y */ translate: function(x, y) { this.m[4] += this.m[0] * x + this.m[2] * y; this.m[5] += this.m[1] * x + this.m[3] * y; }, /** * Apply scale * @param {Number} sx * @param {Number} sy */ scale: function(sx, sy) { this.m[0] *= sx; this.m[1] *= sx; this.m[2] *= sy; this.m[3] *= sy; }, /** * Apply rotation * @param {Number} rad Angle in radians */ rotate: function(rad) { var c = Math.cos(rad); var s = Math.sin(rad); var m11 = this.m[0] * c + this.m[2] * s; var m12 = this.m[1] * c + this.m[3] * s; var m21 = this.m[0] * -s + this.m[2] * c; var m22 = this.m[1] * -s + this.m[3] * c; this.m[0] = m11; this.m[1] = m12; this.m[2] = m21; this.m[3] = m22; }, /** * Returns the translation * @returns {Object} 2D point(x, y) */ getTranslation: function() { return { x: this.m[4], y: this.m[5] }; }, /** * Transform multiplication * @param {Kinetic.Transform} matrix */ multiply: function(matrix) { var m11 = this.m[0] * matrix.m[0] + this.m[2] * matrix.m[1]; var m12 = this.m[1] * matrix.m[0] + this.m[3] * matrix.m[1]; var m21 = this.m[0] * matrix.m[2] + this.m[2] * matrix.m[3]; var m22 = this.m[1] * matrix.m[2] + this.m[3] * matrix.m[3]; var dx = this.m[0] * matrix.m[4] + this.m[2] * matrix.m[5] + this.m[4]; var dy = this.m[1] * matrix.m[4] + this.m[3] * matrix.m[5] + this.m[5]; this.m[0] = m11; this.m[1] = m12; this.m[2] = m21; this.m[3] = m22; this.m[4] = dx; this.m[5] = dy; }, /** * Invert the matrix */ invert: function() { var d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]); var m0 = this.m[3] * d; var m1 = -this.m[1] * d; var m2 = -this.m[2] * d; var m3 = this.m[0] * d; var m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]); var m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]); this.m[0] = m0; this.m[1] = m1; this.m[2] = m2; this.m[3] = m3; this.m[4] = m4; this.m[5] = m5; }, /** * return matrix */ getMatrix: function() { return this.m; } };