@sky-foundry/two.js
Version:
A renderer agnostic two-dimensional drawing api for the web.
1,578 lines (1,287 loc) • 352 kB
JavaScript
/**
MIT License
Copyright (c) 2012 - 2017 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.
*/
(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.3',
/**
* @name Two.PublishDate
* @property {String} - The automatically generated publish date in the build process to verify version release candidates.
*/
PublishDate: '2018-12-08T22:41:43+01:00',
/**
* @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