cupiditatea
Version:
A two-dimensional drawing api meant for modern browsers.
1,614 lines (1,201 loc) • 44.4 kB
JavaScript
(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,
min = Math.min,
max = Math.max;
/**
* Localized variables
*/
var count = 0;
/**
* 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
});
_.each(params, function(v, k) {
if (k === 'fullscreen' || k === 'width' || k === 'height' || k === 'autostart') {
return;
}
this[k] = v;
}, this);
// Specified domElement overrides type declaration.
if (_.isElement(params.domElement)) {
this.type = Two.Types[params.domElement.tagName.toLowerCase()];
}
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 if (!_.isElement(params.domElement)) {
this.renderer.setSize(params.width, params.height, this.ratio);
this.width = params.width;
this.height = params.height;
}
this.scene = this.renderer.scene;
Two.Instances.push(this);
};
_.extend(Two, {
/**
* Primitive
*/
Array: root.Float32Array || Array,
Types: {
webgl: 'WebGLRenderer',
svg: 'SVGRenderer',
canvas: 'CanvasRenderer'
},
Version: 'v0.4.0',
Identifier: 'two_',
Properties: {
hierarchy: 'hierarchy',
demotion: 'demotion'
},
Events: {
play: 'play',
pause: 'pause',
update: 'update',
render: 'render',
resize: 'resize',
change: 'change',
remove: 'remove',
insert: 'insert'
},
Commands: {
move: 'M',
line: 'L',
curve: 'C',
close: 'Z'
},
Resolution: 8,
Instances: [],
noConflict: function() {
root.Two = previousTwo;
return this;
},
uniqueId: function() {
var id = count;
count++;
return id;
},
Utils: {
/**
* Release an arbitrary class' events from the two.js corpus and recurse
* through its children and or vertices.
*/
release: function(obj) {
if (!_.isObject(obj)) {
return;
}
if (_.isFunction(obj.unbind)) {
obj.unbind();
}
if (obj.vertices) {
if (_.isFunction(obj.vertices.unbind)) {
obj.vertices.unbind();
}
_.each(obj.vertices, function(v) {
if (_.isFunction(v.unbind)) {
v.unbind();
}
});
}
if (obj.children) {
_.each(obj.children, function(obj) {
Two.Utils.release(obj);
});
}
},
Curve: {
CollinearityEpsilon: pow(10, -30),
RecursionLimit: 16,
CuspLimit: 0,
Tolerance: {
distance: 0.25,
angle: 0,
epsilon: 0.01
},
// Lookup tables for abscissas and weights with values for n = 2 .. 16.
// As values are symmetric, only store half of them and adapt algorithm
// to factor in symmetry.
abscissas: [
[ 0.5773502691896257645091488],
[0,0.7745966692414833770358531],
[ 0.3399810435848562648026658,0.8611363115940525752239465],
[0,0.5384693101056830910363144,0.9061798459386639927976269],
[ 0.2386191860831969086305017,0.6612093864662645136613996,0.9324695142031520278123016],
[0,0.4058451513773971669066064,0.7415311855993944398638648,0.9491079123427585245261897],
[ 0.1834346424956498049394761,0.5255324099163289858177390,0.7966664774136267395915539,0.9602898564975362316835609],
[0,0.3242534234038089290385380,0.6133714327005903973087020,0.8360311073266357942994298,0.9681602395076260898355762],
[ 0.1488743389816312108848260,0.4333953941292471907992659,0.6794095682990244062343274,0.8650633666889845107320967,0.9739065285171717200779640],
[0,0.2695431559523449723315320,0.5190961292068118159257257,0.7301520055740493240934163,0.8870625997680952990751578,0.9782286581460569928039380],
[ 0.1252334085114689154724414,0.3678314989981801937526915,0.5873179542866174472967024,0.7699026741943046870368938,0.9041172563704748566784659,0.9815606342467192506905491],
[0,0.2304583159551347940655281,0.4484927510364468528779129,0.6423493394403402206439846,0.8015780907333099127942065,0.9175983992229779652065478,0.9841830547185881494728294],
[ 0.1080549487073436620662447,0.3191123689278897604356718,0.5152486363581540919652907,0.6872929048116854701480198,0.8272013150697649931897947,0.9284348836635735173363911,0.9862838086968123388415973],
[0,0.2011940939974345223006283,0.3941513470775633698972074,0.5709721726085388475372267,0.7244177313601700474161861,0.8482065834104272162006483,0.9372733924007059043077589,0.9879925180204854284895657],
[ 0.0950125098376374401853193,0.2816035507792589132304605,0.4580167776572273863424194,0.6178762444026437484466718,0.7554044083550030338951012,0.8656312023878317438804679,0.9445750230732325760779884,0.9894009349916499325961542]
],
weights: [
[1],
[0.8888888888888888888888889,0.5555555555555555555555556],
[0.6521451548625461426269361,0.3478548451374538573730639],
[0.5688888888888888888888889,0.4786286704993664680412915,0.2369268850561890875142640],
[0.4679139345726910473898703,0.3607615730481386075698335,0.1713244923791703450402961],
[0.4179591836734693877551020,0.3818300505051189449503698,0.2797053914892766679014678,0.1294849661688696932706114],
[0.3626837833783619829651504,0.3137066458778872873379622,0.2223810344533744705443560,0.1012285362903762591525314],
[0.3302393550012597631645251,0.3123470770400028400686304,0.2606106964029354623187429,0.1806481606948574040584720,0.0812743883615744119718922],
[0.2955242247147528701738930,0.2692667193099963550912269,0.2190863625159820439955349,0.1494513491505805931457763,0.0666713443086881375935688],
[0.2729250867779006307144835,0.2628045445102466621806889,0.2331937645919904799185237,0.1862902109277342514260976,0.1255803694649046246346943,0.0556685671161736664827537],
[0.2491470458134027850005624,0.2334925365383548087608499,0.2031674267230659217490645,0.1600783285433462263346525,0.1069393259953184309602547,0.0471753363865118271946160],
[0.2325515532308739101945895,0.2262831802628972384120902,0.2078160475368885023125232,0.1781459807619457382800467,0.1388735102197872384636018,0.0921214998377284479144218,0.0404840047653158795200216],
[0.2152638534631577901958764,0.2051984637212956039659241,0.1855383974779378137417166,0.1572031671581935345696019,0.1215185706879031846894148,0.0801580871597602098056333,0.0351194603317518630318329],
[0.2025782419255612728806202,0.1984314853271115764561183,0.1861610000155622110268006,0.1662692058169939335532009,0.1395706779261543144478048,0.1071592204671719350118695,0.0703660474881081247092674,0.0307532419961172683546284],
[0.1894506104550684962853967,0.1826034150449235888667637,0.1691565193950025381893121,0.1495959888165767320815017,0.1246289712555338720524763,0.0951585116824927848099251,0.0622535239386478928628438,0.0271524594117540948517806]
]
},
/**
* Account for high dpi rendering.
* http://www.html5rocks.com/en/tutorials/canvas/hidpi/
*/
devicePixelRatio: root.devicePixelRatio || 1,
getBackingStoreRatio: function(ctx) {
return ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
},
getRatio: function(ctx) {
return Two.Utils.devicePixelRatio / getBackingStoreRatio(ctx);
},
/**
* Properly defer play calling until after all objects
* have been updated with their newest styles.
*/
setPlaying: function(b) {
this.playing = !!b;
return this;
},
/**
* Return the computed matrix of a nested object.
* TODO: Optimize traversal.
*/
getComputedMatrix: function(object, matrix) {
matrix = (matrix && matrix.identity()) || new Two.Matrix();
var parent = object, matrices = [];
while (parent && parent._matrix) {
matrices.push(parent._matrix);
parent = parent.parent;
}
matrices.reverse();
_.each(matrices, function(m) {
var e = m.elements;
matrix.multiply(
e[0], e[1], e[2], e[3], e[4], e[5], e[6], e[7], e[8], e[9]);
});
return matrix;
},
deltaTransformPoint: function(matrix, x, y) {
var dx = x * matrix.a + y * matrix.c + 0;
var dy = x * matrix.b + y * matrix.d + 0;
return new Two.Vector(dx, dy);
},
/**
* https://gist.github.com/2052247
*/
decomposeMatrix: function(matrix) {
// calculate delta transform point
var px = Two.Utils.deltaTransformPoint(matrix, 0, 1);
var py = Two.Utils.deltaTransformPoint(matrix, 1, 0);
// calculate skew
var skewX = ((180 / Math.PI) * Math.atan2(px.y, px.x) - 90);
var skewY = ((180 / Math.PI) * Math.atan2(py.y, py.x));
return {
translateX: matrix.e,
translateY: matrix.f,
scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b),
scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d),
skewX: skewX,
skewY: skewY,
rotation: skewX // rotation is the same as skew x
};
},
/**
* Walk through item properties and pick the ones of interest.
* Will try to resolve styles applied via CSS
*/
applySvgAttributes: function(node, elem) {
var attributes = {}, styles = {}, i, key, value, attr;
// Not available in non browser environments
if (getComputedStyle) {
// Convert CSSStyleDeclaration to a normal object
var computedStyles = getComputedStyle(node);
i = computedStyles.length;
while(i--) {
key = computedStyles[i];
value = computedStyles[key];
// Gecko returns undefined for unset properties
// Webkit returns the default value
if (value !== undefined) {
styles[key] = value;
}
}
}
// Convert NodeMap to a normal object
i = node.attributes.length;
while(i--) {
attr = node.attributes[i];
attributes[attr.nodeName] = attr.value;
}
// Getting the correct opacity is a bit tricky, since SVG path elements don't
// support opacity as an attribute, but you can apply it via CSS.
// So we take the opacity and set (stroke/fill)-opacity to the same value.
if (!_.isUndefined(styles.opacity)) {
styles['stroke-opacity'] = styles.opacity;
styles['fill-opacity'] = styles.opacity;
}
// Merge attributes and applied styles (attributes take precedence)
_.extend(styles, attributes);
// Similarly visibility is influenced by the value of both display and visibility.
// Calculate a unified value here which defaults to `true`.
styles.visible = !(_.isUndefined(styles.display) && styles.display === 'none')
|| (_.isUndefined(styles.visibility) && styles.visibility === 'hidden');
// Now iterate the whole thing
for (key in styles) {
value = styles[key];
switch (key) {
case 'transform':
if (value === 'none') break;
var m = node.getCTM();
// Might happen when transform string is empty or not valid.
if (m === null) break;
// // Option 1: edit the underlying matrix and don't force an auto calc.
// var m = node.getCTM();
// elem._matrix.manual = true;
// elem._matrix.set(m.a, m.b, m.c, m.d, m.e, m.f);
// Option 2: Decompose and infer Two.js related properties.
var transforms = Two.Utils.decomposeMatrix(node.getCTM());
elem.translation.set(transforms.translateX, transforms.translateY);
elem.rotation = transforms.rotation;
// Warning: Two.js elements only support uniform scalars...
elem.scale = transforms.scaleX;
// Override based on attributes.
if (styles.x) {
elem.translation.x = styles.x;
}
if (styles.y) {
elem.translation.y = styles.y;
}
break;
case 'visible':
elem.visible = value;
break;
case 'stroke-linecap':
elem.cap = value;
break;
case 'stroke-linejoin':
elem.join = value;
break;
case 'stroke-miterlimit':
elem.miter = value;
break;
case 'stroke-width':
elem.linewidth = parseFloat(value);
break;
case 'stroke-opacity':
case 'fill-opacity':
case 'opacity':
elem.opacity = parseFloat(value);
break;
case 'fill':
case 'stroke':
elem[key] = (value === 'none') ? 'transparent' : value;
break;
case 'id':
elem.id = value;
break;
case 'class':
elem.classList = value.split(' ');
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();
// Switched up order to inherit more specific styles
Two.Utils.applySvgAttributes(node, group);
for (var i = 0, l = node.childNodes.length; i < l; i++) {
var n = node.childNodes[i];
var tag = n.nodeName;
if (!tag) return;
var tagName = tag.replace(/svg\:/ig, '').toLowerCase();
if (tagName in Two.Utils.read) {
var o = Two.Utils.read[tagName].call(this, n);
group.add(o);
}
}
return group;
},
polygon: function(node, open) {
var points = node.getAttribute('points');
var verts = [];
points.replace(/(-?[\d\.?]+),(-?[\d\.?]+)/g, function(match, p1, p2) {
verts.push(new Two.Anchor(parseFloat(p1), parseFloat(p2)));
});
var poly = new Two.Polygon(verts, !open).noStroke();
poly.fill = 'black';
return Two.Utils.applySvgAttributes(node, poly);
},
polyline: function(node) {
return Two.Utils.read.polygon(node, true);
},
path: function(node) {
var path = node.getAttribute('d');
// Create a Two.Polygon from the paths.
var coord = new Two.Anchor();
var control, coords;
var closed = false, relative = false;
var commands = path.match(/[a-df-z][^a-df-z]*/ig);
var last = commands.length - 1;
// Split up polybeziers
_.each(commands.slice(0), function(command, i) {
var type = command[0];
var lower = type.toLowerCase();
var items = command.slice(1).trim().split(/[\s,]+|(?=\s?[+\-])/);
var pre, post, result = [], bin;
if (i <= 0) {
commands = [];
}
switch (lower) {
case 'h':
case 'v':
if (items.length > 1) {
bin = 1;
}
break;
case 'm':
case 'l':
case 't':
if (items.length > 2) {
bin = 2;
}
break;
case 's':
case 'q':
if (items.length > 4) {
bin = 4;
}
break;
case 'c':
if (items.length > 6) {
bin = 6;
}
break;
case 'a':
// TODO: Handle Ellipses
break;
}
if (bin) {
for (var j = 0, l = items.length, times = 0; j < l; j+=bin) {
var ct = type;
if (times > 0) {
switch (type) {
case 'm':
ct = 'l';
break;
case 'M':
ct = 'L';
break;
}
}
result.push([ct].concat(items.slice(j, j + bin)).join(' '));
times++;
}
commands = Array.prototype.concat.apply(commands, result);
} else {
commands.push(command);
}
});
// Create the vertices for our Two.Polygon
var points = _.flatten(_.map(commands, function(command, i) {
var result, x, y;
var type = command[0];
var lower = type.toLowerCase();
coords = command.slice(1).trim();
coords = coords.replace(/(-?\d+(?:\.\d*)?)[eE]([+\-]?\d+)/g, function(match, n1, n2) {
return parseFloat(n1) * pow(10, n2);
});
coords = coords.split(/[\s,]+|(?=\s?[+\-])/);
relative = type === lower;
var x1, y1, x2, y2, x3, y3, x4, y4, reflection;
switch (lower) {
case 'z':
if (i >= last) {
closed = true;
} else {
x = coord.x;
y = coord.y;
result = new Two.Anchor(
x, y,
undefined, undefined,
undefined, undefined,
Two.Commands.close
);
}
break;
case 'm':
case 'l':
x = parseFloat(coords[0]);
y = parseFloat(coords[1]);
result = new Two.Anchor(
x, y,
undefined, undefined,
undefined, undefined,
lower === 'm' ? Two.Commands.move : Two.Commands.line
);
if (relative) {
result.addSelf(coord);
}
// result.controls.left.copy(result);
// result.controls.right.copy(result);
coord = result;
break;
case 'h':
case 'v':
var a = lower === 'h' ? 'x' : 'y';
var b = a === 'x' ? 'y' : 'x';
result = new Two.Anchor(
undefined, undefined,
undefined, undefined,
undefined, undefined,
Two.Commands.line
);
result[a] = parseFloat(coords[0]);
result[b] = coord[b];
if (relative) {
result[a] += coord[a];
}
// result.controls.left.copy(result);
// result.controls.right.copy(result);
coord = result;
break;
case 'c':
case 's':
x1 = coord.x;
y1 = coord.y;
if (!control) {
control = new Two.Vector();//.copy(coord);
}
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 = 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;
}
if (!_.isObject(coord.controls)) {
Two.Anchor.AppendCurveProperties(coord);
}
coord.controls.right.set(x2 - coord.x, y2 - coord.y);
result = new Two.Anchor(
x4, y4,
x3 - x4, y3 - y4,
undefined, undefined,
Two.Commands.curve
);
coord = result;
control = result.controls.left;
break;
case 't':
case 'q':
x1 = coord.x;
y1 = coord.y;
if (!control) {
control = new Two.Vector();//.copy(coord);
}
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 = 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;
}
if (!_.isObject(coord.controls)) {
Two.Anchor.AppendCurveProperties(coord);
}
coord.controls.right.set(x2 - coord.x, y2 - coord.y);
result = new Two.Anchor(
x4, y4,
x3 - x4, y3 - y4,
undefined, undefined,
Two.Commands.curve
);
coord = result;
control = result.controls.left;
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, undefined, true).noStroke();
poly.fill = 'black';
return Two.Utils.applySvgAttributes(node, poly);
},
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.Anchor(x, y);
}, this);
var circle = new Two.Polygon(points, true, true).noStroke();
circle.translation.set(x, y);
circle.fill = 'black';
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.Anchor(x, y);
}, this);
var ellipse = new Two.Polygon(points, true, true).noStroke();
ellipse.translation.set(x, y);
ellipse.fill = 'black';
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.Anchor(w2, h2),
new Two.Anchor(-w2, h2),
new Two.Anchor(-w2, -h2),
new Two.Anchor(w2, -h2)
];
var rect = new Two.Polygon(points, true).noStroke();
rect.translation.set(x + w2, y + h2);
rect.fill = 'black';
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.Anchor(- w2, - h2),
new Two.Anchor(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 points plotted along
* the curve. Number points determined by limit.
*/
subdivide: function(x1, y1, x2, y2, x3, y3, x4, y4, limit) {
limit = limit || Two.Utils.Curve.RecursionLimit;
var amount = limit + 1;
// TODO: Issue 73
// Don't recurse if the end points are identical
if (x1 === x4 && y1 === y4) {
return [new Two.Anchor(x4, y4)];
}
return _.map(_.range(0, amount), function(i) {
var t = i / amount;
var x = getPointOnCubicBezier(t, x1, x2, x3, x4);
var y = getPointOnCubicBezier(t, y1, y2, y3, y4);
return new Two.Anchor(x, y);
});
},
getPointOnCubicBezier: function(t, a, b, c, d) {
var k = 1 - t;
return (k * k * k * a) + (3 * k * k * t * b) + (3 * k * t * t * c) +
(t * t * t * d);
},
/**
* Given 2 points (a, b) and corresponding control point for each
* return a float that represents the length of the curve using
* Gauss-Legendre algorithm. Limit iterations of calculation by `limit`.
*/
getCurveLength: function(x1, y1, x2, y2, x3, y3, x4, y4, limit) {
// TODO: Better / fuzzier equality check
// Linear calculation
if (x1 === x2 && y1 === y2 && x3 === x4 && y3 === y4) {
var dx = x4 - x1;
var dy = y4 - y1;
return sqrt(dx * dx + dy * dy);
}
// Calculate the coefficients of a Bezier derivative.
var ax = 9 * (x2 - x3) + 3 * (x4 - x1),
bx = 6 * (x1 + x3) - 12 * x2,
cx = 3 * (x2 - x1),
ay = 9 * (y2 - y3) + 3 * (y4 - y1),
by = 6 * (y1 + y3) - 12 * y2,
cy = 3 * (y2 - y1);
var integrand = function(t) {
// Calculate quadratic equations of derivatives for x and y
var dx = (ax * t + bx) * t + cx,
dy = (ay * t + by) * t + cy;
return sqrt(dx * dx + dy * dy);
};
return integrate(
integrand, 0, 1, limit || Two.Utils.Curve.RecursionLimit
);
},
/**
* Integration for `getCurveLength` calculations. Referenced from
* Paper.js: https://github.com/paperjs/paper.js/blob/master/src/util/Numerical.js#L101
*/
integrate: function(f, a, b, n) {
var x = Two.Utils.Curve.abscissas[n - 2],
w = Two.Utils.Curve.weights[n - 2],
A = 0.5 * (b - a),
B = A + a,
i = 0,
m = (n + 1) >> 1,
sum = n & 1 ? w[i++] * f(B) : 0; // Handle odd n
while (i < m) {
var Ax = A * x[i];
sum += w[i++] * (f(B + Ax) + f(B - Ax));
}
return A * sum;
},
/**
* Creates a set of points that have u, v values for anchor positions
*/
getCurveFromPoints: function(points, closed) {
var l = points.length, last = l - 1;
for (var i = 0; i < l; i++) {
var point = points[i];
if (!_.isObject(point.controls)) {
Two.Anchor.AppendCurveProperties(point);
}
var prev = closed ? mod(i - 1, l) : max(i - 1, 0);
var next = closed ? mod(i + 1, l) : min(i + 1, last);
var a = points[prev];
var b = point;
var c = points[next];
getControlPoints(a, b, c);
b._command = i === 0 ? Two.Commands.move : Two.Commands.curve;
b.controls.left.x = _.isNumber(b.controls.left.x) ? b.controls.left.x : b.x;
b.controls.left.y = _.isNumber(b.controls.left.y) ? b.controls.left.y : b.y;
b.controls.right.x = _.isNumber(b.controls.right.x) ? b.controls.right.x : b.x;
b.controls.right.y = _.isNumber(b.controls.right.y) ? b.controls.right.y : b.y;
}
},
/**
* 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.
b.u = _.isObject(b.controls.left) ? b.controls.left : new Two.Vector(0, 0);
b.v = _.isObject(b.controls.right) ? b.controls.right : new Two.Vector(0, 0);
// TODO: Issue 73
if (d1 < 0.0001 || d2 < 0.0001) {
if (!b._relative) {
b.controls.left.copy(b);
b.controls.right.copy(b);
}
return b;
}
d1 *= 0.33; // Why 0.33?
d2 *= 0.33;
if (a2 < a1) {
mid += HALF_PI;
} else {
mid -= HALF_PI;
}
b.controls.left.x = cos(mid) * d1;
b.controls.left.y = sin(mid) * d1;
mid -= PI;
b.controls.right.x = cos(mid) * d2;
b.controls.right.y = sin(mid) * d2;
if (!b._relative) {
b.controls.left.x += b.x;
b.controls.left.y += b.y;
b.controls.right.x += b.x;
b.controls.right.y += b.y;
}
return b;
},
/**
* Get the reflection of a point "b" about point "a". Where "a" is in
* absolute space and "b" is relative to "a".
*
* http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes
*/
getReflection: function(a, b, relative) {
return new Two.Vector(
2 * a.x - (b.x + a.x) - (relative ? a.x : 0),
2 * a.y - (b.y + a.y) - (relative ? a.y : 0)
);
},
angleBetween: function(A, B) {
var dx, dy;
if (arguments.length >= 4) {
dx = arguments[0] - arguments[2];
dy = arguments[1] - arguments[3];
return atan2(dy, dx);
}
dx = A.x - B.x;
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));
},
// A pretty fast toFixed(3) alternative
// See http://jsperf.com/parsefloat-tofixed-vs-math-round/18
toFixed: function(v) {
return Math.floor(v * 1000) / 1000;
},
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,
getBackingStoreRatio = Two.Utils.getBackingStoreRatio,
getPointOnCubicBezier = Two.Utils.getPointOnCubicBezier,
getCurveLength = Two.Utils.getCurveLength,
integrate = Two.Utils.integrate,
getReflection = Two.Utils.getReflection;
_.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() {
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.ratio);
}
this.trigger(Two.Events.update, this.frameCount, this.timeDelta);
return this.render();
},
/**
* 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() {
this.scene.remove(_.toArray(this.scene.children));
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.Anchor(- w2, - h2),
new Two.Anchor(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.Anchor(-w2, -h2),
new Two.Anchor(w2, -h2),
new Two.Anchor(w2, h2),
new Two.Anchor(-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.Anchor(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.Anchor(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.Anchor(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.
*
* @param {Object} svgNode - The node to be parsed
* @param {Boolean} noWrappingGroup - Don't create a top-most group but
* append all contents directly
*/
interpret: function(svgNode, noWrapInGroup) {
var tag = svgNode.tagName.toLowerCase();
if (!(tag in Two.Utils.read)) {
return null;
}
var node = Two.Utils.read[tag].call(this, svgNode);
if (noWrapInGroup && node instanceof Two.Group) {
this.add(_.values(node.children));
} else {
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.ratio);
this.trigger(Two.Events.resize, width, height);
}
function getNow() {
return ((root.performance && root.performance.now)
? root.performance : Date).now();
}
// Request Animation Frame
(function() {
requestAnimationFrame(arguments.callee);
Two.Instances.forEach(function(t) {
if (t.playing) {
t.update();
}
});
})();
//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;
})();