twojs-browserify
Version:
A browserified two-dimensional drawing api meant for modern browsers.
2,163 lines (1,563 loc) • 97.6 kB
JavaScript
/**
* two.js
* a two-dimensional drawing api meant for modern browsers. It is renderer
* agnostic enabling the same api for rendering in multiple contexts: webgl,
* canvas2d, and svg.
*
* Copyright (c) 2012 - 2013 jonobr1 / http://jonobr1.com
*
* 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.
*
*/
/**
* Code for running this in Browserify
*/
var _ = require('underscore');
var requestAnimationFrame = require('./requestAnimationFrame.js');
var Backbone = {};
Backbone.Events = require('./events.js');
(function() {
var root = this;
var previousTwo = root.Two || {};
/**
* Constants
*/
var sin = Math.sin,
cos = Math.cos,
atan2 = Math.atan2,
sqrt = Math.sqrt,
round = Math.round,
abs = Math.abs,
PI = Math.PI,
TWO_PI = PI * 2,
HALF_PI = PI / 2,
pow = Math.pow;
/**
* Cross browser dom events.
*/
var dom = {
hasEventListeners: _.isFunction(root.addEventListener),
bind: function(elem, event, func, bool) {
if (this.hasEventListeners) {
elem.addEventListener(event, func, !!bool);
} else {
elem.attachEvent('on' + event, func);
}
return this;
},
unbind: function(elem, event, func, bool) {
if (this.hasEventListeners) {
elem.removeEventListeners(event, func, !!bool);
} else {
elem.detachEvent('on' + event, func);
}
return this;
}
};
/**
* @class
*/
var Two = root.Two = function(options) {
// Determine what Renderer to use and setup a scene.
var params = _.defaults(options || {}, {
fullscreen: false,
width: 640,
height: 480,
type: Two.Types.svg,
autostart: false
});
this.type = params.type;
this.renderer = new Two[this.type](this);
Two.Utils.setPlaying.call(this, params.autostart);
this.frameCount = 0;
if (params.fullscreen) {
var fitted = _.bind(fitToWindow, this);
_.extend(document.body.style, {
overflow: 'hidden',
margin: 0,
padding: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
position: 'fixed'
});
_.extend(this.renderer.domElement.style, {
display: 'block',
top: 0,
left: 0,
right: 0,
bottom: 0,
position: 'fixed'
});
dom.bind(root, 'resize', fitted);
fitted();
} else {
this.renderer.setSize(params.width, params.height);
this.width = params.width;
this.height = params.height;
}
this.scene = new Two.Group();
this.renderer.add(this.scene);
Two.Instances.push(this);
};
_.extend(Two, {
/**
* Primitive
*/
Array: root.Float32Array || Array,
Types: {
webgl: 'WebGLRenderer',
svg: 'SVGRenderer',
canvas: 'CanvasRenderer'
},
Version: 'v0.2.1',
Properties: {
hierarchy: 'hierarchy',
demotion: 'demotion'
},
Events: {
play: 'play',
pause: 'pause',
update: 'update',
render: 'render',
resize: 'resize',
change: 'change',
remove: 'remove',
insert: 'insert'
},
Resolution: 8,
Instances: [],
noConflict: function() {
root.Two = previousTwo;
return this;
},
Utils: {
Curve: {
CollinearityEpsilon: pow(10, -30),
RecursionLimit: 16,
CuspLimit: 0,
Tolerance: {
distance: 0.25,
angle: 0,
epsilon: 0.01
}
},
/**
* Properly defer play calling until after all objects
* have been updated with their newest styles.
*/
setPlaying: function(b) {
_.defer(_.bind(function() {
this.playing = !!b;
}, this));
},
/**
* Return the computed matrix of a nested object.
*/
getComputedMatrix: function(object) {
var matrix = new Two.Matrix();
var parent = object;
while (parent && parent._matrix) {
var e = parent._matrix.elements;
matrix.multiply(
e[0], e[1], e[2], e[3], e[4], e[5], e[6], e[7], e[8], e[9]);
parent = parent.parent;
}
return matrix;
},
applySvgAttributes: function(node, elem) {
elem.cap = 'butt';
elem.join = 'bevel';
_.each(node.attributes, function(v, k) {
var property = v.nodeName;
switch (property) {
case 'transform':
// Need to figure out how to decompose matrix into
// translation, rotation, scale.
// var transforms = node[k].baseVal;
// var matrix = new Two.Matrix();
// _.each(_.range(transforms.numberOfItems), function(i) {
// var m = transforms.getItem(i).matrix;
// matrix.multiply(m.a, m.b, m.c, m.d, m.e, m.f);
// });
// elem.setMatrix(matrix);
break;
case 'visibility':
elem.visible = !!v.nodeValue;
break;
case 'stroke-linecap':
elem.cap = v.nodeValue;
break;
case 'stroke-linejoin':
elem.join = v.nodeValue;
break;
case 'stroke-miterlimit':
elem.miter = v.nodeValue;
break;
case 'stroke-width':
elem.linewidth = parseFloat(v.nodeValue);
break;
case 'stroke-opacity':
case 'fill-opacity':
elem.opacity = v.nodeValue;
break;
case 'fill':
elem.fill = v.nodeValue;
break;
case 'stroke':
elem.stroke = v.nodeValue;
break;
}
});
return elem;
},
/**
* Read any number of SVG node types and create Two equivalents of them.
*/
read: {
svg: function() {
return Two.Utils.read.g.apply(this, arguments);
},
g: function(node) {
var group = new Two.Group();
this.add(group);
_.each(node.childNodes, function(n) {
if (!n.localName) {
return;
}
var tag = n.localName.toLowerCase();
if ((tag in Two.Utils.read)) {
var o = Two.Utils.read[tag].call(this, n);
group.add(o);
}
}, this);
return Two.Utils.applySvgAttributes(node, group);
},
polygon: function(node, open) {
var points = node.points;
if (!points) {
return;
}
var verts = _.map(_.range(points.numberOfItems), function(i) {
var p = points.getItem(i);
return new Two.Vector(p.x, p.y);
});
var poly = new Two.Polygon(verts, !open).noStroke();
return Two.Utils.applySvgAttributes(node, poly);
},
polyline: function(node) {
return Two.Utils.read.polygon(node, true);
},
path: function(node) {
var data = node.getAttribute('d');
// Retrieve an array of all commands.
var paths = _.flatten(_.map(_.compact(data.split(/M/g)), function(str) {
var rels = _.map(_.compact(str.split(/m/g)), function(str, i) {
if (i <= 0) {
return str;
}
return 'm' + str;
});
rels[0] = 'M' + rels[0];
return rels;
}));
// Create Two.Polygons from the paths.
var length = paths.length;
var coord = new Two.Vector();
var control = new Two.Vector();
var polys = _.map(paths, function(path) {
var coords, relative = false;
var closed = false;
var points = _.flatten(_.map(path.match(/[a-z][^a-z]*/ig), function(command) {
var result, x, y;
var type = command[0];
var lower = type.toLowerCase();
coords = command.slice(1).trim().split(/[\s,]+|(?=[+\-])/);
relative = type === lower;
var x1, y1, x2, y2, x3, y3, x4, y4, reflection;
switch(lower) {
case 'z':
closed = true;
break;
case 'm':
case 'l':
x = parseFloat(coords[0]);
y = parseFloat(coords[1]);
result = new Two.Vector(x, y);
if (relative) {
result.addSelf(coord);
}
coord.copy(result);
break;
case 'h':
case 'v':
var a = lower === 'h' ? 'x' : 'y';
var b = a === 'x' ? 'y' : 'x';
result = new Two.Vector();
result[a] = parseFloat(coords[0]);
result[b] = coord[b];
if (relative) {
result[a] += coord[a];
}
coord.copy(result);
break;
case 's':
case 'c':
x1 = coord.x, y1 = coord.y;
if (lower === 'c') {
x2 = parseFloat(coords[0]), y2 = parseFloat(coords[1]);
x3 = parseFloat(coords[2]), y3 = parseFloat(coords[3]);
x4 = parseFloat(coords[4]), y4 = parseFloat(coords[5]);
} else {
// Calculate reflection control point for proper x2, y2
// inclusion.
reflection = Two.Utils.getReflection(coord, control, relative);
x2 = reflection.x, y2 = reflection.y;
x3 = parseFloat(coords[0]), y3 = parseFloat(coords[1]);
x4 = parseFloat(coords[2]), y4 = parseFloat(coords[3]);
}
if (relative) {
x2 += x1, y2 += y1;
x3 += x1, y3 += y1;
x4 += x1, y4 += y1;
}
result = Two.Utils.subdivide(x1, y1, x2, y2, x3, y3, x4, y4);
coord.set(x4, y4);
control.set(x3, y3);
var last = result[result.length - 1];
// x4 y4 is not present in the curve, add it.
if (last && !last.equals(coord)) {
result.push(coord.clone());
}
break;
case 't':
case 'q':
x1 = coord.x, y1 = coord.y;
if (control.isZero()) {
x2 = x1, y2 = y1;
} else {
x2 = control.x, y1 = control.y;
}
if (lower === 'q') {
x3 = parseFloat(coords[0]), y3 = parseFloat(coords[1]);
x4 = parseFloat(coords[1]), y4 = parseFloat(coords[2]);
} else {
reflection = Two.Utils.getReflection(coord, control, relative);
x3 = reflection.x, y3 = reflection.y;
x4 = parseFloat(coords[0]), y4 = parseFloat(coords[1]);
}
if (relative) {
x2 += x1, y2 += y1;
x3 += x1, y3 += y1;
x4 += x1, y4 += y1;
}
result = Two.Utils.subdivide(x1, y1, x2, y2, x3, y3, x4, y4);
coord.set(x4, y4);
control.set(x3, y3);
var last = result[result.length - 1];
// x4 y4 is not present in the curve, add it.
if (!last.equals(coord)) {
result.push(coord.clone());
}
break;
case 'a':
throw new Two.Utils.Error('not yet able to interpret Elliptical Arcs.');
}
return result;
}));
if (points.length <= 1) {
return;
}
points = _.compact(points);
var poly = new Two.Polygon(points, closed).noStroke();
return Two.Utils.applySvgAttributes(node, poly);
});
return _.compact(polys);
},
circle: function(node) {
var x = parseFloat(node.getAttribute('cx'));
var y = parseFloat(node.getAttribute('cy'));
var r = parseFloat(node.getAttribute('r'));
var amount = Two.Resolution;
var points = _.map(_.range(amount), function(i) {
var pct = i / amount;
var theta = pct * TWO_PI;
var x = r * cos(theta);
var y = r * sin(theta);
return new Two.Vector(x, y);
}, this);
var circle = new Two.Polygon(points, true, true).noStroke();
circle.translation.set(x, y);
return Two.Utils.applySvgAttributes(node, circle);
},
ellipse: function(node) {
var x = parseFloat(node.getAttribute('cx'));
var y = parseFloat(node.getAttribute('cy'));
var width = parseFloat(node.getAttribute('rx'));
var height = parseFloat(node.getAttribute('ry'));
var amount = Two.Resolution;
var points = _.map(_.range(amount), function(i) {
var pct = i / amount;
var theta = pct * TWO_PI;
var x = width * cos(theta);
var y = height * sin(theta);
return new Two.Vector(x, y);
}, this);
var ellipse = new Two.Polygon(points, true, true).noStroke();
ellipse.translation.set(x, y);
return Two.Utils.applySvgAttributes(node, ellipse);
},
rect: function(node) {
var x = parseFloat(node.getAttribute('x'));
var y = parseFloat(node.getAttribute('y'));
var width = parseFloat(node.getAttribute('width'));
var height = parseFloat(node.getAttribute('height'));
var w2 = width / 2;
var h2 = height / 2;
var points = [
new Two.Vector(w2, h2),
new Two.Vector(-w2, h2),
new Two.Vector(-w2, -h2),
new Two.Vector(w2, -h2)
];
var rect = new Two.Polygon(points, true).noStroke();
rect.translation.set(x + w2, y + h2);
return Two.Utils.applySvgAttributes(node, rect);
},
line: function(node) {
var x1 = parseFloat(node.getAttribute('x1'));
var y1 = parseFloat(node.getAttribute('y1'));
var x2 = parseFloat(node.getAttribute('x2'));
var y2 = parseFloat(node.getAttribute('y2'));
var width = x2 - x1;
var height = y2 - y1;
var w2 = width / 2;
var h2 = height / 2;
var points = [
new Two.Vector(- w2, - h2),
new Two.Vector(w2, h2)
];
// Center line and translate to desired position.
var line = new Two.Polygon(points).noFill();
line.translation.set(x1 + w2, y1 + h2);
return Two.Utils.applySvgAttributes(node, line);
}
},
/**
* Given 2 points (a, b) and corresponding control point for each
* return an array of points that represent an Adaptive Subdivision
* of Bezier Curves. Founded in the online article:
*
* http://www.antigrain.com/research/adaptive_bezier/index.html
*
* Where level represents how many levels deep the function has
* already recursed.
*
*/
subdivide: function(x1, y1, x2, y2, x3, y3, x4, y4, level) {
// Constants
var epsilon = Two.Utils.Curve.CollinearityEpsilon,
limit = Two.Utils.Curve.RecursionLimit,
cuspLimit = Two.Utils.Curve.CuspLimit,
tolerance = Two.Utils.Curve.Tolerance,
da1, da2;
level = level || 0;
if (level > limit) {
return [];
}
var x12 = (x1 + x2) / 2,
y12 = (y1 + y2) / 2,
x23 = (x2 + x3) / 2,
y23 = (y2 + y3) / 2,
x34 = (x3 + x4) / 2,
y34 = (y3 + y4) / 2,
x123 = (x12 + x23) / 2,
y123 = (y12 + y23) / 2,
x234 = (x23 + x34) / 2,
y234 = (y23 + y34) / 2,
x1234 = (x123 + x234) / 2,
y1234 = (y123 + y234) / 2;
// Try to approximate the full cubic curve by a single straight line.
var dx = x4 - x1;
var dy = y4 - y1;
var d2 = abs(((x2 - x4) * dy - (y2 - y4) * dx));
var d3 = abs(((x3 - x4) * dy - (y3 - y4) * dx));
if (level > 0) {
if (d2 > epsilon && d3 > epsilon) {
if ((d2 + d3) * (d2 + d3) <= tolerance.distance * (dx * dx + dy * dy)) {
if (tolerance.angle < tolerance.epsilon) {
return [new Two.Vector(x1234, y1234)];
}
var a23 = atan2(y3 - y2, x3 - x2);
da1 = abs(a23 - atan2(y2 - y1, x2 - x1));
da2 = abs(atan2(y4 - y3, x4 - x3) - a23);
if (da1 >= PI) da1 = TWO_PI - da1;
if (da2 >= PI) da2 = TWO_PI - da2;
if (da1 + da2 < tolerance.angle) {
return [new Two.Vector(x1234, y1234)];
}
if (cuspLimit !== 0) {
if (da1 > cuspLimit) {
return [new Two.Vector(x2, y2)];
}
if (da2 > cuspLimit) {
return [new Two.Vector(x3, y3)];
}
}
}
}
} else {
if (d2 > epsilon) {
if (d2 * d2 <= tolerance.distance * (dx * dx + dy * dy)) {
if (tolerance.angle < tolerance.epsilon) {
return [new Two.Vector(x1234, y1234)];
}
da1 = abs(atan2(y3 - y2, x3 - x2) - atan2(y2 - y1, x2 - x1));
if (da1 >= PI) da1 = TWO_PI - da1;
if (da1 < tolerance.angle) {
return [
new Two.Vector(x2, y2),
new Two.Vector(x3, y3)
];
}
if (cuspLimit !== 0) {
if (da1 > cuspLimit) {
return [new Two.Vector(x2, y2)];
}
}
} else if (d3 > epsilon) {
if (d3 * d3 <= tolerance.distance * (dx * dx + dy * dy)) {
if (tolerance.angle < tolerance.epsilon) {
return [new Two.Vector(x1234, y1234)];
}
da1 = abs(atan2(y4 - y3, x4 - x3) - atan2(y3 - y2, x3 - x2));
if (da1 >= PI) da1 = TWO_PI - da1;
if (da1 < tolerance.angle) {
return [
new Two.Vector(x2, y2),
new Two.Vector(x3, y3)
];
}
if (cuspLimit !== 0) {
if (da1 > cuspLimit) {
return [new Two.Vector2(x3, y3)];
}
}
}
} else {
dx = x1234 - (x1 + x4) / 2;
dy = y1234 - (y1 + y4) / 2;
if (dx * dx + dy * dy <= tolerance.distance) {
return [new Two.Vector(x1234, y1234)];
}
}
}
}
return Two.Utils.subdivide(
x1, y1, x12, y12, x123, y123, x1234, y1234, level + 1
).concat(Two.Utils.subdivide(
x1234, y1234, x234, y234, x34, y34, x4, y4, level + 1
));
},
/**
* Creates a set of points that have u, v values for anchor positions
*/
getCurveFromPoints: function(points, closed) {
var curve = [], l = points.length, last = l - 1;
for (var i = 0; i < l; i++) {
var p = points[i];
var point = { x: p.x, y: p.y };
curve.push(point);
var prev = closed ? mod(i - 1, l) : Math.max(i - 1, 0);
var next = closed ? mod(i + 1, l) : Math.min(i + 1, last);
var a = points[prev];
var b = point;
var c = points[next];
getControlPoints(a, b, c);
if (!b.u.x && !b.u.y) {
b.u.x = b.x;
b.u.y = b.y;
}
if (!b.v.x && !b.v.y) {
b.v.x = b.x;
b.v.y = b.y;
}
}
return curve;
},
/**
* Given three coordinates return the control points for the middle, b,
* vertex.
*/
getControlPoints: function(a, b, c) {
var a1 = angleBetween(a, b);
var a2 = angleBetween(c, b);
var d1 = distanceBetween(a, b);
var d2 = distanceBetween(c, b);
var mid = (a1 + a2) / 2;
// So we know which angle corresponds to which side.
var u, v;
if (d1 < 0.0001 || d2 < 0.0001) {
b.u = { x: b.x, y: b.y };
b.v = { x: b.x, y: b.y };
return b;
}
d1 *= 0.33; // Why 0.33?
d2 *= 0.33;
if (a2 < a1) {
mid += HALF_PI;
} else {
mid -= HALF_PI;
}
u = {
x: b.x + cos(mid) * d1,
y: b.y + sin(mid) * d1
};
mid -= PI;
v = {
x: b.x + cos(mid) * d2,
y: b.y + sin(mid) * d2
};
b.u = u;
b.v = v;
return b;
},
/**
* Get the reflection of a point "b" about point "a".
*/
getReflection: function(a, b, relative) {
var d = a.distanceTo(b);
if (d <= 0.0001) {
return relative ? new Two.Vector() : a.clone();
}
var theta = angleBetween(a, b);
return new Two.Vector(
d * Math.cos(theta) + (relative ? 0 : a.x),
d * Math.sin(theta) + (relative ? 0 : a.y)
);
},
angleBetween: function(A, B) {
var dx = A.x - B.x;
var dy = A.y - B.y;
return atan2(dy, dx);
},
distanceBetweenSquared: function(p1, p2) {
var dx = p1.x - p2.x;
var dy = p1.y - p2.y;
return dx * dx + dy * dy;
},
distanceBetween: function(p1, p2) {
return sqrt(distanceBetweenSquared(p1, p2));
},
mod: function(v, l) {
while (v < 0) {
v += l;
}
return v % l;
},
/**
* Array like collection that triggers inserted and removed events
* removed : pop / shift / splice
* inserted : push / unshift / splice (with > 2 arguments)
*/
Collection: function() {
Array.call(this);
if(arguments.length > 1) {
Array.prototype.push.apply(this, arguments);
} else if( arguments[0] && Array.isArray(arguments[0]) ) {
Array.prototype.push.apply(this, arguments[0]);
}
},
// Custom Error Throwing for Two.js
Error: function(message) {
this.name = 'two.js';
this.message = message;
}
}
});
Two.Utils.Error.prototype = new Error();
Two.Utils.Error.prototype.constructor = Two.Utils.Error;
Two.Utils.Collection.prototype = new Array();
Two.Utils.Collection.constructor = Two.Utils.Collection;
_.extend(Two.Utils.Collection.prototype, Backbone.Events, {
pop: function() {
var popped = Array.prototype.pop.apply(this, arguments);
this.trigger(Two.Events.remove, [popped]);
return popped;
},
shift: function() {
var shifted = Array.prototype.shift.apply(this, arguments);
this.trigger(Two.Events.remove, [shifted]);
return shifted;
},
push: function() {
var pushed = Array.prototype.push.apply(this, arguments);
this.trigger(Two.Events.insert, arguments);
return pushed;
},
unshift: function() {
var unshifted = Array.prototype.unshift.apply(this, arguments);
this.trigger(Two.Events.insert, arguments);
return unshifted;
},
splice: function() {
var spliced = Array.prototype.splice.apply(this, arguments);
var inserted;
this.trigger(Two.Events.remove, spliced);
if(arguments.length > 2) {
inserted = this.slice(arguments[0], arguments.length-2);
this.trigger(Two.Events.insert, inserted);
}
return spliced;
}
});
// Localize utils
var distanceBetween = Two.Utils.distanceBetween,
distanceBetweenSquared = Two.Utils.distanceBetweenSquared,
angleBetween = Two.Utils.angleBetween,
getControlPoints = Two.Utils.getControlPoints,
getCurveFromPoints = Two.Utils.getCurveFromPoints,
solveSegmentIntersection = Two.Utils.solveSegmentIntersection,
decoupleShapes = Two.Utils.decoupleShapes,
mod = Two.Utils.mod;
_.extend(Two.prototype, Backbone.Events, {
appendTo: function(elem) {
elem.appendChild(this.renderer.domElement);
return this;
},
play: function() {
Two.Utils.setPlaying.call(this, true);
return this.trigger(Two.Events.play);
},
pause: function() {
this.playing = false;
return this.trigger(Two.Events.pause);
},
/**
* Update positions and calculations in one pass before rendering.
*/
update: function() {
/**
* Purposefully deferred to be called after all other transformations.
*/
_.defer(_.bind(function() {
var animated = !!this._lastFrame;
var now = getNow();
this.frameCount++;
if (animated) {
this.timeDelta = parseFloat((now - this._lastFrame).toFixed(3));
}
this._lastFrame = now;
var width = this.width;
var height = this.height;
var renderer = this.renderer;
// Update width / height for the renderer
if (width !== renderer.width || height !== renderer.height) {
renderer.setSize(width, height);
}
this.trigger(Two.Events.update, this.frameCount, this.timeDelta);
this.render();
}, this));
return this;
},
/**
* Render all drawable - visible objects of the scene.
*/
render: function() {
this.renderer.render();
return this.trigger(Two.Events.render, this.frameCount);
},
/**
* Convenience Methods
*/
add: function(o) {
var objects = o;
if (!_.isArray(o)) {
objects = _.toArray(arguments);
}
this.scene.add(objects);
return this;
},
remove: function(o) {
var objects = o;
if (!_.isArray(o)) {
objects = _.toArray(arguments);
}
this.scene.remove(objects);
return this;
},
clear: function() {
_.each(this.scene.children, function(child) {
child.remove();
});
return this;
},
makeLine: function(x1, y1, x2, y2) {
var width = x2 - x1;
var height = y2 - y1;
var w2 = width / 2;
var h2 = height / 2;
var points = [
new Two.Vector(- w2, - h2),
new Two.Vector(w2, h2)
];
// Center line and translate to desired position.
var line = new Two.Polygon(points).noFill();
line.translation.set(x1 + w2, y1 + h2);
this.scene.add(line);
return line;
},
makeRectangle: function(x, y, width, height) {
var w2 = width / 2;
var h2 = height / 2;
var points = [
new Two.Vector(w2, h2),
new Two.Vector(-w2, h2),
new Two.Vector(-w2, -h2),
new Two.Vector(w2, -h2)
];
var rect = new Two.Polygon(points, true);
rect.translation.set(x, y);
this.scene.add(rect);
return rect;
},
makeCircle: function(ox, oy, r) {
return this.makeEllipse(ox, oy, r, r);
},
makeEllipse: function(ox, oy, width, height) {
var amount = Two.Resolution;
var points = _.map(_.range(amount), function(i) {
var pct = i / amount;
var theta = pct * TWO_PI;
var x = width * cos(theta);
var y = height * sin(theta);
return new Two.Vector(x, y);
}, this);
var ellipse = new Two.Polygon(points, true, true);
ellipse.translation.set(ox, oy);
this.scene.add(ellipse);
return ellipse;
},
makeCurve: function(p) {
var l = arguments.length, points = p;
if (!_.isArray(p)) {
points = [];
for (var i = 0; i < l; i+=2) {
var x = arguments[i];
if (!_.isNumber(x)) {
break;
}
var y = arguments[i + 1];
points.push(new Two.Vector(x, y));
}
}
var last = arguments[l - 1];
var poly = new Two.Polygon(points, !(_.isBoolean(last) ? last : undefined), true);
var rect = poly.getBoundingClientRect();
var cx = rect.left + rect.width / 2;
var cy = rect.top + rect.height / 2;
_.each(poly.vertices, function(v) {
v.x -= cx;
v.y -= cy;
});
poly.translation.set(cx, cy);
this.scene.add(poly);
return poly;
},
/**
* Convenience method to make and draw a Two.Polygon.
*/
makePolygon: function(p) {
var l = arguments.length, points = p;
if (!_.isArray(p)) {
points = [];
for (var i = 0; i < l; i+=2) {
var x = arguments[i];
if (!_.isNumber(x)) {
break;
}
var y = arguments[i + 1];
points.push(new Two.Vector(x, y));
}
}
var last = arguments[l - 1];
var poly = new Two.Polygon(points, !(_.isBoolean(last) ? last : undefined));
var rect = poly.getBoundingClientRect();
poly.center().translation
.set(rect.left + rect.width / 2, rect.top + rect.height / 2);
this.scene.add(poly);
return poly;
},
makeGroup: function(o) {
var objects = o;
if (!_.isArray(o)) {
objects = _.toArray(arguments);
}
var group = new Two.Group();
this.scene.add(group);
group.add(objects);
return group;
},
// Utility Functions will go here.
/**
* Interpret an SVG Node and add it to this instance's scene. The
* distinction should be made that this doesn't `import` svg's, it solely
* interprets them into something compatible for Two.js — this is slightly
* different than a direct transcription.
*/
interpret: function(svgNode) {
var tag = svgNode.tagName.toLowerCase();
if (!(tag in Two.Utils.read)) {
return null;
}
var node = Two.Utils.read[tag].call(this, svgNode);
this.add(node);
return node;
}
});
function fitToWindow() {
var wr = document.body.getBoundingClientRect();
var width = this.width = wr.width;
var height = this.height = wr.height;
this.renderer.setSize(width, height);
this.trigger(Two.Events.resize, width, height);
}
function getNow() {
return ((root.performance && root.performance.now)
? root.performance : Date).now();
}
// Request Animation Frame
(function() {
_.each(Two.Instances, function(t) {
if (t.playing) {
t.update();
}
});
requestAnimationFrame(arguments.callee);
})();
//exports to multiple environments
if (typeof define === 'function' && define.amd)
//AMD
define(function(){ return Two; });
else if (typeof module != "undefined" && module.exports)
//Node
module.exports = Two;
})();
(function() {
var Vector = Two.Vector = function(x, y) {
this.x = x || 0;
this.y = y || 0;
};
_.extend(Vector, {
MakeGetterSetter: function(object, property, value) {
var secret = '_' + property;
Object.defineProperty(object, property, {
get: function() {
return this[secret];
},
set: function(v) {
this[secret] = v;
this.trigger(Two.Events.change, property);
}
});
object[secret] = value; // Initialize private attribute.
}
});
_.extend(Vector.prototype, Backbone.Events, {
set: function(x, y) {
this.x = x;
this.y = y;
return this;
},
copy: function(v) {
this.x = v.x;
this.y = v.y;
return this;
},
clear: function() {
this.x = 0;
this.y = 0;
return this;
},
clone: function() {
return new Vector(this.x, this.y);
},
add: function(v1, v2) {
this.x = v1.x + v2.x;
this.y = v1.y + v2.y;
return this;
},
addSelf: function(v) {
this.x += v.x;
this.y += v.y;
return this;
},
sub: function(v1, v2) {
this.x = v1.x - v2.x;
this.y = v1.y - v2.y;
return this;
},
subSelf: function(v) {
this.x -= v.x;
this.y -= v.y;
return this;
},
multiplySelf: function(v) {
this.x *= v.x;
this.y *= v.y;
return this;
},
multiplyScalar: function(s) {
this.x *= s;
this.y *= s;
return this;
},
divideScalar: function(s) {
if (s) {
this.x /= s;
this.y /= s;
} else {
this.set(0, 0);
}
return this;
},
negate: function() {
return this.multiplyScalar(-1);
},
dot: function(v) {
return this.x * v.x + this.y * v.y;
},
lengthSquared: function() {
return this.x * this.x + this.y * this.y;
},
length: function() {
return Math.sqrt(this.lengthSquared());
},
normalize: function() {
return this.divideScalar(this.length());
},
distanceTo: function(v) {
return Math.sqrt(this.distanceToSquared(v));
},
distanceToSquared: function(v) {
var dx = this.x - v.x, dy = this.y - v.y;
return dx * dx + dy * dy;
},
setLength: function(l) {
return this.normalize().multiplyScalar(l);
},
equals: function(v) {
return (this.distanceTo(v) < 0.0001 /* almost same position */);
},
lerp: function(v, t) {
var x = (v.x - this.x) * t + this.x;
var y = (v.y - this.y) * t + this.y;
return this.set(x, y);
},
isZero: function() {
return (this.length() < 0.0001 /* almost zero */ );
}
});
var BoundProto = {
set: function(x, y) {
this._x = x;
this._y = y;
return this.trigger(Two.Events.change);
},
copy: function(v) {
this._x = v.x;
this._y = v.y;
return this.trigger(Two.Events.change);
},
clear: function() {
this._x = 0;
this._y = 0;
return this.trigger(Two.Events.change);
},
clone: function() {
return new Vector(this._x, this._y);
},
add: function(v1, v2) {
this._x = v1.x + v2.x;
this._y = v1.y + v2.y;
return this.trigger(Two.Events.change);
},
addSelf: function(v) {
this._x += v.x;
this._y += v.y;
return this.trigger(Two.Events.change);
},
sub: function(v1, v2) {
this._x = v1.x - v2.x;
this._y = v1.y - v2.y;
return this.trigger(Two.Events.change);
},
subSelf: function(v) {
this._x -= v.x;
this._y -= v.y;
return this.trigger(Two.Events.change);
},
multiplySelf: function(v) {
this._x *= v.x;
this._y *= v.y;
return this.trigger(Two.Events.change);
},
multiplyScalar: function(s) {
this._x *= s;
this._y *= s;
return this.trigger(Two.Events.change);
},
divideScalar: function(s) {
if (s) {
this._x /= s;
this._y /= s;
return this.trigger(Two.Events.change);
}
return this.clear();
},
negate: function() {
return this.multiplyScalar(-1);
},
dot: function(v) {
return this._x * v.x + this._y * v.y;
},
lengthSquared: function() {
return this._x * this._x + this._y * this._y;
},
length: function() {
return Math.sqrt(this.lengthSquared());
},
normalize: function() {
return this.divideScalar(this.length());
},
distanceTo: function(v) {
return Math.sqrt(this.distanceToSquared(v));
},
distanceToSquared: function(v) {
var dx = this._x - v.x, dy = this._y - v.y;
return dx * dx + dy * dy;
},
setLength: function(l) {
return this.normalize().multiplyScalar(l);
},
equals: function(v) {
return (this.distanceTo(v) < 0.0001 /* almost same position */);
},
lerp: function(v, t) {
var x = (v.x - this._x) * t + this._x;
var y = (v.y - this._y) * t + this._y;
return this.set(x, y);
},
isZero: function() {
return (this.length() < 0.0001 /* almost zero */ );
}
};
/**
* Override Backbone bind / on in order to add properly broadcasting.
* This allows Two.Vector to not broadcast events unless event listeners
* are explicity bound to it.
*/
Two.Vector.prototype.bind = Two.Vector.prototype.on = function() {
if (!this._bound) {
Two.Vector.MakeGetterSetter(this, 'x', this.x);
Two.Vector.MakeGetterSetter(this, 'y', this.y);
_.extend(this, BoundProto);
this._bound = true; // Reserved for event initialization check
}
Backbone.Events.bind.apply(this, arguments);
return this;
};
})();
(function() {
/**
* Constants
*/
var range = _.range(6),
cos = Math.cos, sin = Math.sin, tan = Math.tan;
/**
* Two.Matrix contains an array of elements that represent
* the two dimensional 3 x 3 matrix as illustrated below:
*
* =====
* a b c
* d e f
* g h i // this row is not really used in 2d transformations
* =====
*
* String order is for transform strings: a, d, b, e, c, f
*
* @class
*/
var Matrix = Two.Matrix = function(a, b, c, d, e, f) {
this.elements = new Two.Array(9);
var elements = a;
if (!_.isArray(elements)) {
elements = _.toArray(arguments);
}
// initialize the elements with default values.
this.identity().set(elements);
};
_.extend(Matrix, {
Identity: [
1, 0, 0,
0, 1, 0,
0, 0, 1
],
/**
* Multiply two matrix 3x3 arrays
*/
Multiply: function(A, B) {
if (B.length <= 3) { // Multiply Vector
var x, y, z;
var a = B[0] || 0, b = B[1] || 0, c = B[2] || 0;
var e = A;
// Go down rows first
// a, d, g, b, e, h, c, f, i
x = e[0] * a + e[1] * b + e[2] * c;
y = e[3] * a + e[4] * b + e[5] * c;
z = e[6] * a + e[7] * b + e[8] * c;
return { x: x, y: y, z: z };
}
var A0 = A[0], A1 = A[1], A2 = A[2];
var A3 = A[3], A4 = A[4], A5 = A[5];
var A6 = A[6], A7 = A[7], A8 = A[8];
var B0 = B[0], B1 = B[1], B2 = B[2];
var B3 = B[3], B4 = B[4], B5 = B[5];
var B6 = B[6], B7 = B[7], B8 = B[8];
return [
A0 * B0 + A1 * B3 + A2 * B6,
A0 * B1 + A1 * B4 + A2 * B7,
A0 * B2 + A1 * B5 + A2 * B8,
A3 * B0 + A4 * B3 + A5 * B6,
A3 * B1 + A4 * B4 + A5 * B7,
A3 * B2 + A4 * B5 + A5 * B8,
A6 * B0 + A7 * B3 + A8 * B6,
A6 * B1 + A7 * B4 + A8 * B7,
A6 * B2 + A7 * B5 + A8 * B8
];
}
});
_.extend(Matrix.prototype, Backbone.Events, {
/**
* Takes an array of elements or the arguments list itself to
* set and update the current matrix's elements. Only updates
* specified values.
*/
set: function(a, b, c, d, e, f) {
var elements = a, l = arguments.length;
if (!_.isArray(elements)) {
elements = _.toArray(arguments);
}
_.each(elements, function(v, i) {
if (_.isNumber(v)) {
this.elements[i] = v;
}
}, this);
return this.trigger(Two.Events.change);
},
/**
* Turn matrix to identity, like resetting.
*/
identity: function() {
this.set(Matrix.Identity);
return this;
},
/**
* Multiply scalar or multiply by another matrix.
*/
multiply: function(a, b, c, d, e, f, g, h, i) {
var elements = arguments, l = elements.length;
// Multiply scalar
if (l <= 1) {
_.each(this.elements, function(v, i) {
this.elements[i] = v * a;
}, this);
return this.trigger(Two.Events.change);
}
if (l <= 3) { // Multiply Vector
var x, y, z;
a = a || 0, b = b || 0, c = c || 0;
e = this.elements;
// Go down rows first
// a, d, g, b, e, h, c, f, i
x = e[0] * a + e[1] * b + e[2] * c;
y = e[3] * a + e[4] * b + e[5] * c;
z = e[6] * a + e[7] * b + e[8] * c;
return { x: x, y: y, z: z };
}
// Multiple matrix
var A = this.elements;
var B = elements;
A0 = A[0], A1 = A[1], A2 = A[2];
A3 = A[3], A4 = A[4], A5 = A[5];
A6 = A[6], A7 = A[7], A8 = A[8];
B0 = B[0], B1 = B[1], B2 = B[2];
B3 = B[3], B4 = B[4], B5 = B[5];
B6 = B[6], B7 = B[7], B8 = B[8];
this.elements[0] = A0 * B0 + A1 * B3 + A2 * B6;
this.elements[1] = A0 * B1 + A1 * B4 + A2 * B7;
this.elements[2] = A0 * B2 + A1 * B5 + A2 * B8;
this.elements[3] = A3 * B0 + A4 * B3 + A5 * B6;
this.elements[4] = A3 * B1 + A4 * B4 + A5 * B7;
this.elements[5] = A3 * B2 + A4 * B5 + A5 * B8;
this.elements[6] = A6 * B0 + A7 * B3 + A8 * B6;
this.elements[7] = A6 * B1 + A7 * B4 + A8 * B7;
this.elements[8] = A6 * B2 + A7 * B5 + A8 * B8;
return this.trigger(Two.Events.change);
},
inverse: function(out) {
var a = this.elements;
var out = out || new Two.Matrix();
var a00 = a[0], a01 = a[1], a02 = a[2],
a10 = a[3], a11 = a[4], a12 = a[5],
a20 = a[6], a21 = a[7], a22 = a[8],
b01 = a22 * a11 - a12 * a21,
b11 = -a22 * a10 + a12 * a20,
b21 = a21 * a10 - a11 * a20,
// Calculate the determinant
det = a00 * b01 + a01 * b11 + a02 * b21;
if (!det) {
return null;
}
det = 1.0 / det;
out.elements[0] = b01 * det;
out.elements[1] = (-a22 * a01 + a02 * a21) * det;
out.elements[2] = (a12 * a01 - a02 * a11) * det;
out.elements[3] = b11 * det;
out.elements[4] = (a22 * a00 - a02 * a20) * det;
out.elements[5] = (-a12 * a00 + a02 * a10) * det;
out.elements[6] = b21 * det;
out.elements[7] = (-a21 * a00 + a01 * a20) * det;
out.elements[8] = (a11 * a00 - a01 * a10) * det;
return out;
},
/**
* Set a scalar onto the matrix.
*/
scale: function(sx, sy) {
var l = arguments.length;
if (l <= 1) {
sy = sx;
}
return this.multiply(sx, 0, 0, 0, sy, 0, 0, 0, 1);
},
/**
* Rotate the matrix.
*/
rotate: function(radians) {
var c = cos(radians);
var s = sin(radians);
return this.multiply(c, -s, 0, s, c, 0, 0, 0, 1);
},
/**
* Translate the matrix.
*/
translate: function(x, y) {
return this.multiply(1, 0, x, 0, 1, y, 0, 0, 1);
},
/*
* Skew the matrix by an angle in the x axis direction.
*/
skewX: function(radians) {
var a = tan(radians);
return this.multiply(1, a, 0, 0, 1, 0, 0, 0, 1);
},
/*
* Skew the matrix by an angle in the y axis direction.
*/
skewY: function(radians) {
var a = tan(radians);
return this.multiply(1, 0, 0, a, 1, 0, 0, 0, 1);
},
/**
* Create a transform string to be used with rendering apis.
*/
toString: function() {
return this.toArray().join(' ');
},
/**
* Create a transform array to be used with rendering apis.
*/
toArray: function(fullMatrix) {
var elements = this.elements;
var a = parseFloat(elements[0].toFixed(3));
var b = parseFloat(elements[1].toFixed(3));
var c = parseFloat(elements[2].toFixed(3));
var d = parseFloat(elements[3].toFixed(3));
var e = parseFloat(elements[4].toFixed(3));
var f = parseFloat(elements[5].toFixed(3));
if (!!fullMatrix) {
var g = parseFloat(elements[6].toFixed(3));
var h = parseFloat(elements[7].toFixed(3));
var i = parseFloat(elements[8].toFixed(3));
return [
a, d, g, b, e, h, c, f, i
];
}
return [
a, d, b, e, c, f // Specific format see LN:19
];
},
/**
* Clone the current matrix.
*/
clone: function() {
var a = this.elements[0];
var b = this.elements[1];
var c = this.elements[2];
var d = this.elements[3];
var e = this.elements[4];
var f = this.elements[5];
var g = this.elements[6];
var h = this.elements[7];
var i = this.elements[8];
return new Two.Matrix(a, b, c, d, e, f, g, h, i);
}
});
})();
(function() {
/**
* Scope specific variables
*/
// Localize variables
var getCurveFromPoints = Two.Utils.getCurveFromPoints,
mod = Two.Utils.mod;
var svg = {
version: 1.1,
ns: 'http://www.w3.org/2000/svg',
xlink: 'http://www.w3.org/1999/xlink',
/**
* Create an svg namespaced element.
*/
createElement: function(name, attrs) {
var tag = name;
var elem = document.createElementNS(this.ns, tag);
if (tag === 'svg') {
attrs = _.defaults(attrs || {}, {
version: this.version
});
}
if (_.isObject(attrs)) {
this.setAttributes(elem, attrs);
}
return elem;
},
/**
* Add attributes from an svg element.
*/
setAttributes: function(elem, attrs) {
_.each(attrs, function(v, k) {
this.setAttribute(k, v);
}, elem);
return this;
},
/**
* Remove attributes from an svg element.
*/
removeAttributes: function(elem, attrs) {
_.each(attrs, function(a) {
this.removeAttribute(a);
}, elem);
return this;
},
/**
* Turn a set of vertices into a string for the d property of a path
* element. It is imperative that the string collation is as fast as
* possible, because this call will be happening multiple times a
* second.
*/
toString: function(points, closed, curved) {
var l = points.length,
last = l - 1;
if (!curved) {
return _.map(points, function(v, i) {
var command;
if (i <= 0) {
command = 'M';
} else {
command = 'L';
}
command += ' ' + v.x.toFixed(3) + ' ' + v.y.toFixed(3);
if (i >= last && closed) {
command += ' Z';
}
return command;
}).join(' ');
}
var curve = getCurveFromPoints(points, closed);
return _.map(curve, function(b, i) {
var command;
var prev = closed ? mod(i - 1, l) : Math.max(i - 1, 0);
var next = closed ? mod(i + 1, l) : Math.min(i + 1, last);
var a = curve[prev];
var c = curve[next];
var vx = a.v.x.toFixed(3);
var vy = a.v.y.toFixed(3);
var ux = b.u.x.toFixed(3);
var uy = b.u.y.toFixed(3);
var x = b.x.toFixed(3);
var y = b.y.toFixed(3);
if (i <= 0) {
command = 'M ' + x + ' ' + y;
} else {
command = 'C ' +
vx + ' ' + vy + ' ' + ux + ' ' + uy + ' ' + x + ' ' + y;
}
// Add a final point and close it off
if (i >= last && closed) {
vx = b.v.x.toFixed(3);
vy = b.v.y.toFixed(3);
ux = c.u.x.toFixed(3);
uy = c.u.y.toFixed(3);
x = c.x.toFixed(3);
y = c.y.toFixed(3);
command +=
' C ' + vx + ' ' + vy + ' ' + ux + ' ' + uy + ' ' + x + ' ' + y;
command += ' Z';
}
return command;
}).join(' ');
}
};
/**
* @class
*/
var Renderer = Two[Two.Types.svg] = function() {
this.count = 0;
this.domElement = svg.createElement('svg');
this.elements = [];
this.domElement.style.visibility = 'hidden';
this.unveil = _.once(_.bind(function() {
this.domElement.style.visibility = 'visible';
}, this));
};
_.extend(Renderer, {
Identifier: 'two-',
Utils: svg
});
_.extend(Renderer.prototype, Backbone.Events, {
setSize: function(width, height) {
this.width = width;
this.height = height;
svg.setAttributes(this.domElement, {
width: width,
height: height
});
return this;