UNPKG

cupiditatea

Version:

A two-dimensional drawing api meant for modern browsers.

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