Last active
August 29, 2015 14:02
-
-
Save ponychicken/e5589db1d04e15277789 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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. | |
* | |
*/ | |
(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 = {}; | |
// Not available in non browser environments | |
if (getComputedStyle) { | |
// Convert CSSStyleDeclaration to a normal object | |
var computedStyles = getComputedStyle(node); | |
_.each(computedStyles, function (item) { | |
styles[item] = computedStyles[item]; | |
}); | |
} | |
// Convert NodeMap to a normal object | |
_.each(node.attributes, function(v, k) { | |
attributes[v.nodeName] = v.nodeValue; | |
}); | |
// 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 | |
styles.visible = (styles.display !== 'none') && (styles.visibility === 'visible'); | |
// Now iterate the whole thing | |
_.each(styles, function(value, key) { | |
switch (key) { | |
case 'transform': | |
if (value === 'none') break; | |
var m = node.getCTM(); | |
if (m === null) break; | |
var matrix = new Two.Matrix(m.a, m.b, m.c, m.d, m.e, m.f); | |
// 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': | |
if (!elem.classList) elem.classList = []; | |
value.split(' ').forEach(function (cl) { | |
elem.classList.push(cl); | |
}); | |
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); | |
_.each(node.childNodes, function(n) { | |
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); | |
} | |
}, this); | |
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, control; | |
var coords, relative = false; | |
var closed = false; | |
var commands = path.match(/[a-df-z][^a-df-z]*/ig); | |
var last = commands.length - 1; | |
// Go through commands and look for Inkscape irregularities | |
_.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 'm': | |
case 'l': | |
case 'h': | |
case 'v': | |
if (items.length > 2) { | |
bin = 2; | |
} | |
break; | |
case 'c': | |
case 's': | |
case 't': | |
case 'q': | |
if (items.length > 6) { | |
bin = 6; | |
} | |
break; | |
case 'a': | |
// TODO: Handle Ellipses | |
break; | |
} | |
if (bin) { | |
for (var j = 0, l = items.length; j < l; j+=bin) { | |
result.push([type].concat(items.slice(j, j + bin)).join(' ')); | |
} | |
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 = 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; | |
} | |
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 = 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; | |
} | |
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". | |
*/ | |
getReflection: function(a, b, relative) { | |
var d = b.distanceTo(Two.Vector.zero); | |
var theta = angleBetween(Two.Vector.zero, b); | |
return new Two.Vector( | |
d * cos(theta) + (relative ? 0 : a.x), | |
d * sin(theta) + (relative ? 0 : a.y) | |
); | |
}, | |
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)); | |
}, | |
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; | |
_.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; | |
})(); | |
(function() { | |
var Vector = Two.Vector = function(x, y) { | |
this.x = x || 0; | |
this.y = y || 0; | |
}; | |
_.extend(Vector, { | |
zero: new Two.Vector() | |
}); | |
_.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 */ ); | |
}, | |
toString: function() { | |
return this.x + ',' + this.y; | |
}, | |
toObject: function() { | |
return { x: this.x, y: this.y }; | |
} | |
}); | |
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 */ ); | |
}, | |
toString: function() { | |
return this._x + ',' + this._y; | |
}, | |
toObject: function() { | |
return { x: this._x, y: this._y }; | |
} | |
}; | |
var xgs = { | |
get: function() { | |
return this._x; | |
}, | |
set: function(v) { | |
this._x = v; | |
this.trigger(Two.Events.change, 'x'); | |
} | |
}; | |
var ygs = { | |
get: function() { | |
return this._y; | |
}, | |
set: function(v) { | |
this._y = v; | |
this.trigger(Two.Events.change, 'y'); | |
} | |
}; | |
/** | |
* 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) { | |
this._x = this.x; | |
this._y = this.y; | |
Object.defineProperty(this, 'x', xgs); | |
Object.defineProperty(this, 'y', ygs); | |
_.extend(this, BoundProto); | |
this._bound = true; // Reserved for event initialization check | |
} | |
Backbone.Events.bind.apply(this, arguments); | |
return this; | |
}; | |
})(); | |
(function() { | |
// Localized variables | |
var commands = Two.Commands; | |
/** | |
* An object that holds 3 `Two.Vector`s, the anchor point and its | |
* corresponding handles: `left` and `right`. | |
*/ | |
var Anchor = Two.Anchor = function(x, y, ux, uy, vx, vy, command) { | |
Two.Vector.call(this, x, y); | |
this._broadcast = _.bind(function() { | |
this.trigger(Two.Events.change); | |
}, this); | |
this._command = command || commands.move; | |
this._relative = true; | |
if (!command) { | |
return this; | |
} | |
Anchor.AppendCurveProperties(this); | |
if (_.isNumber(ux)) { | |
this.controls.left.x = ux; | |
} | |
if (_.isNumber(uy)) { | |
this.controls.left.y = uy; | |
} | |
if (_.isNumber(vx)) { | |
this.controls.right.x = vx; | |
} | |
if (_.isNumber(vy)) { | |
this.controls.right.y = vy; | |
} | |
}; | |
_.extend(Anchor, { | |
AppendCurveProperties: function(anchor) { | |
var x = anchor._x || anchor.x; | |
var y = anchor._y || anchor.y; | |
anchor.controls = { | |
left: new Two.Vector(0, 0), | |
right: new Two.Vector(0, 0) | |
}; | |
} | |
}); | |
var AnchorProto = { | |
listen: function() { | |
if (!_.isObject(this.controls)) { | |
Anchor.AppendCurveProperties(this); | |
} | |
_.each(this.controls, function(v) { | |
v.bind(Two.Events.change, this._broadcast); | |
}, this); | |
return this; | |
}, | |
ignore: function() { | |
_.each(this.controls, function(v) { | |
v.unbind(Two.Events.change, this._broadcast); | |
}, this); | |
return this; | |
}, | |
clone: function() { | |
var controls = this.controls; | |
var clone = new Two.Anchor( | |
this.x, | |
this.y, | |
controls && controls.left.x, | |
controls && controls.left.y, | |
controls && controls.right.x, | |
controls && controls.right.y, | |
this.command | |
); | |
clone.relative = this._relative; | |
return clone; | |
}, | |
toObject: function() { | |
var o = { | |
x: this.x, | |
y: this.y | |
}; | |
if (this._command) { | |
o.command = this._command; | |
} | |
if (this._relative) { | |
o.relative = this._relative; | |
} | |
if (this.controls) { | |
o.controls = { | |
left: this.controls.left.toObject(), | |
right: this.controls.right.toObject() | |
}; | |
} | |
return o; | |
} | |
}; | |
Object.defineProperty(Anchor.prototype, 'command', { | |
get: function() { | |
return this._command; | |
}, | |
set: function(c) { | |
this._command = c; | |
if (this._command === commands.curve && !_.isObject(this.controls)) { | |
Anchor.AppendCurveProperties(this); | |
} | |
return this.trigger(Two.Events.change); | |
} | |
}); | |
Object.defineProperty(Anchor.prototype, 'relative', { | |
get: function() { | |
return this._relative; | |
}, | |
set: function(b) { | |
if (this._relative == b) { | |
return this; | |
} | |
this._relative = !!b; | |
return this.trigger(Two.Events.change); | |
} | |
}); | |
_.extend(Anchor.prototype, Two.Vector.prototype, AnchorProto); | |
// Make it possible to bind and still have the Anchor specific | |
// inheritance from Two.Vector | |
Two.Anchor.prototype.bind = Two.Anchor.prototype.on = function() { | |
Two.Vector.prototype.bind.apply(this, arguments); | |
_.extend(this, AnchorProto); | |
}; | |
Two.Anchor.prototype.unbind = Two.Anchor.prototype.off = function() { | |
Two.Vector.prototype.unbind.apply(this, arguments); | |
_.extend(this, AnchorProto); | |
}; | |
})(); | |
(function() { | |
/** | |
* Constants | |
*/ | |
var 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, C) { | |
if (B.length <= 3) { // Multiply Vector | |
var x, y, z, e = A; | |
var a = B[0] || 0, | |
b = B[1] || 0, | |
c = B[2] || 0; | |
// 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]; | |
C = C || new Two.Array(9); | |
C[0] = A0 * B0 + A1 * B3 + A2 * B6; | |
C[1] = A0 * B1 + A1 * B4 + A2 * B7; | |
C[2] = A0 * B2 + A1 * B5 + A2 * B8; | |
C[3] = A3 * B0 + A4 * B3 + A5 * B6; | |
C[4] = A3 * B1 + A4 * B4 + A5 * B7; | |
C[5] = A3 * B2 + A4 * B5 + A5 * B8; | |
C[6] = A6 * B0 + A7 * B3 + A8 * B6; | |
C[7] = A6 * B1 + A7 * B4 + A8 * B7; | |
C[8] = A6 * B2 + A7 * B5 + A8 * B8; | |
return C; | |
} | |
}); | |
_.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; | |
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; | |
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]; | |
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; | |
out = out || new Two.Matrix(); | |
var a00 = a[0], a01 = a[1], a02 = a[2]; | |
var a10 = a[3], a11 = a[4], a12 = a[5]; | |
var a20 = a[6], a21 = a[7], a22 = a[8]; | |
var b01 = a22 * a11 - a12 * a21; | |
var b11 = -a22 * a10 + a12 * a20; | |
var b21 = a21 * a10 - a11 * a20; | |
// Calculate the determinant | |
var 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(fullMatrix) { | |
var temp = []; | |
this.toArray(fullMatrix, temp); | |
return temp.join(' '); | |
}, | |
/** | |
* Create a transform array to be used with rendering apis. | |
*/ | |
toArray: function(fullMatrix, output) { | |
var elements = this.elements; | |
var hasOutput = !!output; | |
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)); | |
if (hasOutput) { | |
output[0] = a; | |
output[1] = d; | |
output[2] = g; | |
output[3] = b; | |
output[4] = e; | |
output[5] = h; | |
output[6] = c; | |
output[7] = f; | |
output[8] = i; | |
return; | |
} | |
return [ | |
a, d, g, b, e, h, c, f, i | |
]; | |
} | |
if (hasOutput) { | |
output[0] = a; | |
output[1] = d; | |
output[2] = b; | |
output[3] = e; | |
output[4] = c; | |
output[5] = f; | |
return; | |
} | |
return [ | |
a, d, b, e, c, f // Specific format see LN:19 | |
]; | |
}, | |
/** | |
* Clone the current matrix. | |
*/ | |
clone: function() { | |
var a, b, c, d, e, f, g, h, i; | |
a = this.elements[0]; | |
b = this.elements[1]; | |
c = this.elements[2]; | |
d = this.elements[3]; | |
e = this.elements[4]; | |
f = this.elements[5]; | |
g = this.elements[6]; | |
h = this.elements[7]; | |
i = this.elements[8]; | |
return new Two.Matrix(a, b, c, d, e, f, g, h, i); | |
} | |
}); | |
})(); | |
(function() { | |
// Localize variables | |
var 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)) { | |
svg.setAttributes(elem, attrs); | |
} | |
return elem; | |
}, | |
setAttribute: function(v, k) { | |
this.setAttribute(k, v); | |
}, | |
/** | |
* Add attributes from an svg element. | |
*/ | |
setAttributes: function(elem, attrs) { | |
_.each(attrs, svg.setAttribute, elem); | |
return this; | |
}, | |
removeAttribute: function(v, k) { | |
this.removeAttribute(k); | |
}, | |
/** | |
* Remove attributes from an svg element. | |
*/ | |
removeAttributes: function(elem, attrs) { | |
_.each(attrs, svg.removeAttribute, 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) { | |
var l = points.length, | |
last = l - 1, | |
d; // The elusive last Two.Commands.move point | |
return _.map(points, 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 = points[prev]; | |
var c = points[next]; | |
var vx, vy, ux, uy, ar, bl, br, cl; | |
var x = b.x.toFixed(3); | |
var y = b.y.toFixed(3); | |
switch (b._command) { | |
case Two.Commands.close: | |
command = Two.Commands.close; | |
break; | |
case Two.Commands.curve: | |
ar = (a.controls && a.controls.right) || a; | |
bl = (b.controls && b.controls.left) || b; | |
if (a._relative) { | |
vx = (ar.x + a.x).toFixed(3); | |
vy = (ar.y + a.y).toFixed(3); | |
} else { | |
vx = ar.x.toFixed(3); | |
vy = ar.y.toFixed(3); | |
} | |
if (b._relative) { | |
ux = (bl.x + b.x).toFixed(3); | |
uy = (bl.y + b.y).toFixed(3); | |
} else { | |
ux = bl.x.toFixed(3); | |
uy = bl.y.toFixed(3); | |
} | |
command = ((i === 0) ? Two.Commands.move : Two.Commands.curve) + | |
' ' + vx + ' ' + vy + ' ' + ux + ' ' + uy + ' ' + x + ' ' + y; | |
break; | |
case Two.Commands.move: | |
d = b; | |
command = Two.Commands.move + ' ' + x + ' ' + y; | |
break; | |
default: | |
command = b._command + ' ' + x + ' ' + y; | |
} | |
// Add a final point and close it off | |
if (i >= last && closed) { | |
if (b._command === Two.Commands.curve) { | |
// Make sure we close to the most previous Two.Commands.move | |
c = d; | |
br = (b.controls && b.controls.right) || b; | |
cl = (c.controls && c.controls.left) || c; | |
if (b._relative) { | |
vx = (br.x + b.x).toFixed(3); | |
vy = (br.y + b.y).toFixed(3); | |
} else { | |
vx = br.x.toFixed(3); | |
vy = br.y.toFixed(3); | |
} | |
if (c._relative) { | |
ux = (cl.x + c.x).toFixed(3); | |
uy = (cl.y + c.y).toFixed(3); | |
} else { | |
ux = cl.x.toFixed(3); | |
uy = cl.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(' '); | |
}, | |
group: { | |
// TODO: Can speed up. | |
appendChild: function(id) { | |
var elem = this.domElement.querySelector('#' + id); | |
if (elem) { | |
this.elem.appendChild(elem); | |
} | |
}, | |
// TODO: Can speed up. | |
removeChild: function(id) { | |
var elem = this.elem.querySelector('#' + id); | |
if (elem) { | |
this.elem.removeChild(elem); | |
} | |
}, | |
renderChild: function(child) { | |
svg[child._renderer.type].render.call(child, this); | |
}, | |
render: function(domElement) { | |
this._update(); | |
if (!this._renderer.elem) { | |
this._renderer.elem = svg.createElement('g', { | |
id: this.id | |
}); | |
domElement.appendChild(this._renderer.elem); | |
} | |
// _Update styles for the <g> | |
var flagMatrix = this._matrix.manual || this._flagMatrix; | |
var context = { | |
domElement: domElement, | |
elem: this._renderer.elem | |
}; | |
if (flagMatrix) { | |
this._renderer.elem.setAttribute('transform', 'matrix(' + this._matrix.toString() + ')'); | |
} | |
for (var id in this.children) { | |
svg.group.renderChild.call(domElement, this.children[id]); | |
} | |
if (this._flagAdditions) { | |
_.each(this.additions, svg.group.appendChild, context); | |
} | |
if (this._flagSubtractions) { | |
_.each(this.subtractions, svg.group.removeChild, context); | |
} | |
return this.flagReset(); | |
} | |
}, | |
polygon: { | |
render: function(domElement) { | |
this._update(); | |
if (!this._renderer.elem) { | |
this._renderer.elem = svg.createElement('path', { | |
id: this.id | |
}); | |
domElement.appendChild(this._renderer.elem); | |
} | |
var elem = this._renderer.elem; | |
var flagMatrix = this._matrix.manual || this._flagMatrix; | |
if (flagMatrix) { | |
elem.setAttribute('transform', 'matrix(' + this._matrix.toString() + ')'); | |
} | |
if (this._flagVertices) { | |
var vertices = svg.toString(this._vertices, this._closed); | |
elem.setAttribute('d', vertices); | |
} | |
if (this._flagFill) { | |
elem.setAttribute('fill', this._fill); | |
} | |
if (this._flagStroke) { | |
elem.setAttribute('stroke', this._stroke); | |
} | |
if (this._flagLinewidth) { | |
elem.setAttribute('stroke-width', this._linewidth); | |
} | |
if (this._flagOpacity) { | |
elem.setAttribute('stroke-opacity', this._opacity); | |
elem.setAttribute('fill-opacity', this._opacity); | |
} | |
if (this._flagVisible) { | |
elem.setAttribute('visibility', this._visible ? 'visible' : 'hidden'); | |
} | |
if (this._flagCap) { | |
elem.setAttribute('stroke-linecap', this._cap); | |
} | |
if (this._flagJoin) { | |
elem.setAttribute('stroke-linejoin', this._join); | |
} | |
if (this._flagMiter) { | |
elem.setAttribute('stroke-miterlimit', this.miter); | |
} | |
return this.flagReset(); | |
} | |
} | |
}; | |
/** | |
* @class | |
*/ | |
var Renderer = Two[Two.Types.svg] = function(params) { | |
this.domElement = params.domElement || svg.createElement('svg'); | |
this.scene = new Two.Group(); | |
this.scene._renderer.elem = this.domElement; | |
this.scene.parent = this; | |
}; | |
_.extend(Renderer, { | |
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; | |
}, | |
render: function() { | |
svg.group.render.call(this.scene, this.domElement); | |
return this; | |
} | |
}); | |
})(); | |
(function() { | |
/** | |
* Constants | |
*/ | |
var mod = Two.Utils.mod; | |
var getRatio = Two.Utils.getRatio; | |
var canvas = { | |
group: { | |
renderChild: function(child) { | |
canvas[child._renderer.type].render.call(child, this); | |
}, | |
render: function(ctx) { | |
// TODO: Add a check here to only invoke _update if need be. | |
this._update(); | |
var matrix = this._matrix.elements; | |
ctx.save(); | |
ctx.transform( | |
matrix[0], matrix[3], matrix[1], matrix[4], matrix[2], matrix[5]); | |
_.each(this.children, canvas.group.renderChild, ctx); | |
ctx.restore(); | |
return this.flagReset(); | |
} | |
}, | |
polygon: { | |
render: function(ctx) { | |
var matrix, stroke, linewidth, fill, opacity, visible, cap, join, miter, | |
closed, commands, length, last, next, prev, a, c, d, ux, uy, vx, vy, | |
ar, bl, br, cl, x, y; | |
// TODO: Add a check here to only invoke _update if need be. | |
this._update(); | |
matrix = this._matrix.elements; | |
stroke = this.stroke; | |
linewidth = this.linewidth; | |
fill = this.fill; | |
opacity = this.opacity; | |
visible = this.visible; | |
cap = this.cap; | |
join = this.join; | |
miter = this.miter; | |
closed = this.closed; | |
commands = this._vertices; // Commands | |
length = commands.length; | |
last = length - 1; | |
if (!visible) { | |
return this; | |
} | |
// Transform | |
ctx.save(); | |
if (matrix) { | |
ctx.transform( | |
matrix[0], matrix[3], matrix[1], matrix[4], matrix[2], matrix[5]); | |
} | |
// Styles | |
if (fill) { | |
ctx.fillStyle = fill; | |
} | |
if (stroke) { | |
ctx.strokeStyle = stroke; | |
} | |
if (linewidth) { | |
ctx.lineWidth = linewidth; | |
} | |
if (miter) { | |
ctx.miterLimit = miter; | |
} | |
if (join) { | |
ctx.lineJoin = join; | |
} | |
if (cap) { | |
ctx.lineCap = cap; | |
} | |
if (_.isNumber(opacity)) { | |
ctx.globalAlpha = opacity; | |
} | |
ctx.beginPath(); | |
commands.forEach(function(b, i) { | |
x = b.x.toFixed(3); | |
y = b.y.toFixed(3); | |
switch (b._command) { | |
case Two.Commands.close: | |
ctx.closePath(); | |
break; | |
case Two.Commands.curve: | |
prev = closed ? mod(i - 1, length) : Math.max(i - 1, 0); | |
next = closed ? mod(i + 1, length) : Math.min(i + 1, last); | |
a = commands[prev]; | |
c = commands[next]; | |
ar = (a.controls && a.controls.right) || a; | |
bl = (b.controls && b.controls.left) || b; | |
if (a._relative) { | |
vx = (ar.x + a.x).toFixed(3); | |
vy = (ar.y + a.y).toFixed(3); | |
} else { | |
vx = ar.x.toFixed(3); | |
vy = ar.y.toFixed(3); | |
} | |
if (b._relative) { | |
ux = (bl.x + b.x).toFixed(3); | |
uy = (bl.y + b.y).toFixed(3); | |
} else { | |
ux = bl.x.toFixed(3); | |
uy = bl.y.toFixed(3); | |
} | |
ctx.bezierCurveTo(vx, vy, ux, uy, x, y); | |
if (i >= last && closed) { | |
c = d; | |
br = (b.controls && b.controls.right) || b; | |
cl = (c.controls && c.controls.left) || c; | |
if (b._relative) { | |
vx = (br.x + b.x).toFixed(3); | |
vy = (br.y + b.y).toFixed(3); | |
} else { | |
vx = br.x.toFixed(3); | |
vy = br.y.toFixed(3); | |
} | |
if (c._relative) { | |
ux = (cl.x + c.x).toFixed(3); | |
uy = (cl.y + c.y).toFixed(3); | |
} else { | |
ux = cl.x.toFixed(3); | |
uy = cl.y.toFixed(3); | |
} | |
x = c.x.toFixed(3); | |
y = c.y.toFixed(3); | |
ctx.bezierCurveTo(vx, vy, ux, uy, x, y); | |
} | |
break; | |
case Two.Commands.line: | |
ctx.lineTo(x, y); | |
break; | |
case Two.Commands.move: | |
d = b; | |
ctx.moveTo(x, y); | |
break; | |
} | |
}); | |
// Loose ends | |
if (closed) { | |
ctx.closePath(); | |
} | |
ctx.fill(); | |
ctx.stroke(); | |
ctx.restore(); | |
return this.flagReset(); | |
} | |
} | |
}; | |
var Renderer = Two[Two.Types.canvas] = function(params) { | |
this.domElement = params.domElement || document.createElement('canvas'); | |
this.ctx = this.domElement.getContext('2d'); | |
this.overdraw = params.overdraw || false; | |
// Everything drawn on the canvas needs to be added to the scene. | |
this.scene = new Two.Group(); | |
this.scene.parent = this; | |
}; | |
_.extend(Renderer, { | |
Utils: canvas | |
}); | |
_.extend(Renderer.prototype, Backbone.Events, { | |
setSize: function(width, height, ratio) { | |
this.width = width; | |
this.height = height; | |
this.ratio = _.isUndefined(ratio) ? getRatio(this.ctx) : ratio; | |
this.domElement.width = width * this.ratio; | |
this.domElement.height = height * this.ratio; | |
_.extend(this.domElement.style, { | |
width: width + 'px', | |
height: height + 'px' | |
}); | |
return this; | |
}, | |
render: function() { | |
var isOne = this.ratio === 1; | |
if (!isOne) { | |
this.ctx.save(); | |
this.ctx.scale(this.ratio, this.ratio); | |
} | |
if (!this.overdraw) { | |
this.ctx.clearRect(0, 0, this.width, this.height); | |
} | |
canvas.group.render.call(this.scene, this.ctx); | |
if (!isOne) { | |
this.ctx.restore(); | |
} | |
return this; | |
} | |
}); | |
function resetTransform(ctx) { | |
ctx.setTransform(1, 0, 0, 1, 0, 0); | |
} | |
})(); | |
(function() { | |
/** | |
* Constants | |
*/ | |
var multiplyMatrix = Two.Matrix.Multiply, | |
mod = Two.Utils.mod, | |
identity = [1, 0, 0, 0, 1, 0, 0, 0, 1], | |
transformation = new Two.Array(9), | |
getRatio = Two.Utils.getRatio; | |
var webgl = { | |
canvas: document.createElement('canvas'), | |
uv: new Two.Array([ | |
0, 0, | |
1, 0, | |
0, 1, | |
0, 1, | |
1, 0, | |
1, 1 | |
]), | |
group: { | |
renderChild: function(child) { | |
webgl[child._renderer.type].render.call(child, this.gl, this.program); | |
}, | |
render: function(gl, program) { | |
this._update(); | |
var parent = this.parent; | |
var flagParentMatrix = (parent._matrix && parent._matrix.manual) || parent._flagMatrix; | |
var flagMatrix = this._matrix.manual || this._flagMatrix; | |
if (flagParentMatrix || flagMatrix) { | |
if (!this._renderer.matrix) { | |
this._renderer.matrix = new Two.Array(9); | |
} | |
// Reduce amount of object / array creation / deletion | |
this._matrix.toArray(true, transformation); | |
multiplyMatrix(transformation, parent._renderer.matrix, this._renderer.matrix); | |
this._renderer.scale = this._scale * parent._renderer.scale; | |
if (flagParentMatrix) { | |
this._flagMatrix = true; | |
} | |
} | |
_.each(this.children, webgl.group.renderChild, { | |
gl: gl, | |
program: program | |
}); | |
return this.flagReset(); | |
} | |
}, | |
polygon: { | |
render: function(gl, program) { | |
if (!this._visible || !this._opacity) { | |
return this; | |
} | |
// Calculate what changed | |
var parent = this.parent; | |
var flagParentMatrix = parent._matrix.manual || parent._flagMatrix; | |
var flagMatrix = this._matrix.manual || this._flagMatrix; | |
var flagTexture = this._flagVertices || this._flagFill | |
|| this._flagStroke || this._flagLinewidth || this._flagOpacity | |
|| this._flagVisible || this._flagCap || this._flagJoin | |
|| this._flagMiter || this._flagScale; | |
this._update(); | |
if (flagParentMatrix || flagMatrix) { | |
if (!this._renderer.matrix) { | |
this._renderer.matrix = new Two.Array(9); | |
} | |
// Reduce amount of object / array creation / deletion | |
this._matrix.toArray(true, transformation); | |
multiplyMatrix(transformation, parent._renderer.matrix, this._renderer.matrix); | |
this._renderer.scale = this._scale * parent._renderer.scale; | |
} | |
if (flagTexture) { | |
if (!this._renderer.rect) { | |
this._renderer.rect = {}; | |
} | |
if (!this._renderer.triangles) { | |
this._renderer.triangles = new Two.Array(12); | |
} | |
webgl.getBoundingClientRect(this._vertices, this._linewidth, this._renderer.rect); | |
webgl.getTriangles(this._renderer.rect, this._renderer.triangles); | |
webgl.updateBuffer(gl, this, program); | |
webgl.updateTexture(gl, this); | |
} | |
// Draw Texture | |
gl.bindBuffer(gl.ARRAY_BUFFER, this._renderer.textureCoordsBuffer); | |
gl.vertexAttribPointer(program.textureCoords, 2, gl.FLOAT, false, 0, 0); | |
gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture); | |
// Draw Rect | |
gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix); | |
gl.bindBuffer(gl.ARRAY_BUFFER, this._renderer.buffer); | |
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0); | |
gl.drawArrays(gl.TRIANGLES, 0, 6); | |
return this.flagReset(); | |
} | |
}, | |
/** | |
* Returns the rect of a set of verts. Typically takes vertices that are | |
* "centered" around 0 and returns them to be anchored upper-left. | |
*/ | |
getBoundingClientRect: function(vertices, border, rect) { | |
var left = Infinity, right = -Infinity, | |
top = Infinity, bottom = -Infinity, | |
width, height; | |
vertices.forEach(function(v) { | |
var x = v.x, y = v.y, controls = v.controls; | |
var a, b, c, d, cl, cr; | |
top = Math.min(y, top); | |
left = Math.min(x, left); | |
right = Math.max(x, right); | |
bottom = Math.max(y, bottom); | |
if (!v.controls) { | |
return; | |
} | |
cl = controls.left; | |
cr = controls.right; | |
if (!cl || !cr) { | |
return; | |
} | |
a = v._relative ? cl.x + x : cl.x; | |
b = v._relative ? cl.y + y : cl.y; | |
c = v._relative ? cr.x + x : cr.x; | |
d = v._relative ? cr.y + y : cr.y; | |
if (!a || !b || !c || !d) { | |
return; | |
} | |
top = Math.min(b, d, top); | |
left = Math.min(a, c, left); | |
right = Math.max(a, c, right); | |
bottom = Math.max(b, d, bottom); | |
}); | |
// Expand borders | |
if (_.isNumber(border)) { | |
top -= border; | |
left -= border; | |
right += border; | |
bottom += border; | |
} | |
width = right - left; | |
height = bottom - top; | |
rect.top = top; | |
rect.left = left; | |
rect.right = right; | |
rect.bottom = bottom; | |
rect.width = width; | |
rect.height = height; | |
if (!rect.centroid) { | |
rect.centroid = {}; | |
} | |
rect.centroid.x = - left; | |
rect.centroid.y = - top; | |
}, | |
getTriangles: function(rect, triangles) { | |
var top = rect.top, | |
left = rect.left, | |
right = rect.right, | |
bottom = rect.bottom; | |
// First Triangle | |
triangles[0] = left; | |
triangles[1] = top; | |
triangles[2] = right; | |
triangles[3] = top; | |
triangles[4] = left; | |
triangles[5] = bottom; | |
// Second Triangle | |
triangles[6] = left; | |
triangles[7] = bottom; | |
triangles[8] = right; | |
triangles[9] = top; | |
triangles[10] = right; | |
triangles[11] = bottom; | |
}, | |
updateCanvas: function(elem) { | |
var commands = elem._vertices; | |
var canvas = this.canvas; | |
var ctx = this.ctx; | |
// Styles | |
var scale = elem._renderer.scale; | |
var stroke = elem._stroke; | |
var linewidth = elem._linewidth * scale; | |
var fill = elem._fill; | |
var opacity = elem._opacity; | |
var cap = elem._cap; | |
var join = elem._join; | |
var miter = elem._miter; | |
var closed = elem._closed; | |
var length = commands.length; | |
var last = length - 1; | |
canvas.width = Math.max(Math.ceil(elem._renderer.rect.width * scale), 1); | |
canvas.height = Math.max(Math.ceil(elem._renderer.rect.height * scale), 1); | |
var centroid = elem._renderer.rect.centroid; | |
var cx = centroid.x * scale; | |
var cy = centroid.y * scale; | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
if (fill) { | |
ctx.fillStyle = fill; | |
} | |
if (stroke) { | |
ctx.strokeStyle = stroke; | |
} | |
if (linewidth) { | |
ctx.lineWidth = linewidth; | |
} | |
if (miter) { | |
ctx.miterLimit = miter; | |
} | |
if (join) { | |
ctx.lineJoin = join; | |
} | |
if (cap) { | |
ctx.lineCap = cap; | |
} | |
if (_.isNumber(opacity)) { | |
ctx.globalAlpha = opacity; | |
} | |
var d; | |
ctx.beginPath(); | |
commands.forEach(function(b, i) { | |
var next, prev, a, c, ux, uy, vx, vy, ar, bl, br, cl, x, y; | |
x = (b.x * scale + cx).toFixed(3); | |
y = (b.y * scale + cy).toFixed(3); | |
switch (b._command) { | |
case Two.Commands.close: | |
ctx.closePath(); | |
break; | |
case Two.Commands.curve: | |
prev = closed ? mod(i - 1, length) : Math.max(i - 1, 0); | |
next = closed ? mod(i + 1, length) : Math.min(i + 1, last); | |
a = commands[prev]; | |
c = commands[next]; | |
ar = (a.controls && a.controls.right) || a; | |
bl = (b.controls && b.controls.left) || b; | |
if (a._relative) { | |
vx = ((ar.x + a.x) * scale + cx).toFixed(3); | |
vy = ((ar.y + a.y) * scale + cy).toFixed(3); | |
} else { | |
vx = (ar.x * scale + cx).toFixed(3); | |
vy = (ar.y * scale + cy).toFixed(3); | |
} | |
if (b._relative) { | |
ux = ((bl.x + b.x) * scale + cx).toFixed(3); | |
uy = ((bl.y + b.y) * scale + cy).toFixed(3); | |
} else { | |
ux = (bl.x * scale + cx).toFixed(3); | |
uy = (bl.y * scale + cy).toFixed(3); | |
} | |
ctx.bezierCurveTo(vx, vy, ux, uy, x, y); | |
if (i >= last && closed) { | |
// FIXME: d is undefined here? | |
c = d; | |
br = (b.controls && b.controls.right) || b; | |
cl = (c.controls && c.controls.left) || c; | |
if (b._relative) { | |
vx = ((br.x + b.x) * scale + cx).toFixed(3); | |
vy = ((br.y + b.y) * scale + cy).toFixed(3); | |
} else { | |
vx = (br.x * scale + cx).toFixed(3); | |
vy = (br.y * scale + cy).toFixed(3); | |
} | |
if (c._relative) { | |
ux = ((cl.x + c.x) * scale + cx).toFixed(3); | |
uy = ((cl.y + c.y) * scale + cx).toFixed(3); | |
} else { | |
ux = (cl.x * scale + cx).toFixed(3); | |
uy = (cl.y * scale + cy).toFixed(3); | |
} | |
x = (c.x * scale + cx).toFixed(3); | |
y = (c.y * scale + cy).toFixed(3); | |
ctx.bezierCurveTo(vx, vy, ux, uy, x, y); | |
} | |
break; | |
case Two.Commands.line: | |
ctx.lineTo(x, y); | |
break; | |
case Two.Commands.move: | |
d = b; | |
ctx.moveTo(x, y); | |
break; | |
} | |
}); | |
// Loose ends | |
if (closed) { | |
ctx.closePath(); | |
} | |
ctx.fill(); | |
ctx.stroke(); | |
}, | |
updateTexture: function(gl, elem) { | |
this.updateCanvas(elem); | |
if (elem._renderer.texture) { | |
gl.deleteTexture(elem._renderer.texture); | |
} | |
gl.bindBuffer(gl.ARRAY_BUFFER, elem._renderer.textureCoordsBuffer); | |
elem._renderer.texture = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, elem._renderer.texture); | |
// Set the parameters so we can render any size image. | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); | |
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); | |
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
if (this.canvas.width <= 0 || this.canvas.height <= 0) { | |
return; | |
} | |
// Upload the image into the texture. | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.canvas); | |
}, | |
updateBuffer: function(gl, elem, program) { | |
if (_.isObject(elem._renderer.buffer)) { | |
gl.deleteBuffer(elem._renderer.buffer); | |
} | |
elem._renderer.buffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ARRAY_BUFFER, elem._renderer.buffer); | |
gl.enableVertexAttribArray(program.position); | |
gl.bufferData(gl.ARRAY_BUFFER, elem._renderer.triangles, gl.STATIC_DRAW); | |
if (_.isObject(elem._renderer.textureCoordsBuffer)) { | |
gl.deleteBuffer(elem._renderer.textureCoordsBuffer); | |
} | |
elem._renderer.textureCoordsBuffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ARRAY_BUFFER, elem._renderer.textureCoordsBuffer); | |
gl.enableVertexAttribArray(program.textureCoords); | |
gl.bufferData(gl.ARRAY_BUFFER, this.uv, gl.STATIC_DRAW); | |
}, | |
program: { | |
create: function(gl, shaders) { | |
var program, linked, error; | |
program = gl.createProgram(); | |
_.each(shaders, function(s) { | |
gl.attachShader(program, s); | |
}); | |
gl.linkProgram(program); | |
linked = gl.getProgramParameter(program, gl.LINK_STATUS); | |
if (!linked) { | |
error = gl.getProgramInfoLog(program); | |
gl.deleteProgram(program); | |
throw new Two.Utils.Error('unable to link program: ' + error); | |
} | |
return program; | |
} | |
}, | |
shaders: { | |
create: function(gl, source, type) { | |
var shader, compiled, error; | |
shader = gl.createShader(gl[type]); | |
gl.shaderSource(shader, source); | |
gl.compileShader(shader); | |
compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); | |
if (!compiled) { | |
error = gl.getShaderInfoLog(shader); | |
gl.deleteShader(shader); | |
throw new Two.Utils.Error('unable to compile shader ' + shader + ': ' + error); | |
} | |
return shader; | |
}, | |
types: { | |
vertex: 'VERTEX_SHADER', | |
fragment: 'FRAGMENT_SHADER' | |
}, | |
vertex: [ | |
'attribute vec2 a_position;', | |
'attribute vec2 a_textureCoords;', | |
'', | |
'uniform mat3 u_matrix;', | |
'uniform vec2 u_resolution;', | |
'', | |
'varying vec2 v_textureCoords;', | |
'', | |
'void main() {', | |
' vec2 projected = (u_matrix * vec3(a_position, 1.0)).xy;', | |
' vec2 normal = projected / u_resolution;', | |
' vec2 clipspace = (normal * 2.0) - 1.0;', | |
'', | |
' gl_Position = vec4(clipspace * vec2(1.0, -1.0), 0.0, 1.0);', | |
' v_textureCoords = a_textureCoords;', | |
'}' | |
].join('\n'), | |
fragment: [ | |
'precision mediump float;', | |
'', | |
'uniform sampler2D u_image;', | |
'varying vec2 v_textureCoords;', | |
'', | |
'void main() {', | |
' gl_FragColor = texture2D(u_image, v_textureCoords);', | |
'}' | |
].join('\n') | |
} | |
}; | |
webgl.ctx = webgl.canvas.getContext('2d'); | |
var Renderer = Two[Two.Types.webgl] = function(options) { | |
var params, gl, vs, fs; | |
this.domElement = options.domElement || document.createElement('canvas'); | |
// Everything drawn on the canvas needs to come from the stage. | |
this.scene = new Two.Group(); | |
this.scene.parent = this; | |
this._renderer = { | |
matrix: new Two.Array(identity), | |
scale: 1 | |
}; | |
this._flagMatrix = true; | |
// http://games.greggman.com/game/webgl-and-alpha/ | |
// http://www.khronos.org/registry/webgl/specs/latest/#5.2 | |
params = _.defaults(options || {}, { | |
antialias: false, | |
alpha: true, | |
premultipliedAlpha: true, | |
stencil: true, | |
preserveDrawingBuffer: true, | |
overdraw: false | |
}); | |
this.overdraw = params.overdraw; | |
gl = this.ctx = this.domElement.getContext('webgl', params) || | |
this.domElement.getContext('experimental-webgl', params); | |
if (!this.ctx) { | |
throw new Two.Utils.Error( | |
'unable to create a webgl context. Try using another renderer.'); | |
} | |
// Compile Base Shaders to draw in pixel space. | |
vs = webgl.shaders.create( | |
gl, webgl.shaders.vertex, webgl.shaders.types.vertex); | |
fs = webgl.shaders.create( | |
gl, webgl.shaders.fragment, webgl.shaders.types.fragment); | |
this.program = webgl.program.create(gl, [vs, fs]); | |
gl.useProgram(this.program); | |
// Create and bind the drawing buffer | |
// look up where the vertex data needs to go. | |
this.program.position = gl.getAttribLocation(this.program, 'a_position'); | |
this.program.matrix = gl.getUniformLocation(this.program, 'u_matrix'); | |
this.program.textureCoords = gl.getAttribLocation(this.program, 'a_textureCoords'); | |
// Copied from Three.js WebGLRenderer | |
gl.disable(gl.DEPTH_TEST); | |
// Setup some initial statements of the gl context | |
gl.enable(gl.BLEND); | |
// https://code.google.com/p/chromium/issues/detail?id=316393 | |
// gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, gl.TRUE); | |
gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); | |
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, | |
gl.ONE, gl.ONE_MINUS_SRC_ALPHA ); | |
}; | |
_.extend(Renderer.prototype, Backbone.Events, { | |
setSize: function(width, height, ratio) { | |
this.width = width; | |
this.height = height; | |
this.ratio = _.isUndefined(ratio) ? getRatio(this.ctx) : ratio; | |
this.domElement.width = width * this.ratio; | |
this.domElement.height = height * this.ratio; | |
_.extend(this.domElement.style, { | |
width: width + 'px', | |
height: height + 'px' | |
}); | |
width *= this.ratio; | |
height *= this.ratio; | |
// Set for this.stage parent scaling to account for HDPI | |
this._renderer.matrix[0] = this._renderer.matrix[4] = this._renderer.scale = this.ratio; | |
this._flagMatrix = true; | |
this.ctx.viewport(0, 0, width, height); | |
var resolutionLocation = this.ctx.getUniformLocation( | |
this.program, 'u_resolution'); | |
this.ctx.uniform2f(resolutionLocation, width, height); | |
return this; | |
}, | |
render: function() { | |
var gl = this.ctx; | |
if (!this.overdraw) { | |
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); | |
} | |
webgl.group.render.call(this.scene, gl, this.program); | |
this._flagMatrix = false; | |
return this; | |
} | |
}); | |
})(); | |
(function() { | |
var Shape = Two.Shape = function() { | |
// Private object for renderer specific variables. | |
this._renderer = {}; | |
this.id = Two.Identifier + Two.uniqueId(); | |
this.classList = []; | |
// Define matrix properties which all inherited | |
// objects of Shape have. | |
this._matrix = new Two.Matrix(); | |
this.translation = new Two.Vector(); | |
this.translation.bind(Two.Events.change, _.bind(Shape.FlagMatrix, this)); | |
this.rotation = 0; | |
this.scale = 1; | |
}; | |
_.extend(Shape, Backbone.Events, { | |
FlagMatrix: function() { | |
this._flagMatrix = true; | |
}, | |
MakeObservable: function(object) { | |
Object.defineProperty(object, 'rotation', { | |
get: function() { | |
return this._rotation; | |
}, | |
set: function(v) { | |
this._rotation = v; | |
this._flagMatrix = true; | |
} | |
}); | |
Object.defineProperty(object, 'scale', { | |
get: function() { | |
return this._scale; | |
}, | |
set: function(v) { | |
this._scale = v; | |
this._flagMatrix = true; | |
this._flagScale = true; | |
} | |
}); | |
} | |
}); | |
_.extend(Shape.prototype, { | |
// Flags | |
_flagMatrix: true, | |
// Underlying Properties | |
_rotation: 0, | |
_scale: 1, | |
addTo: function(group) { | |
group.add(this); | |
return this; | |
}, | |
clone: function() { | |
var clone = new Shape(); | |
clone.translation.copy(this.translation); | |
clone.rotation = this.rotation; | |
clone.scale = this.scale; | |
_.each(Shape.Properties, function(k) { | |
clone[k] = this[k]; | |
}, this); | |
return clone._update(); | |
}, | |
/** | |
* Set the parent of this object to another object | |
* and updates parent-child relationships | |
* Calling with no arguments will simply remove the parenting | |
*/ | |
replaceParent: function(newParent) { | |
var id = this.id, index; | |
// Release object from previous parent. | |
if (this.parent) { | |
delete this.parent.children[id]; | |
index = _.indexOf(parent.additions, id); | |
if (index >= 0) { | |
this.parent.additions.splice(index, 1); | |
} | |
this.parent.subtractions.push(id); | |
this._flagSubtractions = true; | |
} | |
if (newParent) { | |
// Add it to this group and update parent-child relationship. | |
newParent.children[id] = this; | |
this.parent = newParent; | |
newParent.additions.push(id); | |
newParent._flagAdditions = true; | |
} else { | |
delete this.parent; | |
} | |
return this; | |
}, | |
/** | |
* To be called before render that calculates and collates all information | |
* to be as up-to-date as possible for the render. Called once a frame. | |
*/ | |
_update: function() { | |
if (!this._matrix.manual && this._flagMatrix) { | |
this._matrix | |
.identity() | |
.translate(this.translation.x, this.translation.y) | |
.scale(this.scale) | |
.rotate(this.rotation); | |
} | |
// Bubble up to parents mainly for `getBoundingClientRect` method. | |
if (this.parent && this.parent._update) { | |
this.parent._update(); | |
} | |
return this; | |
}, | |
flagReset: function() { | |
this._flagMatrix = false; | |
this._flagScale = false; | |
return this; | |
} | |
}); | |
Shape.MakeObservable(Shape.prototype); | |
})(); | |
(function() { | |
/** | |
* Constants | |
*/ | |
var min = Math.min, max = Math.max, round = Math.round, | |
getComputedMatrix = Two.Utils.getComputedMatrix; | |
var commands = {}; | |
_.each(Two.Commands, function(v, k) { | |
commands[k] = new RegExp(v); | |
}); | |
var Polygon = Two.Polygon = function(vertices, closed, curved, manual) { | |
Two.Shape.call(this); | |
this._renderer.type = 'polygon'; | |
this._closed = !!closed; | |
this._curved = !!curved; | |
this.beginning = 0; | |
this.ending = 1; | |
// Style properties | |
this.fill = '#fff'; | |
this.stroke = '#000'; | |
this.linewidth = 1.0; | |
this.opacity = 1.0; | |
this.visible = true; | |
this.cap = 'butt'; // Default of Adobe Illustrator | |
this.join = 'miter'; // Default of Adobe Illustrator | |
this.miter = 4; // Default of Adobe Illustrator | |
this._vertices = []; | |
this.vertices = vertices; | |
// Determines whether or not two.js should calculate curves, lines, and | |
// commands automatically for you or to let the developer manipulate them | |
// for themselves. | |
this.automatic = !manual; | |
}; | |
_.extend(Polygon, { | |
Properties: [ | |
'fill', | |
'stroke', | |
'linewidth', | |
'opacity', | |
'visible', | |
'cap', | |
'join', | |
'miter', // Order matters here! See LN:388 | |
'closed', | |
'curved', | |
'automatic', | |
'beginning', | |
'ending' | |
], | |
FlagVertices: function() { | |
this._flagVertices = true; | |
this._flagLength = true; | |
}, | |
MakeObservable: function(object) { | |
Two.Shape.MakeObservable(object); | |
// Only the first 8 properties are flagged like this. The subsequent | |
// properties behave differently and need to be hand written. | |
_.each(Polygon.Properties.slice(0, 8), function(property) { | |
var secret = '_' + property; | |
var flag = '_flag' + property.charAt(0).toUpperCase() + property.slice(1); | |
Object.defineProperty(object, property, { | |
get: function() { | |
return this[secret]; | |
}, | |
set: function(v) { | |
this[secret] = v; | |
this[flag] = true; | |
} | |
}); | |
}); | |
Object.defineProperty(object, 'length', { | |
get: function() { | |
if (this._flagLength) { | |
this._updateLength(); | |
} | |
return this._length; | |
} | |
}); | |
Object.defineProperty(object, 'closed', { | |
get: function() { | |
return this._closed; | |
}, | |
set: function(v) { | |
this._closed = !!v; | |
this._flagVertices = true; | |
} | |
}); | |
Object.defineProperty(object, 'curved', { | |
get: function() { | |
return this._curved; | |
}, | |
set: function(v) { | |
this._curved = !!v; | |
this._flagVertices = true; | |
} | |
}); | |
Object.defineProperty(Polygon.prototype, 'automatic', { | |
get: function() { | |
return this._automatic; | |
}, | |
set: function(v) { | |
if (v === this._automatic) { | |
return; | |
} | |
this._automatic = !!v; | |
var method = this._automatic ? 'ignore' : 'listen'; | |
_.each(this.vertices, function(v) { | |
v[method](); | |
}); | |
} | |
}); | |
Object.defineProperty(object, 'beginning', { | |
get: function() { | |
return this._beginning; | |
}, | |
set: function(v) { | |
this._beginning = min(max(v, 0.0), this._ending); | |
this._flagVertices = true; | |
} | |
}); | |
Object.defineProperty(object, 'ending', { | |
get: function() { | |
return this._ending; | |
}, | |
set: function(v) { | |
this._ending = min(max(v, this._beginning), 1.0); | |
this._flagVertices = true; | |
} | |
}); | |
Object.defineProperty(object, 'vertices', { | |
get: function() { | |
return this._collection; | |
}, | |
set: function(vertices) { | |
var updateVertices = _.bind(Polygon.FlagVertices, this); | |
var bindVerts = _.bind(function(items) { | |
_.each(items, function(v) { | |
v.bind(Two.Events.change, updateVertices); | |
}, this); | |
updateVertices(); | |
}, this); | |
var unbindVerts = _.bind(function(items) { | |
_.each(items, function(v) { | |
v.unbind(Two.Events.change, updateVertices); | |
}, this); | |
updateVertices(); | |
}, this); | |
// Remove previous listeners | |
if (this._collection) { | |
this._collection.unbind(); | |
} | |
// Create new Collection with copy of vertices | |
this._collection = new Two.Utils.Collection(vertices.slice(0)); | |
// Listen for Collection changes and bind / unbind | |
this._collection.bind(Two.Events.insert, bindVerts); | |
this._collection.bind(Two.Events.remove, unbindVerts); | |
// Bind Initial Vertices | |
bindVerts(this._collection); | |
} | |
}); | |
} | |
}); | |
_.extend(Polygon.prototype, Two.Shape.prototype, { | |
// Flags | |
// http://en.wikipedia.org/wiki/Flag | |
_flagVertices: true, | |
_flagLength: true, | |
_flagFill: true, | |
_flagStroke: true, | |
_flagLinewidth: true, | |
_flagOpacity: true, | |
_flagVisible: true, | |
_flagCap: true, | |
_flagJoin: true, | |
_flagMiter: true, | |
// Underlying Properties | |
_length: 0, | |
_fill: '#fff', | |
_stroke: '#000', | |
_linewidth: 1.0, | |
_opacity: 1.0, | |
_visible: true, | |
_cap: 'round', | |
_join: 'round', | |
_miter: 4, | |
_closed: true, | |
_curved: false, | |
_automatic: true, | |
_beginning: 0, | |
_ending: 1.0, | |
clone: function(parent) { | |
parent = parent || this.parent; | |
var points = _.map(this.vertices, function(v) { | |
return v.clone(); | |
}); | |
var clone = new Polygon(points, this.closed, this.curved, !this.automatic); | |
_.each(Two.Shape.Properties, function(k) { | |
clone[k] = this[k]; | |
}, this); | |
clone.translation.copy(this.translation); | |
clone.rotation = this.rotation; | |
clone.scale = this.scale; | |
parent.add(clone); | |
return clone; | |
}, | |
toObject: function() { | |
var result = { | |
vertices: _.map(this.vertices, function(v) { | |
return v.toObject(); | |
}) | |
}; | |
_.each(Two.Shape.Properties, function(k) { | |
result[k] = this[k]; | |
}, this); | |
result.translation = this.translation.toObject; | |
result.rotation = this.rotation; | |
result.scale = this.scale; | |
return result; | |
}, | |
noFill: function() { | |
this.fill = 'transparent'; | |
return this; | |
}, | |
noStroke: function() { | |
this.stroke = 'transparent'; | |
return this; | |
}, | |
/** | |
* Orient the vertices of the shape to the upper lefthand | |
* corner of the polygon. | |
*/ | |
corner: function() { | |
var rect = this.getBoundingClientRect(true); | |
rect.centroid = { | |
x: rect.left + rect.width / 2, | |
y: rect.top + rect.height / 2 | |
}; | |
_.each(this.vertices, function(v) { | |
v.addSelf(rect.centroid); | |
}); | |
return this; | |
}, | |
/** | |
* Orient the vertices of the shape to the center of the | |
* polygon. | |
*/ | |
center: function() { | |
var rect = this.getBoundingClientRect(true); | |
rect.centroid = { | |
x: rect.left + rect.width / 2, | |
y: rect.top + rect.height / 2 | |
}; | |
_.each(this.vertices, function(v) { | |
v.subSelf(rect.centroid); | |
}); | |
// this.translation.addSelf(rect.centroid); | |
return this; | |
}, | |
/** | |
* Remove self from the scene / parent. | |
*/ | |
remove: function() { | |
if (!this.parent) { | |
return this; | |
} | |
this.parent.remove(this); | |
return this; | |
}, | |
/** | |
* Return an object with top, left, right, bottom, width, and height | |
* parameters of the group. | |
*/ | |
getBoundingClientRect: function(shallow) { | |
// TODO: Update this to not __always__ update. Just when it needs to. | |
this._update(); | |
var matrix = !!shallow ? this._matrix : getComputedMatrix(this); | |
var border = this.linewidth / 2, x, y; | |
var left = Infinity, right = -Infinity, | |
top = Infinity, bottom = -Infinity; | |
_.each(this._vertices, function(v) { | |
x = v.x; | |
y = v.y; | |
v = matrix.multiply(x, y , 1); | |
top = min(v.y - border, top); | |
left = min(v.x - border, left); | |
right = max(v.x + border, right); | |
bottom = max(v.y + border, bottom); | |
}); | |
return { | |
top: top, | |
left: left, | |
right: right, | |
bottom: bottom, | |
width: right - left, | |
height: bottom - top | |
}; | |
}, | |
/** | |
* Given a float `t` from 0 to 1, return a point or assign a passed `obj`'s | |
* coordinates to that percentage on this Two.Polygon's curve. | |
*/ | |
getPointAt: function(t, obj) { | |
var x, x1, x2, x3, x4, y, y1, y2, y3, y4, left, right; | |
var target = this.length * Math.min(Math.max(t, 0), 1); | |
var length = this.vertices.length; | |
var last = length - 1; | |
var a = null; | |
var b = null; | |
for (var i = 0, l = this._lengths.length, sum = 0; i < l; i++) { | |
if (sum + this._lengths[i] > target) { | |
a = this.vertices[this.closed ? Two.Utils.mod(i, length) : i]; | |
b = this.vertices[Math.min(Math.max(i - 1, 0), last)]; | |
target -= sum; | |
t = target / this._lengths[i]; | |
break; | |
} | |
sum += this._lengths[i]; | |
} | |
if (_.isNull(a) || _.isNull(b)) { | |
return null; | |
} | |
right = b.controls && b.controls.right; | |
left = a.controls && a.controls.left; | |
x1 = b.x; | |
y1 = b.y; | |
x2 = (right || b).x; | |
y2 = (right || b).y; | |
x3 = (left || a).x; | |
y3 = (left || a).y; | |
x4 = a.x; | |
y4 = a.y; | |
if (right && b._relative) { | |
x2 += b.x; | |
y2 += b.y; | |
} | |
if (left && a._relative) { | |
x3 += a.x; | |
y3 += a.y; | |
} | |
x = Two.Utils.getPointOnCubicBezier(t, x1, x2, x3, x4); | |
y = Two.Utils.getPointOnCubicBezier(t, y1, y2, y3, y4); | |
if (_.isObject(obj)) { | |
obj.x = x; | |
obj.y = y; | |
return obj; | |
} | |
return new Two.Vector(x, y); | |
}, | |
/** | |
* Based on closed / curved and sorting of vertices plot where all points | |
* should be and where the respective handles should be too. | |
*/ | |
plot: function() { | |
if (this.curved) { | |
Two.Utils.getCurveFromPoints(this._vertices, this.closed); | |
return this; | |
} | |
_.each(this._vertices, function(p, i) { | |
p._command = i === 0 ? Two.Commands.move : Two.Commands.line; | |
}, this); | |
return this; | |
}, | |
subdivide: function(limit) { | |
//TODO: DRYness (function below) | |
this._update(); | |
var last = this.vertices.length - 1; | |
var b = this.vertices[last]; | |
var closed = this._closed || this.vertices[last]._command === Two.Commands.close; | |
var points = []; | |
_.each(this.vertices, function(a, i) { | |
if (i <= 0 && !closed) { | |
b = a; | |
return; | |
} | |
if (a.command === Two.Commands.move) { | |
points.push(new Two.Anchor(b.x, b.y)); | |
if (i > 0) { | |
points[points.length - 1].command = Two.Commands.line; | |
} | |
b = a; | |
return; | |
} | |
var verts = getSubdivisions(a, b, limit); | |
points = points.concat(verts); | |
// Assign commands to all the verts | |
_.each(verts, function(v, i) { | |
if (i <= 0 && b.command === Two.Commands.move) { | |
v.command = Two.Commands.move; | |
} else { | |
v.command = Two.Commands.line; | |
} | |
}); | |
if (i >= last) { | |
// TODO: Add check if the two vectors in question are the same values. | |
if (this._closed && this._automatic) { | |
b = a; | |
verts = getSubdivisions(a, b, limit); | |
points = points.concat(verts); | |
// Assign commands to all the verts | |
_.each(verts, function(v, i) { | |
if (i <= 0 && b.command === Two.Commands.move) { | |
v.command = Two.Commands.move; | |
} else { | |
v.command = Two.Commands.line; | |
} | |
}); | |
} else if (closed) { | |
points.push(new Two.Anchor(a.x, a.y)); | |
} | |
points[points.length - 1].command = closed ? Two.Commands.close : Two.Commands.line; | |
} | |
b = a; | |
}, this); | |
this._automatic = false; | |
this._curved = false; | |
this.vertices = points; | |
return this; | |
}, | |
_updateLength: function(limit) { | |
//TODO: DRYness (function above) | |
this._update(); | |
var last = this.vertices.length - 1; | |
var b = this.vertices[last]; | |
var closed = this._closed || this.vertices[last]._command === Two.Commands.close; | |
var sum = 0; | |
if (_.isUndefined(this._lengths)) { | |
this._lengths = []; | |
} | |
_.each(this.vertices, function(a, i) { | |
if ((i <= 0 && !closed) || a.command === Two.Commands.move) { | |
b = a; | |
this._lengths[i] = 0; | |
return; | |
} | |
this._lengths[i] = getCurveLength(a, b, limit); | |
sum += this._lengths[i]; | |
if (i >= last && closed) { | |
b = a; | |
this._lengths[i + 1] = getCurveLength(a, b, limit); | |
sum += this._lengths[i + 1]; | |
} | |
b = a; | |
}, this); | |
this._length = sum; | |
return this; | |
}, | |
_update: function() { | |
if (this._flagVertices) { | |
var l = this.vertices.length; | |
var last = l - 1, v; | |
var ia = round((this._beginning) * last); | |
var ib = round((this._ending) * last); | |
this._vertices.length = 0; | |
for (var i = ia; i < ib + 1; i++) { | |
v = this.vertices[i]; | |
this._vertices.push(v); | |
} | |
if (this._automatic) { | |
this.plot(); | |
} | |
} | |
Two.Shape.prototype._update.call(this); | |
return this; | |
}, | |
flagReset: function() { | |
this._flagVertices = this._flagFill = this._flagStroke = | |
this._flagLinewidth = this._flagOpacity = this._flagVisible = | |
this._flagCap = this._flagJoin = this._flagMiter = false; | |
Two.Shape.prototype.flagReset.call(this); | |
return this; | |
} | |
}); | |
Polygon.MakeObservable(Polygon.prototype); | |
function getCurveLength(a, b, limit) { | |
// TODO: DRYness | |
var x1, x2, x3, x4, y1, y2, y3, y4; | |
var right = b.controls && b.controls.right; | |
var left = a.controls && a.controls.left; | |
x1 = b.x; | |
y1 = b.y; | |
x2 = (right || b).x; | |
y2 = (right || b).y; | |
x3 = (left || a).x; | |
y3 = (left || a).y; | |
x4 = a.x; | |
y4 = a.y; | |
if (right && b._relative) { | |
x2 += b.x; | |
y2 += b.y; | |
} | |
if (left && a._relative) { | |
x3 += a.x; | |
y3 += a.y; | |
} | |
return Two.Utils.getCurveLength(x1, y1, x2, y2, x3, y3, x4, y4, limit); | |
} | |
function getSubdivisions(a, b, limit) { | |
// TODO: DRYness | |
var x1, x2, x3, x4, y1, y2, y3, y4; | |
var right = b.controls && b.controls.right; | |
var left = a.controls && a.controls.left; | |
x1 = b.x; | |
y1 = b.y; | |
x2 = (right || b).x; | |
y2 = (right || b).y; | |
x3 = (left || a).x; | |
y3 = (left || a).y; | |
x4 = a.x; | |
y4 = a.y; | |
if (right && b._relative) { | |
x2 += b.x; | |
y2 += b.y; | |
} | |
if (left && a._relative) { | |
x3 += a.x; | |
y3 += a.y; | |
} | |
return Two.Utils.subdivide(x1, y1, x2, y2, x3, y3, x4, y4, limit); | |
} | |
})(); | |
(function() { | |
/** | |
* Constants | |
*/ | |
var min = Math.min, max = Math.max; | |
var Group = Two.Group = function() { | |
Two.Shape.call(this, true); | |
this._renderer.type = 'group'; | |
this.additions = []; | |
this.subtractions = []; | |
this.children = {}; | |
}; | |
_.extend(Group, { | |
MakeObservable: function(object) { | |
Two.Shape.MakeObservable(object); | |
Group.MakeGetterSetters(object, Two.Polygon.Properties); | |
}, | |
MakeGetterSetters: function(group, properties) { | |
if (!_.isArray(properties)) { | |
properties = [properties]; | |
} | |
_.each(properties, function(k) { | |
Group.MakeGetterSetter(group, k); | |
}); | |
}, | |
MakeGetterSetter: function(group, k) { | |
var secret = '_' + k; | |
Object.defineProperty(group, k, { | |
get: function() { | |
return this[secret]; | |
}, | |
set: function(v) { | |
this[secret] = v; | |
_.each(this.children, function(child) { // Trickle down styles | |
child[k] = v; | |
}); | |
} | |
}); | |
} | |
}); | |
_.extend(Group.prototype, Two.Shape.prototype, { | |
// Flags | |
// http://en.wikipedia.org/wiki/Flag | |
_flagAdditions: false, | |
_flagSubtractions: false, | |
// Underlying Properties | |
_fill: '#fff', | |
_stroke: '#000', | |
_linewidth: 1.0, | |
_opacity: 1.0, | |
_visible: true, | |
_cap: 'round', | |
_join: 'round', | |
_miter: 4, | |
_closed: true, | |
_curved: false, | |
_automatic: true, | |
_beginning: 0, | |
_ending: 1.0, | |
/** | |
* Group has a gotcha in that it's at the moment required to be bound to | |
* an instance of two in order to add elements correctly. This needs to | |
* be rethought and fixed. | |
*/ | |
clone: function(parent) { | |
parent = parent || this.parent; | |
var group = new Group(); | |
parent.add(group); | |
var children = _.map(this.children, function(child) { | |
return child.clone(group); | |
}); | |
group.translation.copy(this.translation); | |
group.rotation = this.rotation; | |
group.scale = this.scale; | |
return group; | |
}, | |
toObject: function() { | |
var result = { | |
children: {}, | |
translation: this.translation.toObject(), | |
rotation: this.rotation, | |
scale: this.scale | |
}; | |
_.each(this.children, function(child, i) { | |
result.children[i] = child.toObject(); | |
}, this); | |
return result; | |
}, | |
/** | |
* Anchor all children to the upper left hand corner | |
* of the group. | |
*/ | |
corner: function() { | |
var rect = this.getBoundingClientRect(true), | |
corner = { x: rect.left, y: rect.top }; | |
_.each(this.children, function(child) { | |
child.translation.subSelf(corner); | |
}); | |
return this; | |
}, | |
/** | |
* Anchors all children around the center of the group, | |
* effectively placing the shape around the unit circle. | |
*/ | |
center: function() { | |
var rect = this.getBoundingClientRect(true); | |
rect.centroid = { | |
x: rect.left + rect.width / 2, | |
y: rect.top + rect.height / 2 | |
}; | |
_.each(this.children, function(child) { | |
child.translation.subSelf(rect.centroid); | |
}); | |
// this.translation.copy(rect.centroid); | |
return this; | |
}, | |
/** | |
* Recursively search for id. Returns the first element found. | |
* Returns null if none found. | |
*/ | |
getById: function (id) { | |
var search = function (node, id) { | |
if (node.id === id) { | |
return node; | |
} | |
for (var child in node.children) { | |
var found = search(node.children[child], id); | |
if (found) return found; | |
} | |
}; | |
return search(this, id) || null; | |
}, | |
/** | |
* Recursively search for classes. Returns an array of matching elements. | |
* Empty array if none found. | |
*/ | |
getByClassName: function (cl) { | |
var found = []; | |
var search = function (node, cl) { | |
if (node.classList.indexOf(cl) != -1) { | |
found.push(node); | |
} | |
for (var child in node.children) { | |
search(node.children[child], cl); | |
} | |
return found; | |
}; | |
return search(this, cl); | |
}, | |
/** | |
* Recursively search for children of a specific type, | |
* e.g. Two.Polygon. Pass a reference to this type as the param. | |
* Returns an empty array if none found. | |
*/ | |
getByType: function(type) { | |
var found = []; | |
var search = function (node, type) { | |
for (var id in node.children) { | |
if (node.children[id] instanceof type) { | |
found.push(node.children[id]); | |
} else if (node.children[id] instanceof Two.Group) { | |
search(node.children[id], type); | |
} | |
} | |
return found; | |
}; | |
return search(this, type); | |
}, | |
/** | |
* Add objects to the group. | |
*/ | |
add: function(objects) { | |
var l = arguments.length, | |
children = this.children, | |
grandparent = this.parent, | |
ids = this.additions, | |
id, parent, index; | |
if (!_.isArray(objects)) { | |
objects = _.toArray(arguments); | |
} | |
// Add the objects | |
_.each(objects, function(object) { | |
if (!object) { | |
return; | |
} | |
id = object.id; | |
parent = object.parent; | |
if (_.isUndefined(children[id])) { | |
// Release object from previous parent. | |
if (parent) { | |
delete parent.children[id]; | |
index = _.indexOf(parent.additions, id); | |
if (index >= 0) { | |
parent.additions.splice(index, 1); | |
} | |
} | |
// Add it to this group and update parent-child relationship. | |
children[id] = object; | |
object.parent = this; | |
ids.push(id); | |
this._flagAdditions = true; | |
} | |
}, this); | |
return this; | |
}, | |
/** | |
* Remove objects from the group. | |
*/ | |
remove: function(objects) { | |
var l = arguments.length, | |
children = this.children, | |
grandparent = this.parent, | |
ids = this.subtractions, | |
id, parent, index, grandchildren; | |
if (l <= 0 && grandparent) { | |
grandparent.remove(this); | |
return this; | |
} | |
if (!_.isArray(objects)) { | |
objects = _.toArray(arguments); | |
} | |
_.each(objects, function(object) { | |
id = object.id; | |
grandchildren = object.children; | |
parent = object.parent; | |
if (!(id in children)) { | |
return; | |
} | |
delete children[id]; | |
delete object.parent; | |
index = _.indexOf(parent.additions, id); | |
if (index >= 0) { | |
parent.additions.splice(index, 1); | |
} | |
ids.push(id); | |
this._flagSubtractions = true; | |
}, this); | |
return this; | |
}, | |
/** | |
* Return an object with top, left, right, bottom, width, and height | |
* parameters of the group. | |
*/ | |
getBoundingClientRect: function() { | |
var rect; | |
// TODO: Update this to not __always__ update. Just when it needs to. | |
this._update(); | |
// Variables need to be defined here, because of nested nature of groups. | |
var left = Infinity, right = -Infinity, | |
top = Infinity, bottom = -Infinity; | |
_.each(this.children, function(child) { | |
rect = child.getBoundingClientRect(); | |
if (!_.isNumber(rect.top) || !_.isNumber(rect.left) || | |
!_.isNumber(rect.right) || !_.isNumber(rect.bottom)) { | |
return; | |
} | |
top = min(rect.top, top); | |
left = min(rect.left, left); | |
right = max(rect.right, right); | |
bottom = max(rect.bottom, bottom); | |
}, this); | |
return { | |
top: top, | |
left: left, | |
right: right, | |
bottom: bottom, | |
width: right - left, | |
height: bottom - top | |
}; | |
}, | |
/** | |
* Trickle down of noFill | |
*/ | |
noFill: function() { | |
_.each(this.children, function(child) { | |
child.noFill(); | |
}); | |
return this; | |
}, | |
/** | |
* Trickle down of noStroke | |
*/ | |
noStroke: function() { | |
_.each(this.children, function(child) { | |
child.noStroke(); | |
}); | |
return this; | |
}, | |
/** | |
* Trickle down subdivide | |
*/ | |
subdivide: function() { | |
var args = arguments; | |
_.each(this.children, function(child) { | |
child.subdivide.apply(child, args); | |
}); | |
return this; | |
}, | |
flagReset: function() { | |
if (this._flagAdditions) { | |
this.additions.length = 0; | |
this._flagAdditions = false; | |
} | |
if (this._flagSubtractions) { | |
this.subtractions.length = 0; | |
this._flagSubtractions = false; | |
} | |
Two.Shape.prototype.flagReset.call(this); | |
return this; | |
} | |
}); | |
Group.MakeObservable(Group.prototype); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment