UNPKG

twojs-browserify

Version:

A browserified two-dimensional drawing api meant for modern browsers.

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