@sky-foundry/two.js
Version:
A renderer agnostic two-dimensional drawing api for the web.
1,586 lines (1,291 loc) • 93.1 kB
JavaScript
(this || window).Two = (function(previousTwo) {
var root = typeof window != 'undefined' ? window : typeof global != 'undefined' ? global : null;
var toString = Object.prototype.toString;
/**
* @name _
* @interface
* @private
* @description A collection of useful functions borrowed and repurposed from Underscore.js.
* @see {@link http://underscorejs.org/}
*/
var _ = {
// http://underscorejs.org/ • 1.8.3
_indexAmount: 0,
natural: {
slice: Array.prototype.slice,
indexOf: Array.prototype.indexOf,
keys: Object.keys,
bind: Function.prototype.bind,
create: Object.create
},
identity: function(value) {
return value;
},
isArguments: function(obj) {
return toString.call(obj) === '[object Arguments]';
},
isFunction: function(obj) {
return toString.call(obj) === '[object Function]';
},
isString: function(obj) {
return toString.call(obj) === '[object String]';
},
isNumber: function(obj) {
return toString.call(obj) === '[object Number]';
},
isDate: function(obj) {
return toString.call(obj) === '[object Date]';
},
isRegExp: function(obj) {
return toString.call(obj) === '[object RegExp]';
},
isError: function(obj) {
return toString.call(obj) === '[object Error]';
},
isFinite: function(obj) {
return isFinite(obj) && !isNaN(parseFloat(obj));
},
isNaN: function(obj) {
return _.isNumber(obj) && obj !== +obj;
},
isBoolean: function(obj) {
return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
},
isNull: function(obj) {
return obj === null;
},
isUndefined: function(obj) {
return obj === void 0;
},
isEmpty: function(obj) {
if (obj == null) return true;
if (isArrayLike && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;
return _.keys(obj).length === 0;
},
isElement: function(obj) {
return !!(obj && obj.nodeType === 1);
},
isArray: Array.isArray || function(obj) {
return toString.call(obj) === '[object Array]';
},
isObject: function(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
},
toArray: function(obj) {
if (!obj) {
return [];
}
if (_.isArray(obj)) {
return slice.call(obj);
}
if (isArrayLike(obj)) {
return _.map(obj, _.identity);
}
return _.values(obj);
},
range: function(start, stop, step) {
if (stop == null) {
stop = start || 0;
start = 0;
}
step = step || 1;
var length = Math.max(Math.ceil((stop - start) / step), 0);
var range = Array(length);
for (var idx = 0; idx < length; idx++, start += step) {
range[idx] = start;
}
return range;
},
indexOf: function(list, item) {
if (!!_.natural.indexOf) {
return _.natural.indexOf.call(list, item);
}
for (var i = 0; i < list.length; i++) {
if (list[i] === item) {
return i;
}
}
return -1;
},
has: function(obj, key) {
return obj != null && hasOwnProperty.call(obj, key);
},
bind: function(func, ctx) {
var natural = _.natural.bind;
if (natural && func.bind === natural) {
return natural.apply(func, slice.call(arguments, 1));
}
var args = slice.call(arguments, 2);
return function() {
func.apply(ctx, args);
};
},
extend: function(base) {
var sources = slice.call(arguments, 1);
for (var i = 0; i < sources.length; i++) {
var obj = sources[i];
for (var k in obj) {
base[k] = obj[k];
}
}
return base;
},
defaults: function(base) {
var sources = slice.call(arguments, 1);
for (var i = 0; i < sources.length; i++) {
var obj = sources[i];
for (var k in obj) {
if (base[k] === void 0) {
base[k] = obj[k];
}
}
}
return base;
},
keys: function(obj) {
if (!_.isObject(obj)) {
return [];
}
if (_.natural.keys) {
return _.natural.keys(obj);
}
var keys = [];
for (var k in obj) {
if (_.has(obj, k)) {
keys.push(k);
}
}
return keys;
},
values: function(obj) {
var keys = _.keys(obj);
var values = [];
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
values.push(obj[k]);
}
return values;
},
each: function(obj, iteratee, context) {
var ctx = context || this;
var keys = !isArrayLike(obj) && _.keys(obj);
var length = (keys || obj).length;
for (var i = 0; i < length; i++) {
var k = keys ? keys[i] : i;
iteratee.call(ctx, obj[k], k, obj);
}
return obj;
},
map: function(obj, iteratee, context) {
var ctx = context || this;
var keys = !isArrayLike(obj) && _.keys(obj);
var length = (keys || obj).length;
var result = [];
for (var i = 0; i < length; i++) {
var k = keys ? keys[i] : i;
result[i] = iteratee.call(ctx, obj[k], k, obj);
}
return result;
},
once: function(func) {
var init = false;
return function() {
if (!!init) {
return func;
}
init = true;
return func.apply(this, arguments);
}
},
after: function(times, func) {
return function() {
while (--times < 1) {
return func.apply(this, arguments);
}
}
},
uniqueId: function(prefix) {
var id = ++_._indexAmount + '';
return prefix ? prefix + id : id;
}
};
// Constants
var sin = Math.sin,
cos = Math.cos,
acos = Math.acos,
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;
var slice = _.natural.slice;
var perf = ((root.performance && root.performance.now) ? root.performance : Date);
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var getLength = function(obj) {
return obj == null ? void 0 : obj['length'];
};
var isArrayLike = function(collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
// Cross browser dom events.
var dom = {
temp: (root.document ? root.document.createElement('div') : {}),
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 dom;
},
unbind: function(elem, event, func, bool) {
if (dom.hasEventListeners) {
elem.removeEventListeners(event, func, !!bool);
} else {
elem.detachEvent('on' + event, func);
}
return dom;
},
getRequestAnimationFrame: function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
var request = root.requestAnimationFrame, cancel;
if(!request) {
for (var i = 0; i < vendors.length; i++) {
request = root[vendors[i] + 'RequestAnimationFrame'] || request;
cancel = root[vendors[i] + 'CancelAnimationFrame']
|| root[vendors[i] + 'CancelRequestAnimationFrame'] || cancel;
}
request = request || function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = root.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
// cancel = cancel || function(id) {
// clearTimeout(id);
// };
}
request.init = _.once(loop);
return request;
}
};
/**
* @name Two
* @class
* @global
* @param {Object} [options]
* @param {Boolean} [options.fullscreen=false] - Set to `true` to automatically make the stage adapt to the width and height of the parent document. This parameter overrides `width` and `height` parameters if set to `true`.
* @param {Number} [options.width=640] - The width of the stage on construction. This can be set at a later time.
* @param {Number} [options.height=480] - The height of the stage on construction. This can be set at a later time.
* @param {String} [options.type=Two.Types.svg] - The type of renderer to setup drawing with. See [`Two.Types`]{@link Two.Types} for available options.
* @param {Boolean} [options.autostart=false] - Set to `true` to add the instance to draw on `requestAnimationFrame`. This is a convenient substitute for {@link Two#play}.
* @description The entrypoint for Two.js. Instantiate a `new Two` in order to setup a scene to render to. `Two` is also the publicly accessible namespace that all other sub-classes, functions, and utilities attach to.
*/
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 (/fullscreen/i.test(k) || /autostart/i.test(k)) {
return;
}
this[k] = v;
}, this);
// Specified domElement overrides type declaration only if the element does not support declared renderer type.
if (_.isElement(params.domElement)) {
var tagName = params.domElement.tagName.toLowerCase();
// TODO: Reconsider this if statement's logic.
if (!/^(CanvasRenderer-canvas|WebGLRenderer-canvas|SVGRenderer-svg)$/.test(this.type+'-'+tagName)) {
this.type = Two.Types[tagName];
}
}
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.renderer.bind(Two.Events.resize, _.bind(updateDimensions, this));
this.scene = this.renderer.scene;
Two.Instances.push(this);
if (params.autostart) {
raf.init();
}
};
_.extend(Two, {
// Access to root in other files.
/**
* @name Two.root
* @description The root of the session context. In the browser this is the `window` variable. This varies in headless environments.
*/
root: root,
/**
* @name Two.nextFrameID
* @property {Integer}
* @description The id of the next requestAnimationFrame function.
*/
nextFrameID: null,
// Primitive
/**
* @name Two.Array
* @description A simple polyfill for Float32Array.
*/
Array: root.Float32Array || Array,
/**
* @name Two.Types
* @property {Object} - The different rendering types availabe in the library.
*/
Types: {
webgl: 'WebGLRenderer',
svg: 'SVGRenderer',
canvas: 'CanvasRenderer'
},
/**
* @name Two.Version
* @property {String} - The current working version of the library.
*/
Version: 'v0.7.0-beta.4',
/**
* @name Two.PublishDate
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
*/
PublishDate: '<%= publishDate %>',
/**
* @name Two.Identifier
* @property {String} - String prefix for all Two.js object's ids. This trickles down to SVG ids.
*/
Identifier: 'two-',
/**
* @name Two.Events
* @property {Object} - Map of possible events in Two.js.
*/
Events: {
play: 'play',
pause: 'pause',
update: 'update',
render: 'render',
resize: 'resize',
change: 'change',
remove: 'remove',
insert: 'insert',
order: 'order',
load: 'load'
},
/**
* @name Two.Commands
* @property {Object} - Map of possible path commands. Taken from the SVG specification.
*/
Commands: {
move: 'M',
line: 'L',
curve: 'C',
arc: 'A',
close: 'Z'
},
/**
* @name Two.Resolution
* @property {Number} - Default amount of vertices to be used for interpreting Arcs and ArcSegments.
*/
Resolution: 12,
/**
* @name Two.Instances
* @property {Array} - Registered list of all Two.js instances in the current session.
*/
Instances: [],
/**
* @function Two.noConflict
* @description A function to revert the global namespaced `Two` variable to its previous incarnation.
* @returns {Two} Returns access to the top-level Two.js library for local use.
*/
noConflict: function() {
root.Two = previousTwo;
return Two;
},
/**
* @function Two.uniqueId
* @description Simple method to access an incrementing value. Used for `id` allocation on all Two.js objects.
* @returns {Number} Ever increasing integer.
*/
uniqueId: function() {
var id = count;
count++;
return id;
},
/**
* @name Two.Utils
* @interface
* @implements {_}
* @description A hodgepodge of handy functions, math, and properties are stored here.
*/
Utils: _.extend(_, {
/**
* @name Two.Utils.performance
* @property {Date} - A special `Date` like object to get the current millis of the session. Used internally to calculate time between frames.
* e.g: `Two.Utils.performance.now() // milliseconds since epoch`
*/
performance: perf,
/**
* @name Two.Utils.defineProperty
* @function
* @this Two#
* @param {String} property - The property to add an enumerable getter / setter to.
* @description Convenience function to setup the flag based getter / setter that most properties are defined as in Two.js.
*/
defineProperty: function(property) {
var object = this;
var secret = '_' + property;
var flag = '_flag' + property.charAt(0).toUpperCase() + property.slice(1);
Object.defineProperty(object, property, {
enumerable: true,
get: function() {
return this[secret];
},
set: function(v) {
this[secret] = v;
this[flag] = true;
}
});
},
Image: null,
isHeadless: false,
/**
* @name Two.Utils.shim
* @function
* @param {canvas} canvas - The instanced `Canvas` object provided by `node-canvas`.
* @param {Image} [Image] - The prototypical `Image` object provided by `node-canvas`. This is only necessary to pass if you're going to load bitmap imagery.
* @returns {canvas} Returns the instanced canvas object you passed from with additional attributes needed for Two.js.
* @description Convenience method for defining all the dependencies from the npm package `node-canvas`. See [node-canvas]{@link https://github.com/Automattic/node-canvas} for additional information on setting up HTML5 `<canvas />` drawing in a node.js environment.
*/
shim: function(canvas, Image) {
Two.CanvasRenderer.Utils.shim(canvas);
if (!_.isUndefined(Image)) {
Two.Utils.Image = Image;
}
Two.Utils.isHeadless = true;
return canvas;
},
/**
* @name Two.Utils.release
* @function
* @param {Object} obj
* @returns {Object} The object passed for event deallocation.
* @description 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);
});
}
return obj;
},
/**
* @name Two.Utils.xhr
* @function
* @param {String} path
* @param {Function} callback
* @returns {XMLHttpRequest} The constructed and called XHR request.
* @description Canonical method to initiate `GET` requests in the browser. Mainly used by {@link Two#load} method.
*/
xhr: function(path, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', path);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
callback(xhr.responseText);
}
};
xhr.send();
return xhr;
},
/**
* @name Two.Utils.Curve
* @property {Object} - Additional utility constant variables related to curve math and calculations.
*/
Curve: {
CollinearityEpsilon: pow(10, -30),
RecursionLimit: 16,
CuspLimit: 0,
Tolerance: {
distance: 0.25,
angle: 0,
epsilon: Number.EPSILON
},
// 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]
]
},
devicePixelRatio: root.devicePixelRatio || 1,
getBackingStoreRatio: function(ctx) {
return ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
},
/**
* @name Two.Utils.getRatio
* @function
* @param {Canvas.context2D} ctx
* @returns {Number} The ratio of a unit in Two.js to the pixel density of a session's screen.
* @see [High DPI Rendering]{@link http://www.html5rocks.com/en/tutorials/canvas/hidpi/}
*/
getRatio: function(ctx) {
return Two.Utils.devicePixelRatio / getBackingStoreRatio(ctx);
},
/**
* @name Two.Utils.setPlaying
* @function
* @this Two#
* @returns {Two} The instance called with for potential chaining.
* @description Internal convenience method to properly defer play calling until after all objects have been updated with their newest styles.
*/
setPlaying: function(b) {
this.playing = !!b;
return this;
},
/**
* @name Two.Utils.getComputedMatrix
* @function
* @param {Two.Shape} object - The Two.js object that has a matrix property to calculate from.
* @param {Two.Matrix} [matrix] - The matrix to apply calculated transformations to if available.
* @returns {Two.Matrix} The computed matrix of a nested object. If no `matrix` was passed in arguments then a `new Two.Matrix` is returned.
* @description Method to get the world space transformation of a given object in a Two.js scene.
*/
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();
for (var i = 0; i < matrices.length; i++) {
var m = matrices[i];
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;
},
/**
* @name Two.Utils.deltaTransformPoint
* @function
* @param {Two.Matrix} matrix
* @param {Number} x
* @param {Number} y
* @returns {Two.Vector}
* @description Used by {@link Two.Utils.decomposeMatrix}
*/
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);
},
/**
* @name Two.Utils.decomposeMatrix
* @function
* @param {Two.Matrix} matrix - The matrix to decompose.
* @returns {Object} An object containing relevant skew values.
* @description Decompose a 2D 3x3 Matrix to find the skew.
* @see {@link 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
};
},
/**
* @name Two.Utils.extractCSSText
* @function
* @param {String} text - The CSS text body to be parsed and extracted.
* @param {Object} [styles] - The styles object to apply CSS key values to.
* @returns {Object} styles
* @description Parse CSS text body and apply them as key value pairs to a JavaScript object.
*/
extractCSSText: function(text, styles) {
var commands, command, name, value;
if (!styles) {
styles = {};
}
commands = text.split(';');
for (var i = 0; i < commands.length; i++) {
command = commands[i].split(':');
name = command[0];
value = command[1];
if (_.isUndefined(name) || _.isUndefined(value)) {
continue;
}
styles[name] = value.replace(/\s/, '');
}
return styles;
},
/**
* @name Two.Utils.getSvgStyles
* @function
* @param {SvgNode} node - The SVG node to parse.
* @returns {Object} styles
* @description Get the CSS comands from the `style` attribute of an SVG node and apply them as key value pairs to a JavaScript object.
*/
getSvgStyles: function(node) {
var styles = {};
for (var i = 0; i < node.style.length; i++) {
var command = node.style[i];
styles[command] = node.style[command];
}
return styles;
},
/**
* @name Two.Utils.applySvgViewBox
* @function
* @param {Two.Shape} node - The Two.js object to apply viewbox matrix to
* @param {String} value - The viewBox value from the SVG attribute
* @returns {Two.Shape} node
@ @description
*/
applySvgViewBox: function(node, value) {
var elements = value.split(/\s/);
var x = parseFloat(elements[0]);
var y = parseFloat(elements[1]);
var width = parseFloat(elements[2]);
var height = parseFloat(elements[3]);
var s = Math.min(this.width / width, this.height / height);
node.translation.x -= x * s;
node.translation.y -= y * s;
node.scale = s;
return node;
},
/**
* @name Two.Utils.applySvgAttributes
* @function
* @param {SvgNode} node - An SVG Node to extrapolate attributes from.
* @param {Two.Shape} elem - The Two.js object to apply extrapolated attributes to.
* @returns {Two.Shape} The Two.js object passed now with applied attributes.
* @description This function iterates through an SVG Node's properties and stores ones of interest. It tries to resolve styles applied via CSS as well.
* @TODO Reverse calculate `Two.Gradient`s for fill / stroke of any given path.
*/
applySvgAttributes: function(node, elem, parentStyles) {
var styles = {}, attributes = {}, extracted = {}, i, key, value, attr;
// Not available in non browser environments
if (root.getComputedStyle) {
// Convert CSSStyleDeclaration to a normal object
var computedStyles = root.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 (!_.isUndefined(value)) {
styles[key] = value;
}
}
}
// Convert NodeMap to a normal object
for (i = 0; i < node.attributes.length; i++) {
attr = node.attributes[i];
if (/style/i.test(attr.nodeName)) {
Two.Utils.extractCSSText(attr.value, extracted);
} else {
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;
delete styles.opacity;
}
// Merge attributes and applied styles (attributes take precedence)
if (parentStyles) {
_.defaults(styles, parentStyles);
}
_.extend(styles, attributes, extracted);
// 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) && /none/i.test(styles.display))
|| (_.isUndefined(styles.visibility) && /hidden/i.test(styles.visibility));
// Now iterate the whole thing
for (key in styles) {
value = styles[key];
switch (key) {
case 'transform':
// TODO: Check this out https://github.com/paperjs/paper.js/blob/develop/src/svg/SvgImport.js#L315
if (/none/i.test(value)) break;
var m = (node.transform && node.transform.baseVal && node.transform.baseVal.length > 0)
? node.transform.baseVal[0].matrix
: (node.getCTM ? node.getCTM() : null);
// Might happen when transform string is empty or not valid.
if (_.isNull(m)) 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(m);
elem.translation.set(transforms.translateX, transforms.translateY);
elem.rotation = transforms.rotation;
elem.scale = new Two.Vector(transforms.scaleX, transforms.scaleY);
var x = parseFloat((styles.x + '').replace('px'));
var y = parseFloat((styles.y + '').replace('px'));
// Override based on attributes.
if (x) {
elem.translation.x = x;
}
if (y) {
elem.translation.y = y;
}
break;
case 'viewBox':
Two.Utils.applySvgViewBox.call(this, elem, value);
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 'opacity':
case 'stroke-opacity':
case 'fill-opacity':
// Only apply styles to rendered shapes
// in the scene.
if (!(elem instanceof Two.Group)) {
elem.opacity = parseFloat(value);
}
break;
case 'fill':
case 'stroke':
if (/url\(\#.*\)/i.test(value)) {
elem[key] = this.getById(
value.replace(/url\(\#(.*)\)/i, '$1'));
} else {
elem[key] = (/none/i.test(value)) ? 'transparent' : value;
}
break;
case 'id':
elem.id = value;
break;
case 'class':
case 'className':
elem.classList = value.split(' ');
break;
}
}
return styles;
},
/**
* @name Two.Utils.read
* @property {Object} read - A map of functions to read any number of SVG node types and create Two.js equivalents of them. Primarily used by the {@link Two#interpret} method.
*/
read: {
svg: function(node) {
var svg = Two.Utils.read.g.call(this, node);
var viewBox = node.getAttribute('viewBox');
// Two.Utils.applySvgViewBox(svg, viewBox);
return svg;
},
g: function(node) {
var styles, attrs;
var group = new Two.Group();
// Switched up order to inherit more specific styles
styles = Two.Utils.getSvgStyles.call(this, node);
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(group, n, styles);
group.add(o);
}
}
return group;
},
polygon: function(node, parentStyles) {
var points = node.getAttribute('points');
var verts = [];
points.replace(/(-?[\d\.?]+)[,|\s](-?[\d\.?]+)/g, function(match, p1, p2) {
verts.push(new Two.Anchor(parseFloat(p1), parseFloat(p2)));
});
var poly = new Two.Path(verts, true).noStroke();
poly.fill = 'black';
Two.Utils.applySvgAttributes.call(this, node, poly, parentStyles);
return poly;
},
polyline: function(node, parentStyles) {
var poly = Two.Utils.read.polygon.call(this, node, parentStyles);
poly.closed = false;
return poly;
},
path: function(node, parentStyles) {
var path = node.getAttribute('d');
// Create a Two.Path 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 number, fid, lid, numbers, first, s;
var j, k, ct, l, times;
var type = command[0];
var lower = type.toLowerCase();
var items = command.slice(1).trim().split(/[\s,]+|(?=\s?[+\-])/);
var pre, post, result = [], bin;
var hasDoubleDecimals = false;
// Handle double decimal values e.g: 48.6037.71.8
// Like: https://m.abcsofchinese.com/images/svg/亼ji2.svg
for (j = 0; j < items.length; j++) {
number = items[j];
fid = number.indexOf('.');
lid = number.lastIndexOf('.');
if (fid !== lid) {
numbers = number.split('.');
first = numbers[0] + '.' + numbers[1];
items.splice(j, 1, first);
for (s = 2; s < numbers.length; s++) {
items.splice(j + s - 1, 0, '0.' + numbers[s]);
}
hasDoubleDecimals = true;
}
}
if (hasDoubleDecimals) {
command = type + items.join(',');
}
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':
if (items.length > 7) {
bin = 7;
}
break;
}
// This means we have a polybezier.
if (bin) {
for (j = 0, l = items.length, times = 0; j < l; j+=bin) {
ct = type;
if (times > 0) {
switch (type) {
case 'm':
ct = 'l';
break;
case 'M':
ct = 'L';
break;
}
}
result.push(ct + items.slice(j, j + bin).join(' '));
times++;
}
commands = Array.prototype.concat.apply(commands, result);
} else {
commands.push(command);
}
});
// Create the vertices for our Two.Path
var points = [];
_.each(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
);
// Make coord be the last `m` command
for (var i = points.length - 1; i >= 0; i--) {
var point = points[i];
if (/m/i.test(point.command)) {
coord = point;
break;
}
}
}
break;
case 'm':
case 'l':
control = undefined;
x = parseFloat(coords[0]);
y = parseFloat(coords[1]);
result = new Two.Anchor(
x, y,
undefined, undefined,
undefined, undefined,
/m/i.test(lower) ? 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 = /h/i.test(lower) ? 'x' : 'y';
var b = /x/i.test(a) ? '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 (/c/i.test(lower)) {
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;
y2 = control.y;
}
if (/q/i.test(lower)) {
x3 = parseFloat(coords[0]);
y3 = parseFloat(coords[1]);
x4 = parseFloat(coords[2]);
y4 = parseFloat(coords[3]);
} 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':
x1 = coord.x;
y1 = coord.y;
var rx = parseFloat(coords[0]);
var ry = parseFloat(coords[1]);
var xAxisRotation = parseFloat(coords[2]);// * PI / 180;
var largeArcFlag = parseFloat(coords[3]);
var sweepFlag = parseFloat(coords[4]);
x4 = parseFloat(coords[5]);
y4 = parseFloat(coords[6]);
if (relative) {
x4 += x1;
y4 += y1;
}
var anchor = new Two.Anchor(x4, y4);
anchor.command = Two.Commands.arc;
anchor.rx = rx;
anchor.ry = ry;
anchor.xAxisRotation = xAxisRotation;
anchor.largeArcFlag = largeArcFlag;
anchor.sweepFlag = sweepFlag;
result = anchor;
coord = anchor;
control = undefined;
break;
}
if (result) {
if (_.isArray(result)) {
points = points.concat(result);
} else {
points.push(result);
}
}
});
if (points.length <= 1) {
return;
}
var path = new Two.Path(points, closed, undefined, true).noStroke();
path.fill = 'black';
var rect = path.getBoundingClientRect(true);
// Center objects to stay consistent
// with the rest of the Two.js API.
rect.centroid = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
_.each(path.vertices, function(v) {
v.subSelf(rect.centroid);
});
path.translation.addSelf(rect.centroid);
Two.Utils.applySvgAttributes.call(this, node, path, parentStyles);
return path;
},
circle: function(node, parentStyles) {
var x = parseFloat(node.getAttribute('cx'));
var y = parseFloat(node.getAttribute('cy'));
var r = parseFloat(node.getAttribute('r'));
var circle = new Two.Circle(x, y, r).noStroke();
circle.fill = 'black';
Two.Utils.applySvgAttributes.call(this, node, circle, parentStyles);
return circle;
},
ellipse: function(node, parentStyles) {
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 ellipse = new Two.Ellipse(x, y, width, height).noStroke();
ellipse.fill = 'black';
Two.Utils.applySvgAttributes.call(this, node, ellipse, parentStyles);
return ellipse;
},
rect: function(node, parentStyles) {
var rx = parseFloat(node.getAttribute('rx'));
var ry = parseFloat(node.getAttribute('ry'));
if (!_.isNaN(rx) || !_.isNaN(ry)) {
return Two.Utils.read['rounded-rect'](node);
}
var x = parseFloat(node.getAttribute('x')) || 0;
var y = parseFloat(node.getAttribute('y')) || 0;
var width = parseFloat(node.getAttribute('width'));
var height = parseFloat(node.getAttribute('height'));
var w2 = width / 2;
var h2 = height / 2;
var rect = new Two.Rectangle(x + w2, y + h2, width, height)
.noStroke();
rect.fill = 'black';
Two.Utils.applySvgAttributes.call(this, node, rect, parentStyles);
return rect;
},
'rounded-rect': function(node, parentStyles) {
var x = parseFloat(node.getAttribute('x')) || 0;
var y = parseFloat(node.getAttribute('y')) || 0;
var rx = parseFloat(node.getAttribute('rx')) || 0;
var ry = parseFloat(node.getAttribute('ry')) || 0;
var width = parseFloat(node.getAttribute('width'));
var height = parseFloat(node.getAttribute('height'));
var w2 = width / 2;
var h2 = height / 2;