/** * 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 = {}, i, key, value, attr; // Not available in non browser environments if (getComputedStyle) { // Convert CSSStyleDeclaration to a normal object var computedStyles = getComputedStyle(node); i = computedStyles.length; while(i--) { key = computedStyles[i]; value = computedStyles[key]; // Gecko returns undefined for unset properties // Webkit returns the default value if (value !== undefined) { styles[key] = value; } } } // Convert NodeMap to a normal object i = node.attributes.length; while(i--) { attr = node.attributes[i]; attributes[attr.nodeName] = attr.value; } // Getting the correct opacity is a bit tricky, since SVG path elements don't // support opacity as an attribute, but you can apply it via CSS. // So we take the opacity and set (stroke/fill)-opacity to the same value. if (!_.isUndefined(styles.opacity)) { styles['stroke-opacity'] = styles.opacity; styles['fill-opacity'] = styles.opacity; } // Merge attributes and applied styles (attributes take precedence) _.extend(styles, attributes); // Similarly visibility is influenced by the value of both display and visibility. // Calculate a unified value here styles.visible = (styles.display !== 'none') && (styles.visibility === 'visible'); // Now iterate the whole thing for (key in styles) { value = styles[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': elem.classList = value.split(' '); break; } } return elem; }, /** * Read any number of SVG node types and create Two equivalents of them. */ read: { svg: function() { return Two.Utils.read.g.apply(this, arguments); }, g: function(node) { var group = new Two.Group(); // Switched up order to inherit more specific styles Two.Utils.applySvgAttributes(node, group); for (var i = 0, l = node.childNodes.length; i < l; i++) { var n = node.childNodes[i]; var tag = n.nodeName; if (!tag) return; var tagName = tag.replace(/svg\:/ig, '').toLowerCase(); if (tagName in Two.Utils.read) { var o = Two.Utils.read[tagName].call(this, n); group.add(o); } } return group; }, polygon: function(node, open) { var points = node.getAttribute('points'); var verts = []; points.replace(/(-?[\d\.?]+),(-?[\d\.?]+)/g, function(match, p1, p2) { verts.push(new Two.Anchor(parseFloat(p1), parseFloat(p2))); }); var poly = new Two.Polygon(verts, !open).noStroke(); poly.fill = 'black'; return Two.Utils.applySvgAttributes(node, poly); }, polyline: function(node) { return Two.Utils.read.polygon(node, true); }, path: function(node) { var path = node.getAttribute('d'); // Create a Two.Polygon from the paths. var coord, 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; }; Object.defineProperty(Vector.prototype, 'x', { get: function() { return this._x; }, set: function(v) { this._x = v; if (this._bound) this.trigger(Two.Events.change, 'x'); } }); Object.defineProperty(Vector.prototype, 'y', { get: function() { return this._y; }, set: function(v) { this._y = v; if (this._bound) this.trigger(Two.Events.change, 'y'); } }); _.extend(Vector, { zero: new Two.Vector() }); _.extend(Vector.prototype, Backbone.Events, { set: function(x, y) { this._x = x; this._y = y; if (this._bound) this.trigger(Two.Events.change); return this; }, copy: function(v) { this._x = v.x; this._y = v.y; if (this._bound) this.trigger(Two.Events.change); return this; }, clear: function() { this._x = 0; this._y = 0; if (this._bound) this.trigger(Two.Events.change); 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; if (this._bound) this.trigger(Two.Events.change); return this; }, addSelf: function(v) { this._x += v.x; this._y += v.y; if (this._bound) this.trigger(Two.Events.change); return this; }, sub: function(v1, v2) { this._x = v1.x - v2.x; this._y = v1.y - v2.y; if (this._bound) this.trigger(Two.Events.change); return this; }, subSelf: function(v) { this._x -= v.x; this._y -= v.y; if (this._bound) this.trigger(Two.Events.change); return this; }, multiplySelf: function(v) { this._x *= v.x; this._y *= v.y; if (this._bound) this.trigger(Two.Events.change); return this; }, multiplyScalar: function(s) { this._x *= s; this._y *= s; if (this._bound) this.trigger(Two.Events.change); return this; }, divideScalar: function(s) { if (s) { this._x /= s; this._y /= s; if (this._bound) this.trigger(Two.Events.change); return this; } 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 }; } }); /** * 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() { // Enable events selectively this._bound = true; return Backbone.Events.bind.apply(this, arguments); }; Two.Vector.prototype.unbind = Two.Vector.prototype.on = function() { // Disable events selectively if (!this._events) { // TODO // This doesn't actually work right now // it would require events to unset the _events prop // when no listeners are left this._bound = false; } return Backbone.Events.unbind.apply(this, arguments); }; })(); (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) { 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); } this.controls.left.bind(Two.Events.change, this._broadcast); this.controls.right.bind(Two.Events.change, this._broadcast); return this; }, ignore: function() { this.controls.left.unbind(Two.Events.change, this._broadcast); this.controls.right.unbind(Two.Events.change, this._broadcast); 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); // _.extend doesn't copy all properties properly var xgs = Object.getOwnPropertyDescriptor(Two.Vector.prototype, 'x'); var ygs = Object.getOwnPropertyDescriptor(Two.Vector.prototype, 'y'); Object.defineProperty(Anchor.prototype, 'x', xgs); Object.defineProperty(Anchor.prototype, 'y', ygs); // 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); }; Two.Anchor.prototype.unbind = Two.Anchor.prototype.off = function() { Two.Vector.prototype.unbind.apply(this, arguments); }; })(); (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) { var elements = a; if (!_.isArray(elements)) { elements = _.toArray(arguments); } _.extend(this.elements, elements); 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; }, /** * Add attributes from an svg element. */ setAttributes: function(elem, attrs) { for (var key in attrs) { elem.setAttribute(key, attrs[key]); } return this; }, /** * Remove attributes from an svg element. */ removeAttributes: function(elem, attrs) { for (var key in attrs) { elem.removeAttribute(key); } 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 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(); // Collect any attribute that needs to be changed here var changed = {}; var flagMatrix = this._matrix.manual || this._flagMatrix; if (flagMatrix) { changed.transform = 'matrix(' + this._matrix.toString() + ')'; } if (this._flagVertices) { var vertices = svg.toString(this._vertices, this._closed); changed.d = vertices; } if (this._flagFill) { changed.fill = this._fill; } if (this._flagStroke) { changed.stroke = this._stroke; } if (this._flagLinewidth) { changed['stroke-width'] = this._linewidth; } if (this._flagOpacity) { changed['stroke-opacity'] = this._opacity; changed['fill-opacity'] = this._opacity; } if (this._flagVisible) { changed.visibility = this._visible ? 'visible' : 'hidden'; } if (this._flagCap) { changed['stroke-linecap'] = this._cap; } if (this._flagJoin) { changed['stroke-linejoin'] = this._join; } if (this._flagMiter) { changed['stroke-miterlimit'] = this.miter; } // If there is no attached DOM element yet, // create it with all necessary attributes. if (!this._renderer.elem) { changed.id = this.id; this._renderer.elem = svg.createElement('path', changed); domElement.appendChild(this._renderer.elem); // Otherwise apply all pending attributes } else { svg.setAttributes(this._renderer.elem, changed); } 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) { // This function is called a lot // when importing a large SVG var i = items.length; while(i--) { items[i].bind(Two.Events.change, updateVertices); } 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); })();