Last active
August 29, 2015 14:02
-
-
Save ponychicken/3c73791d1ddc9411094b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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. | |
* | |
*/ | |
// Underscore.js 1.5.1 | |
// http://underscorejs.org | |
// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors | |
// Underscore may be freely distributed under the MIT license. | |
(function() { | |
// Baseline setup | |
// -------------- | |
// Establish the root object, `window` in the browser, or `global` on the server. | |
var root = this; | |
// Save the previous value of the `_` variable. | |
var previousUnderscore = root._; | |
// Establish the object that gets returned to break out of a loop iteration. | |
var breaker = {}; | |
// Save bytes in the minified (but not gzipped) version: | |
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; | |
// Create quick reference variables for speed access to core prototypes. | |
var | |
push = ArrayProto.push, | |
slice = ArrayProto.slice, | |
concat = ArrayProto.concat, | |
toString = ObjProto.toString, | |
hasOwnProperty = ObjProto.hasOwnProperty; | |
// All **ECMAScript 5** native function implementations that we hope to use | |
// are declared here. | |
var | |
nativeForEach = ArrayProto.forEach, | |
nativeMap = ArrayProto.map, | |
nativeReduce = ArrayProto.reduce, | |
nativeReduceRight = ArrayProto.reduceRight, | |
nativeFilter = ArrayProto.filter, | |
nativeEvery = ArrayProto.every, | |
nativeSome = ArrayProto.some, | |
nativeIndexOf = ArrayProto.indexOf, | |
nativeLastIndexOf = ArrayProto.lastIndexOf, | |
nativeIsArray = Array.isArray, | |
nativeKeys = Object.keys, | |
nativeBind = FuncProto.bind; | |
// Create a safe reference to the Underscore object for use below. | |
var _ = function(obj) { | |
if (obj instanceof _) return obj; | |
if (!(this instanceof _)) return new _(obj); | |
this._wrapped = obj; | |
}; | |
// Export the Underscore object for **Node.js**, with | |
// backwards-compatibility for the old `require()` API. If we're in | |
// the browser, add `_` as a global object via a string identifier, | |
// for Closure Compiler "advanced" mode. | |
if (typeof exports !== 'undefined') { | |
if (typeof module !== 'undefined' && module.exports) { | |
exports = module.exports = _; | |
} | |
exports._ = _; | |
} else { | |
root._ = _; | |
} | |
// Current version. | |
_.VERSION = '1.5.1'; | |
// Collection Functions | |
// -------------------- | |
// The cornerstone, an `each` implementation, aka `forEach`. | |
// Handles objects with the built-in `forEach`, arrays, and raw objects. | |
// Delegates to **ECMAScript 5**'s native `forEach` if available. | |
var each = _.each = _.forEach = function(obj, iterator, context) { | |
if (obj == null) return; | |
if (nativeForEach && obj.forEach === nativeForEach) { | |
obj.forEach(iterator, context); | |
} else if (obj.length === +obj.length) { | |
for (var i = 0, l = obj.length; i < l; i++) { | |
if (iterator.call(context, obj[i], i, obj) === breaker) return; | |
} | |
} else { | |
for (var key in obj) { | |
if (_.has(obj, key)) { | |
if (iterator.call(context, obj[key], key, obj) === breaker) return; | |
} | |
} | |
} | |
}; | |
// Return the results of applying the iterator to each element. | |
// Delegates to **ECMAScript 5**'s native `map` if available. | |
_.map = _.collect = function(obj, iterator, context) { | |
var results = []; | |
if (obj == null) return results; | |
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); | |
each(obj, function(value, index, list) { | |
results.push(iterator.call(context, value, index, list)); | |
}); | |
return results; | |
}; | |
var reduceError = 'Reduce of empty array with no initial value'; | |
// **Reduce** builds up a single result from a list of values, aka `inject`, | |
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. | |
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { | |
var initial = arguments.length > 2; | |
if (obj == null) obj = []; | |
if (nativeReduce && obj.reduce === nativeReduce) { | |
if (context) iterator = _.bind(iterator, context); | |
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); | |
} | |
each(obj, function(value, index, list) { | |
if (!initial) { | |
memo = value; | |
initial = true; | |
} else { | |
memo = iterator.call(context, memo, value, index, list); | |
} | |
}); | |
if (!initial) throw new TypeError(reduceError); | |
return memo; | |
}; | |
// The right-associative version of reduce, also known as `foldr`. | |
// Delegates to **ECMAScript 5**'s native `reduceRight` if available. | |
_.reduceRight = _.foldr = function(obj, iterator, memo, context) { | |
var initial = arguments.length > 2; | |
if (obj == null) obj = []; | |
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { | |
if (context) iterator = _.bind(iterator, context); | |
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); | |
} | |
var length = obj.length; | |
if (length !== +length) { | |
var keys = _.keys(obj); | |
length = keys.length; | |
} | |
each(obj, function(value, index, list) { | |
index = keys ? keys[--length] : --length; | |
if (!initial) { | |
memo = obj[index]; | |
initial = true; | |
} else { | |
memo = iterator.call(context, memo, obj[index], index, list); | |
} | |
}); | |
if (!initial) throw new TypeError(reduceError); | |
return memo; | |
}; | |
// Return the first value which passes a truth test. Aliased as `detect`. | |
_.find = _.detect = function(obj, iterator, context) { | |
var result; | |
any(obj, function(value, index, list) { | |
if (iterator.call(context, value, index, list)) { | |
result = value; | |
return true; | |
} | |
}); | |
return result; | |
}; | |
// Return all the elements that pass a truth test. | |
// Delegates to **ECMAScript 5**'s native `filter` if available. | |
// Aliased as `select`. | |
_.filter = _.select = function(obj, iterator, context) { | |
var results = []; | |
if (obj == null) return results; | |
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); | |
each(obj, function(value, index, list) { | |
if (iterator.call(context, value, index, list)) results.push(value); | |
}); | |
return results; | |
}; | |
// Return all the elements for which a truth test fails. | |
_.reject = function(obj, iterator, context) { | |
return _.filter(obj, function(value, index, list) { | |
return !iterator.call(context, value, index, list); | |
}, context); | |
}; | |
// Determine whether all of the elements match a truth test. | |
// Delegates to **ECMAScript 5**'s native `every` if available. | |
// Aliased as `all`. | |
_.every = _.all = function(obj, iterator, context) { | |
iterator || (iterator = _.identity); | |
var result = true; | |
if (obj == null) return result; | |
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); | |
each(obj, function(value, index, list) { | |
if (!(result = result && iterator.call(context, value, index, list))) return breaker; | |
}); | |
return !!result; | |
}; | |
// Determine if at least one element in the object matches a truth test. | |
// Delegates to **ECMAScript 5**'s native `some` if available. | |
// Aliased as `any`. | |
var any = _.some = _.any = function(obj, iterator, context) { | |
iterator || (iterator = _.identity); | |
var result = false; | |
if (obj == null) return result; | |
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); | |
each(obj, function(value, index, list) { | |
if (result || (result = iterator.call(context, value, index, list))) return breaker; | |
}); | |
return !!result; | |
}; | |
// Determine if the array or object contains a given value (using `===`). | |
// Aliased as `include`. | |
_.contains = _.include = function(obj, target) { | |
if (obj == null) return false; | |
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; | |
return any(obj, function(value) { | |
return value === target; | |
}); | |
}; | |
// Invoke a method (with arguments) on every item in a collection. | |
_.invoke = function(obj, method) { | |
var args = slice.call(arguments, 2); | |
var isFunc = _.isFunction(method); | |
return _.map(obj, function(value) { | |
return (isFunc ? method : value[method]).apply(value, args); | |
}); | |
}; | |
// Convenience version of a common use case of `map`: fetching a property. | |
_.pluck = function(obj, key) { | |
return _.map(obj, function(value){ return value[key]; }); | |
}; | |
// Convenience version of a common use case of `filter`: selecting only objects | |
// containing specific `key:value` pairs. | |
_.where = function(obj, attrs, first) { | |
if (_.isEmpty(attrs)) return first ? void 0 : []; | |
return _[first ? 'find' : 'filter'](obj, function(value) { | |
for (var key in attrs) { | |
if (attrs[key] !== value[key]) return false; | |
} | |
return true; | |
}); | |
}; | |
// Convenience version of a common use case of `find`: getting the first object | |
// containing specific `key:value` pairs. | |
_.findWhere = function(obj, attrs) { | |
return _.where(obj, attrs, true); | |
}; | |
// Return the maximum element or (element-based computation). | |
// Can't optimize arrays of integers longer than 65,535 elements. | |
// See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) | |
_.max = function(obj, iterator, context) { | |
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { | |
return Math.max.apply(Math, obj); | |
} | |
if (!iterator && _.isEmpty(obj)) return -Infinity; | |
var result = {computed : -Infinity, value: -Infinity}; | |
each(obj, function(value, index, list) { | |
var computed = iterator ? iterator.call(context, value, index, list) : value; | |
computed > result.computed && (result = {value : value, computed : computed}); | |
}); | |
return result.value; | |
}; | |
// Return the minimum element (or element-based computation). | |
_.min = function(obj, iterator, context) { | |
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { | |
return Math.min.apply(Math, obj); | |
} | |
if (!iterator && _.isEmpty(obj)) return Infinity; | |
var result = {computed : Infinity, value: Infinity}; | |
each(obj, function(value, index, list) { | |
var computed = iterator ? iterator.call(context, value, index, list) : value; | |
computed < result.computed && (result = {value : value, computed : computed}); | |
}); | |
return result.value; | |
}; | |
// Shuffle an array. | |
_.shuffle = function(obj) { | |
var rand; | |
var index = 0; | |
var shuffled = []; | |
each(obj, function(value) { | |
rand = _.random(index++); | |
shuffled[index - 1] = shuffled[rand]; | |
shuffled[rand] = value; | |
}); | |
return shuffled; | |
}; | |
// An internal function to generate lookup iterators. | |
var lookupIterator = function(value) { | |
return _.isFunction(value) ? value : function(obj){ return obj[value]; }; | |
}; | |
// Sort the object's values by a criterion produced by an iterator. | |
_.sortBy = function(obj, value, context) { | |
var iterator = lookupIterator(value); | |
return _.pluck(_.map(obj, function(value, index, list) { | |
return { | |
value : value, | |
index : index, | |
criteria : iterator.call(context, value, index, list) | |
}; | |
}).sort(function(left, right) { | |
var a = left.criteria; | |
var b = right.criteria; | |
if (a !== b) { | |
if (a > b || a === void 0) return 1; | |
if (a < b || b === void 0) return -1; | |
} | |
return left.index < right.index ? -1 : 1; | |
}), 'value'); | |
}; | |
// An internal function used for aggregate "group by" operations. | |
var group = function(obj, value, context, behavior) { | |
var result = {}; | |
var iterator = lookupIterator(value == null ? _.identity : value); | |
each(obj, function(value, index) { | |
var key = iterator.call(context, value, index, obj); | |
behavior(result, key, value); | |
}); | |
return result; | |
}; | |
// Groups the object's values by a criterion. Pass either a string attribute | |
// to group by, or a function that returns the criterion. | |
_.groupBy = function(obj, value, context) { | |
return group(obj, value, context, function(result, key, value) { | |
(_.has(result, key) ? result[key] : (result[key] = [])).push(value); | |
}); | |
}; | |
// Counts instances of an object that group by a certain criterion. Pass | |
// either a string attribute to count by, or a function that returns the | |
// criterion. | |
_.countBy = function(obj, value, context) { | |
return group(obj, value, context, function(result, key) { | |
if (!_.has(result, key)) result[key] = 0; | |
result[key]++; | |
}); | |
}; | |
// Use a comparator function to figure out the smallest index at which | |
// an object should be inserted so as to maintain order. Uses binary search. | |
_.sortedIndex = function(array, obj, iterator, context) { | |
iterator = iterator == null ? _.identity : lookupIterator(iterator); | |
var value = iterator.call(context, obj); | |
var low = 0, high = array.length; | |
while (low < high) { | |
var mid = (low + high) >>> 1; | |
iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; | |
} | |
return low; | |
}; | |
// Safely create a real, live array from anything iterable. | |
_.toArray = function(obj) { | |
if (!obj) return []; | |
if (_.isArray(obj)) return slice.call(obj); | |
if (obj.length === +obj.length) return _.map(obj, _.identity); | |
return _.values(obj); | |
}; | |
// Return the number of elements in an object. | |
_.size = function(obj) { | |
if (obj == null) return 0; | |
return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; | |
}; | |
// Array Functions | |
// --------------- | |
// Get the first element of an array. Passing **n** will return the first N | |
// values in the array. Aliased as `head` and `take`. The **guard** check | |
// allows it to work with `_.map`. | |
_.first = _.head = _.take = function(array, n, guard) { | |
if (array == null) return void 0; | |
return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; | |
}; | |
// Returns everything but the last entry of the array. Especially useful on | |
// the arguments object. Passing **n** will return all the values in | |
// the array, excluding the last N. The **guard** check allows it to work with | |
// `_.map`. | |
_.initial = function(array, n, guard) { | |
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); | |
}; | |
// Get the last element of an array. Passing **n** will return the last N | |
// values in the array. The **guard** check allows it to work with `_.map`. | |
_.last = function(array, n, guard) { | |
if (array == null) return void 0; | |
if ((n != null) && !guard) { | |
return slice.call(array, Math.max(array.length - n, 0)); | |
} else { | |
return array[array.length - 1]; | |
} | |
}; | |
// Returns everything but the first entry of the array. Aliased as `tail` and `drop`. | |
// Especially useful on the arguments object. Passing an **n** will return | |
// the rest N values in the array. The **guard** | |
// check allows it to work with `_.map`. | |
_.rest = _.tail = _.drop = function(array, n, guard) { | |
return slice.call(array, (n == null) || guard ? 1 : n); | |
}; | |
// Trim out all falsy values from an array. | |
_.compact = function(array) { | |
return _.filter(array, _.identity); | |
}; | |
// Internal implementation of a recursive `flatten` function. | |
var flatten = function(input, shallow, output) { | |
if (shallow && _.every(input, _.isArray)) { | |
return concat.apply(output, input); | |
} | |
each(input, function(value) { | |
if (_.isArray(value) || _.isArguments(value)) { | |
shallow ? push.apply(output, value) : flatten(value, shallow, output); | |
} else { | |
output.push(value); | |
} | |
}); | |
return output; | |
}; | |
// Return a completely flattened version of an array. | |
_.flatten = function(array, shallow) { | |
return flatten(array, shallow, []); | |
}; | |
// Return a version of the array that does not contain the specified value(s). | |
_.without = function(array) { | |
return _.difference(array, slice.call(arguments, 1)); | |
}; | |
// Produce a duplicate-free version of the array. If the array has already | |
// been sorted, you have the option of using a faster algorithm. | |
// Aliased as `unique`. | |
_.uniq = _.unique = function(array, isSorted, iterator, context) { | |
if (_.isFunction(isSorted)) { | |
context = iterator; | |
iterator = isSorted; | |
isSorted = false; | |
} | |
var initial = iterator ? _.map(array, iterator, context) : array; | |
var results = []; | |
var seen = []; | |
each(initial, function(value, index) { | |
if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { | |
seen.push(value); | |
results.push(array[index]); | |
} | |
}); | |
return results; | |
}; | |
// Produce an array that contains the union: each distinct element from all of | |
// the passed-in arrays. | |
_.union = function() { | |
return _.uniq(_.flatten(arguments, true)); | |
}; | |
// Produce an array that contains every item shared between all the | |
// passed-in arrays. | |
_.intersection = function(array) { | |
var rest = slice.call(arguments, 1); | |
return _.filter(_.uniq(array), function(item) { | |
return _.every(rest, function(other) { | |
return _.indexOf(other, item) >= 0; | |
}); | |
}); | |
}; | |
// Take the difference between one array and a number of other arrays. | |
// Only the elements present in just the first array will remain. | |
_.difference = function(array) { | |
var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); | |
return _.filter(array, function(value){ return !_.contains(rest, value); }); | |
}; | |
// Zip together multiple lists into a single array -- elements that share | |
// an index go together. | |
_.zip = function() { | |
var length = _.max(_.pluck(arguments, "length").concat(0)); | |
var results = new Array(length); | |
for (var i = 0; i < length; i++) { | |
results[i] = _.pluck(arguments, '' + i); | |
} | |
return results; | |
}; | |
// Converts lists into objects. Pass either a single array of `[key, value]` | |
// pairs, or two parallel arrays of the same length -- one of keys, and one of | |
// the corresponding values. | |
_.object = function(list, values) { | |
if (list == null) return {}; | |
var result = {}; | |
for (var i = 0, l = list.length; i < l; i++) { | |
if (values) { | |
result[list[i]] = values[i]; | |
} else { | |
result[list[i][0]] = list[i][1]; | |
} | |
} | |
return result; | |
}; | |
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), | |
// we need this function. Return the position of the first occurrence of an | |
// item in an array, or -1 if the item is not included in the array. | |
// Delegates to **ECMAScript 5**'s native `indexOf` if available. | |
// If the array is large and already in sort order, pass `true` | |
// for **isSorted** to use binary search. | |
_.indexOf = function(array, item, isSorted) { | |
if (array == null) return -1; | |
var i = 0, l = array.length; | |
if (isSorted) { | |
if (typeof isSorted == 'number') { | |
i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); | |
} else { | |
i = _.sortedIndex(array, item); | |
return array[i] === item ? i : -1; | |
} | |
} | |
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); | |
for (; i < l; i++) if (array[i] === item) return i; | |
return -1; | |
}; | |
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. | |
_.lastIndexOf = function(array, item, from) { | |
if (array == null) return -1; | |
var hasIndex = from != null; | |
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { | |
return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); | |
} | |
var i = (hasIndex ? from : array.length); | |
while (i--) if (array[i] === item) return i; | |
return -1; | |
}; | |
// Generate an integer Array containing an arithmetic progression. A port of | |
// the native Python `range()` function. See | |
// [the Python documentation](http://docs.python.org/library/functions.html#range). | |
_.range = function(start, stop, step) { | |
if (arguments.length <= 1) { | |
stop = start || 0; | |
start = 0; | |
} | |
step = arguments[2] || 1; | |
var len = Math.max(Math.ceil((stop - start) / step), 0); | |
var idx = 0; | |
var range = new Array(len); | |
while(idx < len) { | |
range[idx++] = start; | |
start += step; | |
} | |
return range; | |
}; | |
// Function (ahem) Functions | |
// ------------------ | |
// Reusable constructor function for prototype setting. | |
var ctor = function(){}; | |
// Create a function bound to a given object (assigning `this`, and arguments, | |
// optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if | |
// available. | |
_.bind = function(func, context) { | |
var args, bound; | |
if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); | |
if (!_.isFunction(func)) throw new TypeError; | |
args = slice.call(arguments, 2); | |
return bound = function() { | |
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); | |
ctor.prototype = func.prototype; | |
var self = new ctor; | |
ctor.prototype = null; | |
var result = func.apply(self, args.concat(slice.call(arguments))); | |
if (Object(result) === result) return result; | |
return self; | |
}; | |
}; | |
// Partially apply a function by creating a version that has had some of its | |
// arguments pre-filled, without changing its dynamic `this` context. | |
_.partial = function(func) { | |
var args = slice.call(arguments, 1); | |
return function() { | |
return func.apply(this, args.concat(slice.call(arguments))); | |
}; | |
}; | |
// Bind all of an object's methods to that object. Useful for ensuring that | |
// all callbacks defined on an object belong to it. | |
_.bindAll = function(obj) { | |
var funcs = slice.call(arguments, 1); | |
if (funcs.length === 0) throw new Error("bindAll must be passed function names"); | |
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); | |
return obj; | |
}; | |
// Memoize an expensive function by storing its results. | |
_.memoize = function(func, hasher) { | |
var memo = {}; | |
hasher || (hasher = _.identity); | |
return function() { | |
var key = hasher.apply(this, arguments); | |
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); | |
}; | |
}; | |
// Delays a function for the given number of milliseconds, and then calls | |
// it with the arguments supplied. | |
_.delay = function(func, wait) { | |
var args = slice.call(arguments, 2); | |
return setTimeout(function(){ return func.apply(null, args); }, wait); | |
}; | |
// Defers a function, scheduling it to run after the current call stack has | |
// cleared. | |
_.defer = function(func) { | |
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); | |
}; | |
// Returns a function, that, when invoked, will only be triggered at most once | |
// during a given window of time. Normally, the throttled function will run | |
// as much as it can, without ever going more than once per `wait` duration; | |
// but if you'd like to disable the execution on the leading edge, pass | |
// `{leading: false}`. To disable execution on the trailing edge, ditto. | |
_.throttle = function(func, wait, options) { | |
var context, args, result; | |
var timeout = null; | |
var previous = 0; | |
options || (options = {}); | |
var later = function() { | |
previous = options.leading === false ? 0 : new Date; | |
timeout = null; | |
result = func.apply(context, args); | |
}; | |
return function() { | |
var now = new Date; | |
if (!previous && options.leading === false) previous = now; | |
var remaining = wait - (now - previous); | |
context = this; | |
args = arguments; | |
if (remaining <= 0) { | |
clearTimeout(timeout); | |
timeout = null; | |
previous = now; | |
result = func.apply(context, args); | |
} else if (!timeout && options.trailing !== false) { | |
timeout = setTimeout(later, remaining); | |
} | |
return result; | |
}; | |
}; | |
// Returns a function, that, as long as it continues to be invoked, will not | |
// be triggered. The function will be called after it stops being called for | |
// N milliseconds. If `immediate` is passed, trigger the function on the | |
// leading edge, instead of the trailing. | |
_.debounce = function(func, wait, immediate) { | |
var result; | |
var timeout = null; | |
return function() { | |
var context = this, args = arguments; | |
var later = function() { | |
timeout = null; | |
if (!immediate) result = func.apply(context, args); | |
}; | |
var callNow = immediate && !timeout; | |
clearTimeout(timeout); | |
timeout = setTimeout(later, wait); | |
if (callNow) result = func.apply(context, args); | |
return result; | |
}; | |
}; | |
// Returns a function that will be executed at most one time, no matter how | |
// often you call it. Useful for lazy initialization. | |
_.once = function(func) { | |
var ran = false, memo; | |
return function() { | |
if (ran) return memo; | |
ran = true; | |
memo = func.apply(this, arguments); | |
func = null; | |
return memo; | |
}; | |
}; | |
// Returns the first function passed as an argument to the second, | |
// allowing you to adjust arguments, run code before and after, and | |
// conditionally execute the original function. | |
_.wrap = function(func, wrapper) { | |
return function() { | |
var args = [func]; | |
push.apply(args, arguments); | |
return wrapper.apply(this, args); | |
}; | |
}; | |
// Returns a function that is the composition of a list of functions, each | |
// consuming the return value of the function that follows. | |
_.compose = function() { | |
var funcs = arguments; | |
return function() { | |
var args = arguments; | |
for (var i = funcs.length - 1; i >= 0; i--) { | |
args = [funcs[i].apply(this, args)]; | |
} | |
return args[0]; | |
}; | |
}; | |
// Returns a function that will only be executed after being called N times. | |
_.after = function(times, func) { | |
return function() { | |
if (--times < 1) { | |
return func.apply(this, arguments); | |
} | |
}; | |
}; | |
// Object Functions | |
// ---------------- | |
// Retrieve the names of an object's properties. | |
// Delegates to **ECMAScript 5**'s native `Object.keys` | |
_.keys = nativeKeys || function(obj) { | |
if (obj !== Object(obj)) throw new TypeError('Invalid object'); | |
var keys = []; | |
for (var key in obj) if (_.has(obj, key)) keys.push(key); | |
return keys; | |
}; | |
// Retrieve the values of an object's properties. | |
_.values = function(obj) { | |
var values = []; | |
for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); | |
return values; | |
}; | |
// Convert an object into a list of `[key, value]` pairs. | |
_.pairs = function(obj) { | |
var pairs = []; | |
for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); | |
return pairs; | |
}; | |
// Invert the keys and values of an object. The values must be serializable. | |
_.invert = function(obj) { | |
var result = {}; | |
for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; | |
return result; | |
}; | |
// Return a sorted list of the function names available on the object. | |
// Aliased as `methods` | |
_.functions = _.methods = function(obj) { | |
var names = []; | |
for (var key in obj) { | |
if (_.isFunction(obj[key])) names.push(key); | |
} | |
return names.sort(); | |
}; | |
// Extend a given object with all the properties in passed-in object(s). | |
_.extend = function(obj) { | |
each(slice.call(arguments, 1), function(source) { | |
if (source) { | |
for (var prop in source) { | |
obj[prop] = source[prop]; | |
} | |
} | |
}); | |
return obj; | |
}; | |
// Return a copy of the object only containing the whitelisted properties. | |
_.pick = function(obj) { | |
var copy = {}; | |
var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); | |
each(keys, function(key) { | |
if (key in obj) copy[key] = obj[key]; | |
}); | |
return copy; | |
}; | |
// Return a copy of the object without the blacklisted properties. | |
_.omit = function(obj) { | |
var copy = {}; | |
var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); | |
for (var key in obj) { | |
if (!_.contains(keys, key)) copy[key] = obj[key]; | |
} | |
return copy; | |
}; | |
// Fill in a given object with default properties. | |
_.defaults = function(obj) { | |
each(slice.call(arguments, 1), function(source) { | |
if (source) { | |
for (var prop in source) { | |
if (obj[prop] === void 0) obj[prop] = source[prop]; | |
} | |
} | |
}); | |
return obj; | |
}; | |
// Create a (shallow-cloned) duplicate of an object. | |
_.clone = function(obj) { | |
if (!_.isObject(obj)) return obj; | |
return _.isArray(obj) ? obj.slice() : _.extend({}, obj); | |
}; | |
// Invokes interceptor with the obj, and then returns obj. | |
// The primary purpose of this method is to "tap into" a method chain, in | |
// order to perform operations on intermediate results within the chain. | |
_.tap = function(obj, interceptor) { | |
interceptor(obj); | |
return obj; | |
}; | |
// Internal recursive comparison function for `isEqual`. | |
var eq = function(a, b, aStack, bStack) { | |
// Identical objects are equal. `0 === -0`, but they aren't identical. | |
// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). | |
if (a === b) return a !== 0 || 1 / a == 1 / b; | |
// A strict comparison is necessary because `null == undefined`. | |
if (a == null || b == null) return a === b; | |
// Unwrap any wrapped objects. | |
if (a instanceof _) a = a._wrapped; | |
if (b instanceof _) b = b._wrapped; | |
// Compare `[[Class]]` names. | |
var className = toString.call(a); | |
if (className != toString.call(b)) return false; | |
switch (className) { | |
// Strings, numbers, dates, and booleans are compared by value. | |
case '[object String]': | |
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is | |
// equivalent to `new String("5")`. | |
return a == String(b); | |
case '[object Number]': | |
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for | |
// other numeric values. | |
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); | |
case '[object Date]': | |
case '[object Boolean]': | |
// Coerce dates and booleans to numeric primitive values. Dates are compared by their | |
// millisecond representations. Note that invalid dates with millisecond representations | |
// of `NaN` are not equivalent. | |
return +a == +b; | |
// RegExps are compared by their source patterns and flags. | |
case '[object RegExp]': | |
return a.source == b.source && | |
a.global == b.global && | |
a.multiline == b.multiline && | |
a.ignoreCase == b.ignoreCase; | |
} | |
if (typeof a != 'object' || typeof b != 'object') return false; | |
// Assume equality for cyclic structures. The algorithm for detecting cyclic | |
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. | |
var length = aStack.length; | |
while (length--) { | |
// Linear search. Performance is inversely proportional to the number of | |
// unique nested structures. | |
if (aStack[length] == a) return bStack[length] == b; | |
} | |
// Objects with different constructors are not equivalent, but `Object`s | |
// from different frames are. | |
var aCtor = a.constructor, bCtor = b.constructor; | |
if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && | |
_.isFunction(bCtor) && (bCtor instanceof bCtor))) { | |
return false; | |
} | |
// Add the first object to the stack of traversed objects. | |
aStack.push(a); | |
bStack.push(b); | |
var size = 0, result = true; | |
// Recursively compare objects and arrays. | |
if (className == '[object Array]') { | |
// Compare array lengths to determine if a deep comparison is necessary. | |
size = a.length; | |
result = size == b.length; | |
if (result) { | |
// Deep compare the contents, ignoring non-numeric properties. | |
while (size--) { | |
if (!(result = eq(a[size], b[size], aStack, bStack))) break; | |
} | |
} | |
} else { | |
// Deep compare objects. | |
for (var key in a) { | |
if (_.has(a, key)) { | |
// Count the expected number of properties. | |
size++; | |
// Deep compare each member. | |
if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; | |
} | |
} | |
// Ensure that both objects contain the same number of properties. | |
if (result) { | |
for (key in b) { | |
if (_.has(b, key) && !(size--)) break; | |
} | |
result = !size; | |
} | |
} | |
// Remove the first object from the stack of traversed objects. | |
aStack.pop(); | |
bStack.pop(); | |
return result; | |
}; | |
// Perform a deep comparison to check if two objects are equal. | |
_.isEqual = function(a, b) { | |
return eq(a, b, [], []); | |
}; | |
// Is a given array, string, or object empty? | |
// An "empty" object has no enumerable own-properties. | |
_.isEmpty = function(obj) { | |
if (obj == null) return true; | |
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; | |
for (var key in obj) if (_.has(obj, key)) return false; | |
return true; | |
}; | |
// Is a given value a DOM element? | |
_.isElement = function(obj) { | |
return !!(obj && obj.nodeType === 1); | |
}; | |
// Is a given value an array? | |
// Delegates to ECMA5's native Array.isArray | |
_.isArray = nativeIsArray || function(obj) { | |
return toString.call(obj) == '[object Array]'; | |
}; | |
// Is a given variable an object? | |
_.isObject = function(obj) { | |
return obj === Object(obj); | |
}; | |
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. | |
each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { | |
_['is' + name] = function(obj) { | |
return toString.call(obj) == '[object ' + name + ']'; | |
}; | |
}); | |
// Define a fallback version of the method in browsers (ahem, IE), where | |
// there isn't any inspectable "Arguments" type. | |
if (!_.isArguments(arguments)) { | |
_.isArguments = function(obj) { | |
return !!(obj && _.has(obj, 'callee')); | |
}; | |
} | |
// Optimize `isFunction` if appropriate. | |
if (typeof (/./) !== 'function') { | |
_.isFunction = function(obj) { | |
return typeof obj === 'function'; | |
}; | |
} | |
// Is a given object a finite number? | |
_.isFinite = function(obj) { | |
return isFinite(obj) && !isNaN(parseFloat(obj)); | |
}; | |
// Is the given value `NaN`? (NaN is the only number which does not equal itself). | |
_.isNaN = function(obj) { | |
return _.isNumber(obj) && obj != +obj; | |
}; | |
// Is a given value a boolean? | |
_.isBoolean = function(obj) { | |
return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; | |
}; | |
// Is a given value equal to null? | |
_.isNull = function(obj) { | |
return obj === null; | |
}; | |
// Is a given variable undefined? | |
_.isUndefined = function(obj) { | |
return obj === void 0; | |
}; | |
// Shortcut function for checking if an object has a given property directly | |
// on itself (in other words, not on a prototype). | |
_.has = function(obj, key) { | |
return hasOwnProperty.call(obj, key); | |
}; | |
// Utility Functions | |
// ----------------- | |
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its | |
// previous owner. Returns a reference to the Underscore object. | |
_.noConflict = function() { | |
root._ = previousUnderscore; | |
return this; | |
}; | |
// Keep the identity function around for default iterators. | |
_.identity = function(value) { | |
return value; | |
}; | |
// Run a function **n** times. | |
_.times = function(n, iterator, context) { | |
var accum = Array(Math.max(0, n)); | |
for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); | |
return accum; | |
}; | |
// Return a random integer between min and max (inclusive). | |
_.random = function(min, max) { | |
if (max == null) { | |
max = min; | |
min = 0; | |
} | |
return min + Math.floor(Math.random() * (max - min + 1)); | |
}; | |
// List of HTML entities for escaping. | |
var entityMap = { | |
escape: { | |
'&': '&', | |
'<': '<', | |
'>': '>', | |
'"': '"', | |
"'": ''', | |
'/': '/' | |
} | |
}; | |
entityMap.unescape = _.invert(entityMap.escape); | |
// Regexes containing the keys and values listed immediately above. | |
var entityRegexes = { | |
escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), | |
unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') | |
}; | |
// Functions for escaping and unescaping strings to/from HTML interpolation. | |
_.each(['escape', 'unescape'], function(method) { | |
_[method] = function(string) { | |
if (string == null) return ''; | |
return ('' + string).replace(entityRegexes[method], function(match) { | |
return entityMap[method][match]; | |
}); | |
}; | |
}); | |
// If the value of the named `property` is a function then invoke it with the | |
// `object` as context; otherwise, return it. | |
_.result = function(object, property) { | |
if (object == null) return void 0; | |
var value = object[property]; | |
return _.isFunction(value) ? value.call(object) : value; | |
}; | |
// Add your own custom functions to the Underscore object. | |
_.mixin = function(obj) { | |
each(_.functions(obj), function(name){ | |
var func = _[name] = obj[name]; | |
_.prototype[name] = function() { | |
var args = [this._wrapped]; | |
push.apply(args, arguments); | |
return result.call(this, func.apply(_, args)); | |
}; | |
}); | |
}; | |
// Generate a unique integer id (unique within the entire client session). | |
// Useful for temporary DOM ids. | |
var idCounter = 0; | |
_.uniqueId = function(prefix) { | |
var id = ++idCounter + ''; | |
return prefix ? prefix + id : id; | |
}; | |
// By default, Underscore uses ERB-style template delimiters, change the | |
// following template settings to use alternative delimiters. | |
_.templateSettings = { | |
evaluate : /<%([\s\S]+?)%>/g, | |
interpolate : /<%=([\s\S]+?)%>/g, | |
escape : /<%-([\s\S]+?)%>/g | |
}; | |
// When customizing `templateSettings`, if you don't want to define an | |
// interpolation, evaluation or escaping regex, we need one that is | |
// guaranteed not to match. | |
var noMatch = /(.)^/; | |
// Certain characters need to be escaped so that they can be put into a | |
// string literal. | |
var escapes = { | |
"'": "'", | |
'\\': '\\', | |
'\r': 'r', | |
'\n': 'n', | |
'\t': 't', | |
'\u2028': 'u2028', | |
'\u2029': 'u2029' | |
}; | |
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; | |
// JavaScript micro-templating, similar to John Resig's implementation. | |
// Underscore templating handles arbitrary delimiters, preserves whitespace, | |
// and correctly escapes quotes within interpolated code. | |
_.template = function(text, data, settings) { | |
var render; | |
settings = _.defaults({}, settings, _.templateSettings); | |
// Combine delimiters into one regular expression via alternation. | |
var matcher = new RegExp([ | |
(settings.escape || noMatch).source, | |
(settings.interpolate || noMatch).source, | |
(settings.evaluate || noMatch).source | |
].join('|') + '|$', 'g'); | |
// Compile the template source, escaping string literals appropriately. | |
var index = 0; | |
var source = "__p+='"; | |
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { | |
source += text.slice(index, offset) | |
.replace(escaper, function(match) { return '\\' + escapes[match]; }); | |
if (escape) { | |
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; | |
} | |
if (interpolate) { | |
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; | |
} | |
if (evaluate) { | |
source += "';\n" + evaluate + "\n__p+='"; | |
} | |
index = offset + match.length; | |
return match; | |
}); | |
source += "';\n"; | |
// If a variable is not specified, place data values in local scope. | |
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; | |
source = "var __t,__p='',__j=Array.prototype.join," + | |
"print=function(){__p+=__j.call(arguments,'');};\n" + | |
source + "return __p;\n"; | |
try { | |
render = new Function(settings.variable || 'obj', '_', source); | |
} catch (e) { | |
e.source = source; | |
throw e; | |
} | |
if (data) return render(data, _); | |
var template = function(data) { | |
return render.call(this, data, _); | |
}; | |
// Provide the compiled function source as a convenience for precompilation. | |
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; | |
return template; | |
}; | |
// Add a "chain" function, which will delegate to the wrapper. | |
_.chain = function(obj) { | |
return _(obj).chain(); | |
}; | |
// OOP | |
// --------------- | |
// If Underscore is called as a function, it returns a wrapped object that | |
// can be used OO-style. This wrapper holds altered versions of all the | |
// underscore functions. Wrapped objects may be chained. | |
// Helper function to continue chaining intermediate results. | |
var result = function(obj) { | |
return this._chain ? _(obj).chain() : obj; | |
}; | |
// Add all of the Underscore functions to the wrapper object. | |
_.mixin(_); | |
// Add all mutator Array functions to the wrapper. | |
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { | |
var method = ArrayProto[name]; | |
_.prototype[name] = function() { | |
var obj = this._wrapped; | |
method.apply(obj, arguments); | |
if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; | |
return result.call(this, obj); | |
}; | |
}); | |
// Add all accessor Array functions to the wrapper. | |
each(['concat', 'join', 'slice'], function(name) { | |
var method = ArrayProto[name]; | |
_.prototype[name] = function() { | |
return result.call(this, method.apply(this._wrapped, arguments)); | |
}; | |
}); | |
_.extend(_.prototype, { | |
// Start chaining a wrapped Underscore object. | |
chain: function() { | |
this._chain = true; | |
return this; | |
}, | |
// Extracts the result from a wrapped and chained object. | |
value: function() { | |
return this._wrapped; | |
} | |
}); | |
}).call(this); | |
/** | |
* The Events module pulled from [Backbone.js](http://backbonejs.org/) | |
* Stripped and modified to work with node.js and optimize types of calls | |
* for animation based events. | |
*/ | |
var Backbone = Backbone || {}; | |
(function() { | |
var array = []; | |
var slice = array.slice; | |
// Backbone.Events | |
// --------------- | |
// Regular expression used to split event strings. | |
var eventSplitter = /\s+/; | |
/** | |
* Events API deprecated because of additional calls and checks | |
* multiple times a frame tick in two.js | |
*/ | |
// Optimized internal dispatch function for triggering events. Tries to | |
// keep the usual cases speedy (most Backbone events have 3 arguments). | |
var triggerEvents = function(obj, events, args) { | |
var ev, i = -1, l = events.length; | |
switch (args.length) { | |
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); | |
return; | |
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]); | |
return; | |
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]); | |
return; | |
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]); | |
return; | |
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); | |
} | |
}; | |
var Events = Backbone.Events = { | |
// Bind one or more space separated events, or an events map, | |
// to a `callback` function. Passing `"all"` will bind the callback to | |
// all events fired. | |
on: function(name, callback, context) { | |
// if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this; | |
this._events || (this._events = {}); | |
var list = this._events[name] || (this._events[name] = []); | |
list.push({callback: callback, context: context, ctx: context || this}); | |
return this; | |
}, | |
// Bind events to only be triggered a single time. After the first time | |
// the callback is invoked, it will be removed. | |
once: function(name, callback, context) { | |
// if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this; | |
var self = this; | |
var once = _.once(function() { | |
self.off(name, once); | |
callback.apply(this, arguments); | |
}); | |
once._callback = callback; | |
this.on(name, once, context); | |
return this; | |
}, | |
// Remove one or many callbacks. If `context` is null, removes all | |
// callbacks with that function. If `callback` is null, removes all | |
// callbacks for the event. If `events` is null, removes all bound | |
// callbacks for all events. | |
off: function(name, callback, context) { | |
var list, ev, events, names, i, l, j, k; | |
if (!this._events/** || !eventsApi(this, 'off', name, [callback, context])**/) return this; | |
if (!name && !callback && !context) { | |
this._events = {}; | |
return this; | |
} | |
names = name ? [name] : _.keys(this._events); | |
for (i = 0, l = names.length; i < l; i++) { | |
name = names[i]; | |
if (list = this._events[name]) { | |
events = []; | |
if (callback || context) { | |
for (j = 0, k = list.length; j < k; j++) { | |
ev = list[j]; | |
if ((callback && callback !== (ev.callback._callback || ev.callback)) || | |
(context && context !== ev.context)) { | |
events.push(ev); | |
} | |
} | |
} | |
this._events[name] = events; | |
} | |
} | |
return this; | |
}, | |
// Trigger one or many events, firing all bound callbacks. Callbacks are | |
// passed the same arguments as `trigger` is, apart from the event name | |
// (unless you're listening on `"all"`, which will cause your callback to | |
// receive the true name of the event as the first argument). | |
trigger: function(name) { | |
if (!this._events) return this; | |
var args = slice.call(arguments, 1); | |
// if (!eventsApi(this, 'trigger', name, args)) return this; | |
var events = this._events[name]; | |
var allEvents = this._events.all; | |
if (events) triggerEvents(this, events, args); | |
if (allEvents) triggerEvents(this, allEvents, arguments); | |
return this; | |
}, | |
// An inversion-of-control version of `on`. Tell *this* object to listen to | |
// an event in another object ... keeping track of what it's listening to. | |
listenTo: function(object, events, callback) { | |
var listeners = this._listeners || (this._listeners = {}); | |
var id = object._listenerId || (object._listenerId = _.uniqueId('l')); | |
listeners[id] = object; | |
object.on(events, callback || this, this); | |
return this; | |
}, | |
// Tell this object to stop listening to either specific events ... or | |
// to every object it's currently listening to. | |
stopListening: function(object, events, callback) { | |
var listeners = this._listeners; | |
if (!listeners) return; | |
if (object) { | |
object.off(events, callback, this); | |
if (!events && !callback) delete listeners[object._listenerId]; | |
} else { | |
for (var id in listeners) { | |
listeners[id].off(null, null, this); | |
} | |
this._listeners = {}; | |
} | |
return this; | |
} | |
}; | |
// Aliases for backwards compatibility. | |
Events.bind = Events.on; | |
Events.unbind = Events.off; | |
if (typeof exports !== 'undefined') { | |
if (typeof module !== 'undefined' && module.exports) { | |
exports = module.exports = Events; | |
} | |
exports.Backbone = exports.Backbone || Backbone; | |
} | |
})(); | |
/** | |
* http://paulirish.com/2011/requestanimationframe-for-smart-animating/ | |
* And modified to work with node.js | |
*/ | |
(function() { | |
var root = this; | |
var lastTime = 0; | |
var vendors = ['ms', 'moz', 'webkit', 'o']; | |
if (typeof exports !== 'undefined') { | |
if (typeof module !== 'undefined' && module.exports) { | |
exports = module.exports = raf; | |
} | |
exports.requestAnimationFrame = raf; | |
return; | |
} | |
for (var x = 0; x < vendors.length && !root.requestAnimationFrame; ++x) { | |
root.requestAnimationFrame = root[vendors[x]+'RequestAnimationFrame']; | |
root.cancelAnimationFrame = | |
root[vendors[x]+'CancelAnimationFrame'] || root[vendors[x]+'CancelRequestAnimationFrame']; | |
} | |
if (!root.requestAnimationFrame) | |
root.requestAnimationFrame = raf; | |
if (!root.cancelAnimationFrame) | |
root.cancelAnimationFrame = function(id) { | |
clearTimeout(id); | |
}; | |
function raf(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; | |
} | |
}()); | |
(function() { | |
var root = this; | |
var previousTwo = root.Two || {}; | |
/** | |
* Constants | |
*/ | |
var sin = Math.sin, | |
cos = Math.cos, | |
atan2 = Math.atan2, | |
sqrt = Math.sqrt, | |
round = Math.round, | |
abs = Math.abs, | |
PI = Math.PI, | |
TWO_PI = PI * 2, | |
HALF_PI = PI / 2, | |
pow = Math.pow, | |
min = Math.min, | |
max = Math.max; | |
/** | |
* Localized variables | |
*/ | |
var count = 0; | |
/** | |
* Cross browser dom events. | |
*/ | |
var dom = { | |
hasEventListeners: _.isFunction(root.addEventListener), | |
bind: function(elem, event, func, bool) { | |
if (this.hasEventListeners) { | |
elem.addEventListener(event, func, !!bool); | |
} else { | |
elem.attachEvent('on' + event, func); | |
} | |
return this; | |
}, | |
unbind: function(elem, event, func, bool) { | |
if (this.hasEventListeners) { | |
elem.removeEventListeners(event, func, !!bool); | |
} else { | |
elem.detachEvent('on' + event, func); | |
} | |
return this; | |
} | |
}; | |
/** | |
* @class | |
*/ | |
var Two = root.Two = function(options) { | |
// Determine what Renderer to use and setup a scene. | |
var params = _.defaults(options || {}, { | |
fullscreen: false, | |
width: 640, | |
height: 480, | |
type: Two.Types.svg, | |
autostart: false | |
}); | |
_.each(params, function(v, k) { | |
if (k === 'fullscreen' || k === 'width' || k === 'height' || k === 'autostart') { | |
return; | |
} | |
this[k] = v; | |
}, this); | |
// Specified domElement overrides type declaration. | |
if (_.isElement(params.domElement)) { | |
this.type = Two.Types[params.domElement.tagName.toLowerCase()]; | |
} | |
this.renderer = new Two[this.type](this); | |
Two.Utils.setPlaying.call(this, params.autostart); | |
this.frameCount = 0; | |
if (params.fullscreen) { | |
var fitted = _.bind(fitToWindow, this); | |
_.extend(document.body.style, { | |
overflow: 'hidden', | |
margin: 0, | |
padding: 0, | |
top: 0, | |
left: 0, | |
right: 0, | |
bottom: 0, | |
position: 'fixed' | |
}); | |
_.extend(this.renderer.domElement.style, { | |
display: 'block', | |
top: 0, | |
left: 0, | |
right: 0, | |
bottom: 0, | |
position: 'fixed' | |
}); | |
dom.bind(root, 'resize', fitted); | |
fitted(); | |
} else if (!_.isElement(params.domElement)) { | |
this.renderer.setSize(params.width, params.height, this.ratio); | |
this.width = params.width; | |
this.height = params.height; | |
} | |
this.scene = this.renderer.scene; | |
Two.Instances.push(this); | |
}; | |
_.extend(Two, { | |
/** | |
* Primitive | |
*/ | |
Array: root.Float32Array || Array, | |
Types: { | |
webgl: 'WebGLRenderer', | |
svg: 'SVGRenderer', | |
canvas: 'CanvasRenderer' | |
}, | |
Version: 'v0.4.0', | |
Identifier: 'two_', | |
Properties: { | |
hierarchy: 'hierarchy', | |
demotion: 'demotion' | |
}, | |
Events: { | |
play: 'play', | |
pause: 'pause', | |
update: 'update', | |
render: 'render', | |
resize: 'resize', | |
change: 'change', | |
remove: 'remove', | |
insert: 'insert' | |
}, | |
Commands: { | |
move: 'M', | |
line: 'L', | |
curve: 'C', | |
close: 'Z' | |
}, | |
Resolution: 8, | |
Instances: [], | |
noConflict: function() { | |
root.Two = previousTwo; | |
return this; | |
}, | |
uniqueId: function() { | |
var id = count; | |
count++; | |
return id; | |
}, | |
Utils: { | |
/** | |
* Release an arbitrary class' events from the two.js corpus and recurse | |
* through its children and or vertices. | |
*/ | |
release: function(obj) { | |
if (!_.isObject(obj)) { | |
return; | |
} | |
if (_.isFunction(obj.unbind)) { | |
obj.unbind(); | |
} | |
if (obj.vertices) { | |
if (_.isFunction(obj.vertices.unbind)) { | |
obj.vertices.unbind(); | |
} | |
_.each(obj.vertices, function(v) { | |
if (_.isFunction(v.unbind)) { | |
v.unbind(); | |
} | |
}); | |
} | |
if (obj.children) { | |
_.each(obj.children, function(obj) { | |
Two.Utils.release(obj); | |
}); | |
} | |
}, | |
Curve: { | |
CollinearityEpsilon: pow(10, -30), | |
RecursionLimit: 16, | |
CuspLimit: 0, | |
Tolerance: { | |
distance: 0.25, | |
angle: 0, | |
epsilon: 0.01 | |
}, | |
// Lookup tables for abscissas and weights with values for n = 2 .. 16. | |
// As values are symmetric, only store half of them and adapt algorithm | |
// to factor in symmetry. | |
abscissas: [ | |
[ 0.5773502691896257645091488], | |
[0,0.7745966692414833770358531], | |
[ 0.3399810435848562648026658,0.8611363115940525752239465], | |
[0,0.5384693101056830910363144,0.9061798459386639927976269], | |
[ 0.2386191860831969086305017,0.6612093864662645136613996,0.9324695142031520278123016], | |
[0,0.4058451513773971669066064,0.7415311855993944398638648,0.9491079123427585245261897], | |
[ 0.1834346424956498049394761,0.5255324099163289858177390,0.7966664774136267395915539,0.9602898564975362316835609], | |
[0,0.3242534234038089290385380,0.6133714327005903973087020,0.8360311073266357942994298,0.9681602395076260898355762], | |
[ 0.1488743389816312108848260,0.4333953941292471907992659,0.6794095682990244062343274,0.8650633666889845107320967,0.9739065285171717200779640], | |
[0,0.2695431559523449723315320,0.5190961292068118159257257,0.7301520055740493240934163,0.8870625997680952990751578,0.9782286581460569928039380], | |
[ 0.1252334085114689154724414,0.3678314989981801937526915,0.5873179542866174472967024,0.7699026741943046870368938,0.9041172563704748566784659,0.9815606342467192506905491], | |
[0,0.2304583159551347940655281,0.4484927510364468528779129,0.6423493394403402206439846,0.8015780907333099127942065,0.9175983992229779652065478,0.9841830547185881494728294], | |
[ 0.1080549487073436620662447,0.3191123689278897604356718,0.5152486363581540919652907,0.6872929048116854701480198,0.8272013150697649931897947,0.9284348836635735173363911,0.9862838086968123388415973], | |
[0,0.2011940939974345223006283,0.3941513470775633698972074,0.5709721726085388475372267,0.7244177313601700474161861,0.8482065834104272162006483,0.9372733924007059043077589,0.9879925180204854284895657], | |
[ 0.0950125098376374401853193,0.2816035507792589132304605,0.4580167776572273863424194,0.6178762444026437484466718,0.7554044083550030338951012,0.8656312023878317438804679,0.9445750230732325760779884,0.9894009349916499325961542] | |
], | |
weights: [ | |
[1], | |
[0.8888888888888888888888889,0.5555555555555555555555556], | |
[0.6521451548625461426269361,0.3478548451374538573730639], | |
[0.5688888888888888888888889,0.4786286704993664680412915,0.2369268850561890875142640], | |
[0.4679139345726910473898703,0.3607615730481386075698335,0.1713244923791703450402961], | |
[0.4179591836734693877551020,0.3818300505051189449503698,0.2797053914892766679014678,0.1294849661688696932706114], | |
[0.3626837833783619829651504,0.3137066458778872873379622,0.2223810344533744705443560,0.1012285362903762591525314], | |
[0.3302393550012597631645251,0.3123470770400028400686304,0.2606106964029354623187429,0.1806481606948574040584720,0.0812743883615744119718922], | |
[0.2955242247147528701738930,0.2692667193099963550912269,0.2190863625159820439955349,0.1494513491505805931457763,0.0666713443086881375935688], | |
[0.2729250867779006307144835,0.2628045445102466621806889,0.2331937645919904799185237,0.1862902109277342514260976,0.1255803694649046246346943,0.0556685671161736664827537], | |
[0.2491470458134027850005624,0.2334925365383548087608499,0.2031674267230659217490645,0.1600783285433462263346525,0.1069393259953184309602547,0.0471753363865118271946160], | |
[0.2325515532308739101945895,0.2262831802628972384120902,0.2078160475368885023125232,0.1781459807619457382800467,0.1388735102197872384636018,0.0921214998377284479144218,0.0404840047653158795200216], | |
[0.2152638534631577901958764,0.2051984637212956039659241,0.1855383974779378137417166,0.1572031671581935345696019,0.1215185706879031846894148,0.0801580871597602098056333,0.0351194603317518630318329], | |
[0.2025782419255612728806202,0.1984314853271115764561183,0.1861610000155622110268006,0.1662692058169939335532009,0.1395706779261543144478048,0.1071592204671719350118695,0.0703660474881081247092674,0.0307532419961172683546284], | |
[0.1894506104550684962853967,0.1826034150449235888667637,0.1691565193950025381893121,0.1495959888165767320815017,0.1246289712555338720524763,0.0951585116824927848099251,0.0622535239386478928628438,0.0271524594117540948517806] | |
] | |
}, | |
/** | |
* Account for high dpi rendering. | |
* http://www.html5rocks.com/en/tutorials/canvas/hidpi/ | |
*/ | |
devicePixelRatio: root.devicePixelRatio || 1, | |
getBackingStoreRatio: function(ctx) { | |
return ctx.webkitBackingStorePixelRatio || | |
ctx.mozBackingStorePixelRatio || | |
ctx.msBackingStorePixelRatio || | |
ctx.oBackingStorePixelRatio || | |
ctx.backingStorePixelRatio || 1; | |
}, | |
getRatio: function(ctx) { | |
return Two.Utils.devicePixelRatio / getBackingStoreRatio(ctx); | |
}, | |
/** | |
* Properly defer play calling until after all objects | |
* have been updated with their newest styles. | |
*/ | |
setPlaying: function(b) { | |
this.playing = !!b; | |
return this; | |
}, | |
/** | |
* Return the computed matrix of a nested object. | |
* TODO: Optimize traversal. | |
*/ | |
getComputedMatrix: function(object, matrix) { | |
matrix = (matrix && matrix.identity()) || new Two.Matrix(); | |
var parent = object, matrices = []; | |
while (parent && parent._matrix) { | |
matrices.push(parent._matrix); | |
parent = parent.parent; | |
} | |
matrices.reverse(); | |
_.each(matrices, function(m) { | |
var e = m.elements; | |
matrix.multiply( | |
e[0], e[1], e[2], e[3], e[4], e[5], e[6], e[7], e[8], e[9]); | |
}); | |
return matrix; | |
}, | |
deltaTransformPoint: function(matrix, x, y) { | |
var dx = x * matrix.a + y * matrix.c + 0; | |
var dy = x * matrix.b + y * matrix.d + 0; | |
return new Two.Vector(dx, dy); | |
}, | |
/** | |
* https://gist.github.com/2052247 | |
*/ | |
decomposeMatrix: function(matrix) { | |
// calculate delta transform point | |
var px = Two.Utils.deltaTransformPoint(matrix, 0, 1); | |
var py = Two.Utils.deltaTransformPoint(matrix, 1, 0); | |
// calculate skew | |
var skewX = ((180 / Math.PI) * Math.atan2(px.y, px.x) - 90); | |
var skewY = ((180 / Math.PI) * Math.atan2(py.y, py.x)); | |
return { | |
translateX: matrix.e, | |
translateY: matrix.f, | |
scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b), | |
scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d), | |
skewX: skewX, | |
skewY: skewY, | |
rotation: skewX // rotation is the same as skew x | |
}; | |
}, | |
/** | |
* Walk through item properties and pick the ones of interest. | |
* Will try to resolve styles applied via CSS | |
*/ | |
applySvgAttributes: function(node, elem) { | |
var attributes = {}, styles = {}, i, key, value, attr; | |
// Not available in non browser environments | |
if (getComputedStyle) { | |
// Convert CSSStyleDeclaration to a normal object | |
var computedStyles = getComputedStyle(node); | |
i = computedStyles.length; | |
while(i--) { | |
key = computedStyles[i]; | |
value = computedStyles[key]; | |
// Gecko returns undefined for unset properties | |
// Webkit returns the default value | |
if (value !== undefined) { | |
styles[key] = value; | |
} | |
} | |
} | |
// Convert NodeMap to a normal object | |
i = node.attributes.length; | |
while(i--) { | |
attr = node.attributes[i]; | |
attributes[attr.nodeName] = attr.value; | |
} | |
// Getting the correct opacity is a bit tricky, since SVG path elements don't | |
// support opacity as an attribute, but you can apply it via CSS. | |
// So we take the opacity and set (stroke/fill)-opacity to the same value. | |
if (!_.isUndefined(styles.opacity)) { | |
styles['stroke-opacity'] = styles.opacity; | |
styles['fill-opacity'] = styles.opacity; | |
} | |
// Merge attributes and applied styles (attributes take precedence) | |
_.extend(styles, attributes); | |
// Similarly visibility is influenced by the value of both display and visibility. | |
// Calculate a unified value here | |
styles.visible = (styles.display !== 'none') && (styles.visibility === 'visible'); | |
// Now iterate the whole thing | |
for (key in styles) { | |
value = styles[key]; | |
switch (key) { | |
case 'transform': | |
if (value === 'none') break; | |
var m = node.getCTM(); | |
// Might happen when transform string is empty or not valid. | |
if (m === null) break; | |
var matrix = new Two.Matrix(m.a, m.b, m.c, m.d, m.e, m.f); | |
// Option 1: edit the underlying matrix and don't force an auto calc. | |
// var m = node.getCTM(); | |
// elem._matrix.manual = true; | |
// elem._matrix.set(m.a, m.b, m.c, m.d, m.e, m.f); | |
// Option 2: Decompose and infer Two.js related properties. | |
var transforms = Two.Utils.decomposeMatrix(node.getCTM()); | |
elem.translation.set(transforms.translateX, transforms.translateY); | |
elem.rotation = transforms.rotation; | |
// Warning: Two.js elements only support uniform scalars... | |
elem.scale = transforms.scaleX; | |
// Override based on attributes. | |
if (styles.x) { | |
elem.translation.x = styles.x; | |
} | |
if (styles.y) { | |
elem.translation.y = styles.y; | |
} | |
break; | |
case 'visible': | |
elem.visible = value; | |
break; | |
case 'stroke-linecap': | |
elem.cap = value; | |
break; | |
case 'stroke-linejoin': | |
elem.join = value; | |
break; | |
case 'stroke-miterlimit': | |
elem.miter = value; | |
break; | |
case 'stroke-width': | |
elem.linewidth = parseFloat(value); | |
break; | |
case 'stroke-opacity': | |
case 'fill-opacity': | |
case 'opacity': | |
elem.opacity = parseFloat(value); | |
break; | |
case 'fill': | |
case 'stroke': | |
elem[key] = (value === 'none') ? 'transparent' : value; | |
break; | |
case 'id': | |
elem.id = value; | |
break; | |
case 'class': | |
elem.classList = value.split(' '); | |
break; | |
} | |
} | |
return elem; | |
}, | |
/** | |
* Read any number of SVG node types and create Two equivalents of them. | |
*/ | |
read: { | |
svg: function() { | |
return Two.Utils.read.g.apply(this, arguments); | |
}, | |
g: function(node) { | |
var group = new Two.Group(); | |
// Switched up order to inherit more specific styles | |
Two.Utils.applySvgAttributes(node, group); | |
for (var i = 0, l = node.childNodes.length; i < l; i++) { | |
var n = node.childNodes[i]; | |
var tag = n.nodeName; | |
if (!tag) return; | |
var tagName = tag.replace(/svg\:/ig, '').toLowerCase(); | |
if (tagName in Two.Utils.read) { | |
var o = Two.Utils.read[tagName].call(this, n); | |
group.add(o); | |
} | |
} | |
return group; | |
}, | |
polygon: function(node, open) { | |
var points = node.getAttribute('points'); | |
var verts = []; | |
points.replace(/(-?[\d\.?]+),(-?[\d\.?]+)/g, function(match, p1, p2) { | |
verts.push(new Two.Anchor(parseFloat(p1), parseFloat(p2))); | |
}); | |
var poly = new Two.Polygon(verts, !open).noStroke(); | |
poly.fill = 'black'; | |
return Two.Utils.applySvgAttributes(node, poly); | |
}, | |
polyline: function(node) { | |
return Two.Utils.read.polygon(node, true); | |
}, | |
path: function(node) { | |
var path = node.getAttribute('d'); | |
// Create a Two.Polygon from the paths. | |
var coord = new Two.Anchor(), control; | |
var coords, relative = false; | |
var closed = false; | |
var commands = path.match(/[a-df-z][^a-df-z]*/ig); | |
var last = commands.length - 1; | |
// Go through commands and look for Inkscape irregularities | |
_.each(commands.slice(0), function(command, i) { | |
var type = command[0]; | |
var lower = type.toLowerCase(); | |
var items = command.slice(1).trim().split(/[\s,]+|(?=\s?[+\-])/); | |
var pre, post, result = [], bin; | |
if (i <= 0) { | |
commands = []; | |
} | |
switch (lower) { | |
case 'm': | |
case 'l': | |
case 'h': | |
case 'v': | |
if (items.length > 2) { | |
bin = 2; | |
} | |
break; | |
case 'c': | |
case 's': | |
case 't': | |
case 'q': | |
if (items.length > 6) { | |
bin = 6; | |
} | |
break; | |
case 'a': | |
// TODO: Handle Ellipses | |
break; | |
} | |
if (bin) { | |
for (var j = 0, l = items.length; j < l; j+=bin) { | |
result.push([type].concat(items.slice(j, j + bin)).join(' ')); | |
} | |
commands = Array.prototype.concat.apply(commands, result); | |
} else { | |
commands.push(command); | |
} | |
}); | |
// Create the vertices for our Two.Polygon | |
var points = _.flatten(_.map(commands, function(command, i) { | |
var result, x, y; | |
var type = command[0]; | |
var lower = type.toLowerCase(); | |
coords = command.slice(1).trim(); | |
coords = coords.replace(/(-?\d+(?:\.\d*)?)[eE]([+\-]?\d+)/g, function(match, n1, n2) { | |
return parseFloat(n1) * pow(10, n2); | |
}); | |
coords = coords.split(/[\s,]+|(?=\s?[+\-])/); | |
relative = type === lower; | |
var x1, y1, x2, y2, x3, y3, x4, y4, reflection; | |
switch (lower) { | |
case 'z': | |
if (i >= last) { | |
closed = true; | |
} else { | |
x = coord.x; | |
y = coord.y; | |
result = new Two.Anchor( | |
x, y, | |
undefined, undefined, | |
undefined, undefined, | |
Two.Commands.close | |
); | |
} | |
break; | |
case 'm': | |
case 'l': | |
x = parseFloat(coords[0]); | |
y = parseFloat(coords[1]); | |
result = new Two.Anchor( | |
x, y, | |
undefined, undefined, | |
undefined, undefined, | |
lower === 'm' ? Two.Commands.move : Two.Commands.line | |
); | |
if (relative) { | |
result.addSelf(coord); | |
} | |
// result.controls.left.copy(result); | |
// result.controls.right.copy(result); | |
coord = result; | |
break; | |
case 'h': | |
case 'v': | |
var a = lower === 'h' ? 'x' : 'y'; | |
var b = a === 'x' ? 'y' : 'x'; | |
result = new Two.Anchor( | |
undefined, undefined, | |
undefined, undefined, | |
undefined, undefined, | |
Two.Commands.line | |
); | |
result[a] = parseFloat(coords[0]); | |
result[b] = coord[b]; | |
if (relative) { | |
result[a] += coord[a]; | |
} | |
// result.controls.left.copy(result); | |
// result.controls.right.copy(result); | |
coord = result; | |
break; | |
case 'c': | |
case 's': | |
x1 = coord.x; | |
y1 = coord.y; | |
if (!control) { | |
control = new Two.Vector().copy(coord); | |
} | |
if (lower === 'c') { | |
x2 = parseFloat(coords[0]); | |
y2 = parseFloat(coords[1]); | |
x3 = parseFloat(coords[2]); | |
y3 = parseFloat(coords[3]); | |
x4 = parseFloat(coords[4]); | |
y4 = parseFloat(coords[5]); | |
} else { | |
// Calculate reflection control point for proper x2, y2 | |
// inclusion. | |
reflection = Two.Utils.getReflection(coord, control, relative); | |
x2 = reflection.x; | |
y2 = reflection.y; | |
x3 = parseFloat(coords[0]); | |
y3 = parseFloat(coords[1]); | |
x4 = parseFloat(coords[2]); | |
y4 = parseFloat(coords[3]); | |
} | |
if (relative) { | |
x2 += x1; | |
y2 += y1; | |
x3 += x1; | |
y3 += y1; | |
x4 += x1; | |
y4 += y1; | |
} | |
if (!_.isObject(coord.controls)) { | |
Two.Anchor.AppendCurveProperties(coord); | |
} | |
coord.controls.right.set(x2 - coord.x, y2 - coord.y); | |
result = new Two.Anchor( | |
x4, y4, | |
x3 - x4, y3 - y4, | |
undefined, undefined, | |
Two.Commands.curve | |
); | |
coord = result; | |
control = result.controls.left; | |
break; | |
case 't': | |
case 'q': | |
x1 = coord.x; | |
y1 = coord.y; | |
if (!control) { | |
control = new Two.Vector().copy(coord); | |
} | |
if (control.isZero()) { | |
x2 = x1; | |
y2 = y1; | |
} else { | |
x2 = control.x; | |
y1 = control.y; | |
} | |
if (lower === 'q') { | |
x3 = parseFloat(coords[0]); | |
y3 = parseFloat(coords[1]); | |
x4 = parseFloat(coords[1]); | |
y4 = parseFloat(coords[2]); | |
} else { | |
reflection = Two.Utils.getReflection(coord, control, relative); | |
x3 = reflection.x; | |
y3 = reflection.y; | |
x4 = parseFloat(coords[0]); | |
y4 = parseFloat(coords[1]); | |
} | |
if (relative) { | |
x2 += x1; | |
y2 += y1; | |
x3 += x1; | |
y3 += y1; | |
x4 += x1; | |
y4 += y1; | |
} | |
if (!_.isObject(coord.controls)) { | |
Two.Anchor.AppendCurveProperties(coord); | |
} | |
coord.controls.right.set(x2 - coord.x, y2 - coord.y); | |
result = new Two.Anchor( | |
x4, y4, | |
x3 - x4, y3 - y4, | |
undefined, undefined, | |
Two.Commands.curve | |
); | |
coord = result; | |
control = result.controls.left; | |
break; | |
case 'a': | |
throw new Two.Utils.Error('not yet able to interpret Elliptical Arcs.'); | |
} | |
return result; | |
})); | |
if (points.length <= 1) { | |
return; | |
} | |
points = _.compact(points); | |
var poly = new Two.Polygon(points, closed, undefined, true).noStroke(); | |
poly.fill = 'black'; | |
return Two.Utils.applySvgAttributes(node, poly); | |
}, | |
circle: function(node) { | |
var x = parseFloat(node.getAttribute('cx')); | |
var y = parseFloat(node.getAttribute('cy')); | |
var r = parseFloat(node.getAttribute('r')); | |
var amount = Two.Resolution; | |
var points = _.map(_.range(amount), function(i) { | |
var pct = i / amount; | |
var theta = pct * TWO_PI; | |
var x = r * cos(theta); | |
var y = r * sin(theta); | |
return new Two.Anchor(x, y); | |
}, this); | |
var circle = new Two.Polygon(points, true, true).noStroke(); | |
circle.translation.set(x, y); | |
circle.fill = 'black'; | |
return Two.Utils.applySvgAttributes(node, circle); | |
}, | |
ellipse: function(node) { | |
var x = parseFloat(node.getAttribute('cx')); | |
var y = parseFloat(node.getAttribute('cy')); | |
var width = parseFloat(node.getAttribute('rx')); | |
var height = parseFloat(node.getAttribute('ry')); | |
var amount = Two.Resolution; | |
var points = _.map(_.range(amount), function(i) { | |
var pct = i / amount; | |
var theta = pct * TWO_PI; | |
var x = width * cos(theta); | |
var y = height * sin(theta); | |
return new Two.Anchor(x, y); | |
}, this); | |
var ellipse = new Two.Polygon(points, true, true).noStroke(); | |
ellipse.translation.set(x, y); | |
ellipse.fill = 'black'; | |
return Two.Utils.applySvgAttributes(node, ellipse); | |
}, | |
rect: function(node) { | |
var x = parseFloat(node.getAttribute('x')); | |
var y = parseFloat(node.getAttribute('y')); | |
var width = parseFloat(node.getAttribute('width')); | |
var height = parseFloat(node.getAttribute('height')); | |
var w2 = width / 2; | |
var h2 = height / 2; | |
var points = [ | |
new Two.Anchor(w2, h2), | |
new Two.Anchor(-w2, h2), | |
new Two.Anchor(-w2, -h2), | |
new Two.Anchor(w2, -h2) | |
]; | |
var rect = new Two.Polygon(points, true).noStroke(); | |
rect.translation.set(x + w2, y + h2); | |
rect.fill = 'black'; | |
return Two.Utils.applySvgAttributes(node, rect); | |
}, | |
line: function(node) { | |
var x1 = parseFloat(node.getAttribute('x1')); | |
var y1 = parseFloat(node.getAttribute('y1')); | |
var x2 = parseFloat(node.getAttribute('x2')); | |
var y2 = parseFloat(node.getAttribute('y2')); | |
var width = x2 - x1; | |
var height = y2 - y1; | |
var w2 = width / 2; | |
var h2 = height / 2; | |
var points = [ | |
new Two.Anchor(- w2, - h2), | |
new Two.Anchor(w2, h2) | |
]; | |
// Center line and translate to desired position. | |
var line = new Two.Polygon(points).noFill(); | |
line.translation.set(x1 + w2, y1 + h2); | |
return Two.Utils.applySvgAttributes(node, line); | |
} | |
}, | |
/** | |
* Given 2 points (a, b) and corresponding control point for each | |
* return an array of points that represent points plotted along | |
* the curve. Number points determined by limit. | |
*/ | |
subdivide: function(x1, y1, x2, y2, x3, y3, x4, y4, limit) { | |
limit = limit || Two.Utils.Curve.RecursionLimit; | |
var amount = limit + 1; | |
// TODO: Issue 73 | |
// Don't recurse if the end points are identical | |
if (x1 === x4 && y1 === y4) { | |
return [new Two.Anchor(x4, y4)]; | |
} | |
return _.map(_.range(0, amount), function(i) { | |
var t = i / amount; | |
var x = getPointOnCubicBezier(t, x1, x2, x3, x4); | |
var y = getPointOnCubicBezier(t, y1, y2, y3, y4); | |
return new Two.Anchor(x, y); | |
}); | |
}, | |
getPointOnCubicBezier: function(t, a, b, c, d) { | |
var k = 1 - t; | |
return (k * k * k * a) + (3 * k * k * t * b) + (3 * k * t * t * c) + | |
(t * t * t * d); | |
}, | |
/** | |
* Given 2 points (a, b) and corresponding control point for each | |
* return a float that represents the length of the curve using | |
* Gauss-Legendre algorithm. Limit iterations of calculation by `limit`. | |
*/ | |
getCurveLength: function(x1, y1, x2, y2, x3, y3, x4, y4, limit) { | |
// TODO: Better / fuzzier equality check | |
// Linear calculation | |
if (x1 === x2 && y1 === y2 && x3 === x4 && y3 === y4) { | |
var dx = x4 - x1; | |
var dy = y4 - y1; | |
return sqrt(dx * dx + dy * dy); | |
} | |
// Calculate the coefficients of a Bezier derivative. | |
var ax = 9 * (x2 - x3) + 3 * (x4 - x1), | |
bx = 6 * (x1 + x3) - 12 * x2, | |
cx = 3 * (x2 - x1), | |
ay = 9 * (y2 - y3) + 3 * (y4 - y1), | |
by = 6 * (y1 + y3) - 12 * y2, | |
cy = 3 * (y2 - y1); | |
var integrand = function(t) { | |
// Calculate quadratic equations of derivatives for x and y | |
var dx = (ax * t + bx) * t + cx, | |
dy = (ay * t + by) * t + cy; | |
return sqrt(dx * dx + dy * dy); | |
}; | |
return integrate( | |
integrand, 0, 1, limit || Two.Utils.Curve.RecursionLimit | |
); | |
}, | |
/** | |
* Integration for `getCurveLength` calculations. Referenced from | |
* Paper.js: https://github.com/paperjs/paper.js/blob/master/src/util/Numerical.js#L101 | |
*/ | |
integrate: function(f, a, b, n) { | |
var x = Two.Utils.Curve.abscissas[n - 2], | |
w = Two.Utils.Curve.weights[n - 2], | |
A = 0.5 * (b - a), | |
B = A + a, | |
i = 0, | |
m = (n + 1) >> 1, | |
sum = n & 1 ? w[i++] * f(B) : 0; // Handle odd n | |
while (i < m) { | |
var Ax = A * x[i]; | |
sum += w[i++] * (f(B + Ax) + f(B - Ax)); | |
} | |
return A * sum; | |
}, | |
/** | |
* Creates a set of points that have u, v values for anchor positions | |
*/ | |
getCurveFromPoints: function(points, closed) { | |
var l = points.length, last = l - 1; | |
for (var i = 0; i < l; i++) { | |
var point = points[i]; | |
if (!_.isObject(point.controls)) { | |
Two.Anchor.AppendCurveProperties(point); | |
} | |
var prev = closed ? mod(i - 1, l) : max(i - 1, 0); | |
var next = closed ? mod(i + 1, l) : min(i + 1, last); | |
var a = points[prev]; | |
var b = point; | |
var c = points[next]; | |
getControlPoints(a, b, c); | |
b._command = i === 0 ? Two.Commands.move : Two.Commands.curve; | |
b.controls.left.x = _.isNumber(b.controls.left.x) ? b.controls.left.x : b.x; | |
b.controls.left.y = _.isNumber(b.controls.left.y) ? b.controls.left.y : b.y; | |
b.controls.right.x = _.isNumber(b.controls.right.x) ? b.controls.right.x : b.x; | |
b.controls.right.y = _.isNumber(b.controls.right.y) ? b.controls.right.y : b.y; | |
} | |
}, | |
/** | |
* Given three coordinates return the control points for the middle, b, | |
* vertex. | |
*/ | |
getControlPoints: function(a, b, c) { | |
var a1 = angleBetween(a, b); | |
var a2 = angleBetween(c, b); | |
var d1 = distanceBetween(a, b); | |
var d2 = distanceBetween(c, b); | |
var mid = (a1 + a2) / 2; | |
// So we know which angle corresponds to which side. | |
b.u = _.isObject(b.controls.left) ? b.controls.left : new Two.Vector(0, 0); | |
b.v = _.isObject(b.controls.right) ? b.controls.right : new Two.Vector(0, 0); | |
// TODO: Issue 73 | |
if (d1 < 0.0001 || d2 < 0.0001) { | |
if (!b._relative) { | |
b.controls.left.copy(b); | |
b.controls.right.copy(b); | |
} | |
return b; | |
} | |
d1 *= 0.33; // Why 0.33? | |
d2 *= 0.33; | |
if (a2 < a1) { | |
mid += HALF_PI; | |
} else { | |
mid -= HALF_PI; | |
} | |
b.controls.left.x = cos(mid) * d1; | |
b.controls.left.y = sin(mid) * d1; | |
mid -= PI; | |
b.controls.right.x = cos(mid) * d2; | |
b.controls.right.y = sin(mid) * d2; | |
if (!b._relative) { | |
b.controls.left.x += b.x; | |
b.controls.left.y += b.y; | |
b.controls.right.x += b.x; | |
b.controls.right.y += b.y; | |
} | |
return b; | |
}, | |
/** | |
* Get the reflection of a point "b" about point "a". | |
*/ | |
getReflection: function(a, b, relative) { | |
var d = b.distanceTo(Two.Vector.zero); | |
var theta = angleBetween(Two.Vector.zero, b); | |
return new Two.Vector( | |
d * cos(theta) + (relative ? 0 : a.x), | |
d * sin(theta) + (relative ? 0 : a.y) | |
); | |
}, | |
angleBetween: function(A, B) { | |
var dx, dy; | |
if (arguments.length >= 4) { | |
dx = arguments[0] - arguments[2]; | |
dy = arguments[1] - arguments[3]; | |
return atan2(dy, dx); | |
} | |
dx = A.x - B.x; | |
dy = A.y - B.y; | |
return atan2(dy, dx); | |
}, | |
distanceBetweenSquared: function(p1, p2) { | |
var dx = p1.x - p2.x; | |
var dy = p1.y - p2.y; | |
return dx * dx + dy * dy; | |
}, | |
distanceBetween: function(p1, p2) { | |
return sqrt(distanceBetweenSquared(p1, p2)); | |
}, | |
mod: function(v, l) { | |
while (v < 0) { | |
v += l; | |
} | |
return v % l; | |
}, | |
/** | |
* Array like collection that triggers inserted and removed events | |
* removed : pop / shift / splice | |
* inserted : push / unshift / splice (with > 2 arguments) | |
*/ | |
Collection: function() { | |
Array.call(this); | |
if(arguments.length > 1) { | |
Array.prototype.push.apply(this, arguments); | |
} else if( arguments[0] && Array.isArray(arguments[0]) ) { | |
Array.prototype.push.apply(this, arguments[0]); | |
} | |
}, | |
// Custom Error Throwing for Two.js | |
Error: function(message) { | |
this.name = 'two.js'; | |
this.message = message; | |
} | |
} | |
}); | |
Two.Utils.Error.prototype = new Error(); | |
Two.Utils.Error.prototype.constructor = Two.Utils.Error; | |
Two.Utils.Collection.prototype = new Array(); | |
Two.Utils.Collection.constructor = Two.Utils.Collection; | |
_.extend(Two.Utils.Collection.prototype, Backbone.Events, { | |
pop: function() { | |
var popped = Array.prototype.pop.apply(this, arguments); | |
this.trigger(Two.Events.remove, [popped]); | |
return popped; | |
}, | |
shift: function() { | |
var shifted = Array.prototype.shift.apply(this, arguments); | |
this.trigger(Two.Events.remove, [shifted]); | |
return shifted; | |
}, | |
push: function() { | |
var pushed = Array.prototype.push.apply(this, arguments); | |
this.trigger(Two.Events.insert, arguments); | |
return pushed; | |
}, | |
unshift: function() { | |
var unshifted = Array.prototype.unshift.apply(this, arguments); | |
this.trigger(Two.Events.insert, arguments); | |
return unshifted; | |
}, | |
splice: function() { | |
var spliced = Array.prototype.splice.apply(this, arguments); | |
var inserted; | |
this.trigger(Two.Events.remove, spliced); | |
if (arguments.length > 2) { | |
inserted = this.slice(arguments[0], arguments.length-2); | |
this.trigger(Two.Events.insert, inserted); | |
} | |
return spliced; | |
} | |
}); | |
// Localize utils | |
var distanceBetween = Two.Utils.distanceBetween, | |
distanceBetweenSquared = Two.Utils.distanceBetweenSquared, | |
angleBetween = Two.Utils.angleBetween, | |
getControlPoints = Two.Utils.getControlPoints, | |
getCurveFromPoints = Two.Utils.getCurveFromPoints, | |
solveSegmentIntersection = Two.Utils.solveSegmentIntersection, | |
decoupleShapes = Two.Utils.decoupleShapes, | |
mod = Two.Utils.mod, | |
getBackingStoreRatio = Two.Utils.getBackingStoreRatio, | |
getPointOnCubicBezier = Two.Utils.getPointOnCubicBezier, | |
getCurveLength = Two.Utils.getCurveLength, | |
integrate = Two.Utils.integrate; | |
_.extend(Two.prototype, Backbone.Events, { | |
appendTo: function(elem) { | |
elem.appendChild(this.renderer.domElement); | |
return this; | |
}, | |
play: function() { | |
Two.Utils.setPlaying.call(this, true); | |
return this.trigger(Two.Events.play); | |
}, | |
pause: function() { | |
this.playing = false; | |
return this.trigger(Two.Events.pause); | |
}, | |
/** | |
* Update positions and calculations in one pass before rendering. | |
*/ | |
update: function() { | |
var animated = !!this._lastFrame; | |
var now = getNow(); | |
this.frameCount++; | |
if (animated) { | |
this.timeDelta = parseFloat((now - this._lastFrame).toFixed(3)); | |
} | |
this._lastFrame = now; | |
var width = this.width; | |
var height = this.height; | |
var renderer = this.renderer; | |
// Update width / height for the renderer | |
if (width !== renderer.width || height !== renderer.height) { | |
renderer.setSize(width, height, this.ratio); | |
} | |
this.trigger(Two.Events.update, this.frameCount, this.timeDelta); | |
return this.render(); | |
}, | |
/** | |
* Render all drawable - visible objects of the scene. | |
*/ | |
render: function() { | |
this.renderer.render(); | |
return this.trigger(Two.Events.render, this.frameCount); | |
}, | |
/** | |
* Convenience Methods | |
*/ | |
add: function(o) { | |
var objects = o; | |
if (!_.isArray(o)) { | |
objects = _.toArray(arguments); | |
} | |
this.scene.add(objects); | |
return this; | |
}, | |
remove: function(o) { | |
var objects = o; | |
if (!_.isArray(o)) { | |
objects = _.toArray(arguments); | |
} | |
this.scene.remove(objects); | |
return this; | |
}, | |
clear: function() { | |
this.scene.remove(_.toArray(this.scene.children)); | |
return this; | |
}, | |
makeLine: function(x1, y1, x2, y2) { | |
var width = x2 - x1; | |
var height = y2 - y1; | |
var w2 = width / 2; | |
var h2 = height / 2; | |
var points = [ | |
new Two.Anchor(- w2, - h2), | |
new Two.Anchor(w2, h2) | |
]; | |
// Center line and translate to desired position. | |
var line = new Two.Polygon(points).noFill(); | |
line.translation.set(x1 + w2, y1 + h2); | |
this.scene.add(line); | |
return line; | |
}, | |
makeRectangle: function(x, y, width, height) { | |
var w2 = width / 2; | |
var h2 = height / 2; | |
var points = [ | |
new Two.Anchor(-w2, -h2), | |
new Two.Anchor(w2, -h2), | |
new Two.Anchor(w2, h2), | |
new Two.Anchor(-w2, h2) | |
]; | |
var rect = new Two.Polygon(points, true); | |
rect.translation.set(x, y); | |
this.scene.add(rect); | |
return rect; | |
}, | |
makeCircle: function(ox, oy, r) { | |
return this.makeEllipse(ox, oy, r, r); | |
}, | |
makeEllipse: function(ox, oy, width, height) { | |
var amount = Two.Resolution; | |
var points = _.map(_.range(amount), function(i) { | |
var pct = i / amount; | |
var theta = pct * TWO_PI; | |
var x = width * cos(theta); | |
var y = height * sin(theta); | |
return new Two.Anchor(x, y); | |
}, this); | |
var ellipse = new Two.Polygon(points, true, true); | |
ellipse.translation.set(ox, oy); | |
this.scene.add(ellipse); | |
return ellipse; | |
}, | |
makeCurve: function(p) { | |
var l = arguments.length, points = p; | |
if (!_.isArray(p)) { | |
points = []; | |
for (var i = 0; i < l; i+=2) { | |
var x = arguments[i]; | |
if (!_.isNumber(x)) { | |
break; | |
} | |
var y = arguments[i + 1]; | |
points.push(new Two.Anchor(x, y)); | |
} | |
} | |
var last = arguments[l - 1]; | |
var poly = new Two.Polygon(points, !(_.isBoolean(last) ? last : undefined), true); | |
var rect = poly.getBoundingClientRect(); | |
var cx = rect.left + rect.width / 2; | |
var cy = rect.top + rect.height / 2; | |
_.each(poly.vertices, function(v) { | |
v.x -= cx; | |
v.y -= cy; | |
}); | |
poly.translation.set(cx, cy); | |
this.scene.add(poly); | |
return poly; | |
}, | |
/** | |
* Convenience method to make and draw a Two.Polygon. | |
*/ | |
makePolygon: function(p) { | |
var l = arguments.length, points = p; | |
if (!_.isArray(p)) { | |
points = []; | |
for (var i = 0; i < l; i+=2) { | |
var x = arguments[i]; | |
if (!_.isNumber(x)) { | |
break; | |
} | |
var y = arguments[i + 1]; | |
points.push(new Two.Anchor(x, y)); | |
} | |
} | |
var last = arguments[l - 1]; | |
var poly = new Two.Polygon(points, !(_.isBoolean(last) ? last : undefined)); | |
var rect = poly.getBoundingClientRect(); | |
poly.center().translation | |
.set(rect.left + rect.width / 2, rect.top + rect.height / 2); | |
this.scene.add(poly); | |
return poly; | |
}, | |
makeGroup: function(o) { | |
var objects = o; | |
if (!_.isArray(o)) { | |
objects = _.toArray(arguments); | |
} | |
var group = new Two.Group(); | |
this.scene.add(group); | |
group.add(objects); | |
return group; | |
}, | |
// Utility Functions will go here. | |
/** | |
* Interpret an SVG Node and add it to this instance's scene. The | |
* distinction should be made that this doesn't `import` svg's, it solely | |
* interprets them into something compatible for Two.js — this is slightly | |
* different than a direct transcription. | |
* | |
* @param {Object} svgNode - The node to be parsed | |
* @param {Boolean} noWrappingGroup - Don't create a top-most group but | |
* append all contents directly | |
*/ | |
interpret: function(svgNode, noWrapInGroup) { | |
var tag = svgNode.tagName.toLowerCase(); | |
if (!(tag in Two.Utils.read)) { | |
return null; | |
} | |
var node = Two.Utils.read[tag].call(this, svgNode); | |
if (noWrapInGroup && node instanceof Two.Group) { | |
this.add(_.values(node.children)); | |
} else { | |
this.add(node); | |
} | |
return node; | |
} | |
}); | |
function fitToWindow() { | |
var wr = document.body.getBoundingClientRect(); | |
var width = this.width = wr.width; | |
var height = this.height = wr.height; | |
this.renderer.setSize(width, height, this.ratio); | |
this.trigger(Two.Events.resize, width, height); | |
} | |
function getNow() { | |
return ((root.performance && root.performance.now) | |
? root.performance : Date).now(); | |
} | |
// Request Animation Frame | |
(function() { | |
requestAnimationFrame(arguments.callee); | |
Two.Instances.forEach(function(t) { | |
if (t.playing) { | |
t.update(); | |
} | |
}); | |
})(); | |
//exports to multiple environments | |
if (typeof define === 'function' && define.amd) | |
//AMD | |
define(function(){ return Two; }); | |
else if (typeof module != "undefined" && module.exports) | |
//Node | |
module.exports = Two; | |
})(); | |
(function() { | |
var Vector = Two.Vector = function(x, y) { | |
this.x = x || 0; | |
this.y = y || 0; | |
}; | |
_.extend(Vector, { | |
zero: new Two.Vector() | |
}); | |
_.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 */ ); | |
}, | |
toString: function() { | |
return this.x + ',' + this.y; | |
}, | |
toObject: function() { | |
return { x: this.x, y: this.y }; | |
} | |
}); | |
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 */ ); | |
}, | |
toString: function() { | |
return this._x + ',' + this._y; | |
}, | |
toObject: function() { | |
return { x: this._x, y: this._y }; | |
} | |
}; | |
var xgs = { | |
get: function() { | |
return this._x; | |
}, | |
set: function(v) { | |
this._x = v; | |
this.trigger(Two.Events.change, 'x'); | |
} | |
}; | |
var ygs = { | |
get: function() { | |
return this._y; | |
}, | |
set: function(v) { | |
this._y = v; | |
this.trigger(Two.Events.change, 'y'); | |
} | |
}; | |
/** | |
* Override Backbone bind / on in order to add properly broadcasting. | |
* This allows Two.Vector to not broadcast events unless event listeners | |
* are explicity bound to it. | |
*/ | |
Two.Vector.prototype.bind = Two.Vector.prototype.on = function() { | |
if (!this._bound) { | |
this._x = this.x; | |
this._y = this.y; | |
Object.defineProperty(this, 'x', xgs); | |
Object.defineProperty(this, 'y', ygs); | |
_.extend(this, BoundProto); | |
this._bound = true; // Reserved for event initialization check | |
} | |
Backbone.Events.bind.apply(this, arguments); | |
return this; | |
}; | |
})(); | |
(function() { | |
// Localized variables | |
var commands = Two.Commands; | |
/** | |
* An object that holds 3 `Two.Vector`s, the anchor point and its | |
* corresponding handles: `left` and `right`. | |
*/ | |
var Anchor = Two.Anchor = function(x, y, ux, uy, vx, vy, command) { | |
Two.Vector.call(this, x, y); | |
this._broadcast = _.bind(function() { | |
this.trigger(Two.Events.change); | |
}, this); | |
this._command = command || commands.move; | |
this._relative = true; | |
if (!command) { | |
return this; | |
} | |
Anchor.AppendCurveProperties(this); | |
if (_.isNumber(ux)) { | |
this.controls.left.x = ux; | |
} | |
if (_.isNumber(uy)) { | |
this.controls.left.y = uy; | |
} | |
if (_.isNumber(vx)) { | |
this.controls.right.x = vx; | |
} | |
if (_.isNumber(vy)) { | |
this.controls.right.y = vy; | |
} | |
}; | |
_.extend(Anchor, { | |
AppendCurveProperties: function(anchor) { | |
anchor.controls = { | |
left: new Two.Vector(0, 0), | |
right: new Two.Vector(0, 0) | |
}; | |
} | |
}); | |
var AnchorProto = { | |
listen: function() { | |
if (!_.isObject(this.controls)) { | |
Anchor.AppendCurveProperties(this); | |
} | |
this.controls.left.bind(Two.Events.change, this._broadcast); | |
this.controls.right.bind(Two.Events.change, this._broadcast); | |
return this; | |
}, | |
ignore: function() { | |
this.controls.left.unbind(Two.Events.change, this._broadcast); | |
this.controls.right.unbind(Two.Events.change, this._broadcast); | |
return this; | |
}, | |
clone: function() { | |
var controls = this.controls; | |
var clone = new Two.Anchor( | |
this.x, | |
this.y, | |
controls && controls.left.x, | |
controls && controls.left.y, | |
controls && controls.right.x, | |
controls && controls.right.y, | |
this.command | |
); | |
clone.relative = this._relative; | |
return clone; | |
}, | |
toObject: function() { | |
var o = { | |
x: this.x, | |
y: this.y | |
}; | |
if (this._command) { | |
o.command = this._command; | |
} | |
if (this._relative) { | |
o.relative = this._relative; | |
} | |
if (this.controls) { | |
o.controls = { | |
left: this.controls.left.toObject(), | |
right: this.controls.right.toObject() | |
}; | |
} | |
return o; | |
} | |
}; | |
Object.defineProperty(Anchor.prototype, 'command', { | |
get: function() { | |
return this._command; | |
}, | |
set: function(c) { | |
this._command = c; | |
if (this._command === commands.curve && !_.isObject(this.controls)) { | |
Anchor.AppendCurveProperties(this); | |
} | |
return this.trigger(Two.Events.change); | |
} | |
}); | |
Object.defineProperty(Anchor.prototype, 'relative', { | |
get: function() { | |
return this._relative; | |
}, | |
set: function(b) { | |
if (this._relative == b) { | |
return this; | |
} | |
this._relative = !!b; | |
return this.trigger(Two.Events.change); | |
} | |
}); | |
_.extend(Anchor.prototype, Two.Vector.prototype, AnchorProto); | |
// Make it possible to bind and still have the Anchor specific | |
// inheritance from Two.Vector | |
Two.Anchor.prototype.bind = Two.Anchor.prototype.on = function() { | |
Two.Vector.prototype.bind.apply(this, arguments); | |
_.extend(this, AnchorProto); | |
}; | |
Two.Anchor.prototype.unbind = Two.Anchor.prototype.off = function() { | |
Two.Vector.prototype.unbind.apply(this, arguments); | |
_.extend(this, AnchorProto); | |
}; | |
})(); | |
(function() { | |
/** | |
* Constants | |
*/ | |
var cos = Math.cos, sin = Math.sin, tan = Math.tan; | |
/** | |
* Two.Matrix contains an array of elements that represent | |
* the two dimensional 3 x 3 matrix as illustrated below: | |
* | |
* ===== | |
* a b c | |
* d e f | |
* g h i // this row is not really used in 2d transformations | |
* ===== | |
* | |
* String order is for transform strings: a, d, b, e, c, f | |
* | |
* @class | |
*/ | |
var Matrix = Two.Matrix = function(a, b, c, d, e, f) { | |
this.elements = new Two.Array(9); | |
var elements = a; | |
if (!_.isArray(elements)) { | |
elements = _.toArray(arguments); | |
} | |
// initialize the elements with default values. | |
this.identity().set(elements); | |
}; | |
_.extend(Matrix, { | |
Identity: [ | |
1, 0, 0, | |
0, 1, 0, | |
0, 0, 1 | |
], | |
/** | |
* Multiply two matrix 3x3 arrays | |
*/ | |
Multiply: function(A, B, C) { | |
if (B.length <= 3) { // Multiply Vector | |
var x, y, z, e = A; | |
var a = B[0] || 0, | |
b = B[1] || 0, | |
c = B[2] || 0; | |
// Go down rows first | |
// a, d, g, b, e, h, c, f, i | |
x = e[0] * a + e[1] * b + e[2] * c; | |
y = e[3] * a + e[4] * b + e[5] * c; | |
z = e[6] * a + e[7] * b + e[8] * c; | |
return { x: x, y: y, z: z }; | |
} | |
var A0 = A[0], A1 = A[1], A2 = A[2]; | |
var A3 = A[3], A4 = A[4], A5 = A[5]; | |
var A6 = A[6], A7 = A[7], A8 = A[8]; | |
var B0 = B[0], B1 = B[1], B2 = B[2]; | |
var B3 = B[3], B4 = B[4], B5 = B[5]; | |
var B6 = B[6], B7 = B[7], B8 = B[8]; | |
C = C || new Two.Array(9); | |
C[0] = A0 * B0 + A1 * B3 + A2 * B6; | |
C[1] = A0 * B1 + A1 * B4 + A2 * B7; | |
C[2] = A0 * B2 + A1 * B5 + A2 * B8; | |
C[3] = A3 * B0 + A4 * B3 + A5 * B6; | |
C[4] = A3 * B1 + A4 * B4 + A5 * B7; | |
C[5] = A3 * B2 + A4 * B5 + A5 * B8; | |
C[6] = A6 * B0 + A7 * B3 + A8 * B6; | |
C[7] = A6 * B1 + A7 * B4 + A8 * B7; | |
C[8] = A6 * B2 + A7 * B5 + A8 * B8; | |
return C; | |
} | |
}); | |
_.extend(Matrix.prototype, Backbone.Events, { | |
/** | |
* Takes an array of elements or the arguments list itself to | |
* set and update the current matrix's elements. Only updates | |
* specified values. | |
*/ | |
set: function(a) { | |
var elements = a; | |
if (!_.isArray(elements)) { | |
elements = _.toArray(arguments); | |
} | |
_.extend(this.elements, elements); | |
return this.trigger(Two.Events.change); | |
}, | |
/** | |
* Turn matrix to identity, like resetting. | |
*/ | |
identity: function() { | |
this.set(Matrix.Identity); | |
return this; | |
}, | |
/** | |
* Multiply scalar or multiply by another matrix. | |
*/ | |
multiply: function(a, b, c, d, e, f, g, h, i) { | |
var elements = arguments, l = elements.length; | |
// Multiply scalar | |
if (l <= 1) { | |
_.each(this.elements, function(v, i) { | |
this.elements[i] = v * a; | |
}, this); | |
return this.trigger(Two.Events.change); | |
} | |
if (l <= 3) { // Multiply Vector | |
var x, y, z; | |
a = a || 0; | |
b = b || 0; | |
c = c || 0; | |
e = this.elements; | |
// Go down rows first | |
// a, d, g, b, e, h, c, f, i | |
x = e[0] * a + e[1] * b + e[2] * c; | |
y = e[3] * a + e[4] * b + e[5] * c; | |
z = e[6] * a + e[7] * b + e[8] * c; | |
return { x: x, y: y, z: z }; | |
} | |
// Multiple matrix | |
var A = this.elements; | |
var B = elements; | |
var A0 = A[0], A1 = A[1], A2 = A[2]; | |
var A3 = A[3], A4 = A[4], A5 = A[5]; | |
var A6 = A[6], A7 = A[7], A8 = A[8]; | |
var B0 = B[0], B1 = B[1], B2 = B[2]; | |
var B3 = B[3], B4 = B[4], B5 = B[5]; | |
var B6 = B[6], B7 = B[7], B8 = B[8]; | |
this.elements[0] = A0 * B0 + A1 * B3 + A2 * B6; | |
this.elements[1] = A0 * B1 + A1 * B4 + A2 * B7; | |
this.elements[2] = A0 * B2 + A1 * B5 + A2 * B8; | |
this.elements[3] = A3 * B0 + A4 * B3 + A5 * B6; | |
this.elements[4] = A3 * B1 + A4 * B4 + A5 * B7; | |
this.elements[5] = A3 * B2 + A4 * B5 + A5 * B8; | |
this.elements[6] = A6 * B0 + A7 * B3 + A8 * B6; | |
this.elements[7] = A6 * B1 + A7 * B4 + A8 * B7; | |
this.elements[8] = A6 * B2 + A7 * B5 + A8 * B8; | |
return this.trigger(Two.Events.change); | |
}, | |
inverse: function(out) { | |
var a = this.elements; | |
out = out || new Two.Matrix(); | |
var a00 = a[0], a01 = a[1], a02 = a[2]; | |
var a10 = a[3], a11 = a[4], a12 = a[5]; | |
var a20 = a[6], a21 = a[7], a22 = a[8]; | |
var b01 = a22 * a11 - a12 * a21; | |
var b11 = -a22 * a10 + a12 * a20; | |
var b21 = a21 * a10 - a11 * a20; | |
// Calculate the determinant | |
var det = a00 * b01 + a01 * b11 + a02 * b21; | |
if (!det) { | |
return null; | |
} | |
det = 1.0 / det; | |
out.elements[0] = b01 * det; | |
out.elements[1] = (-a22 * a01 + a02 * a21) * det; | |
out.elements[2] = (a12 * a01 - a02 * a11) * det; | |
out.elements[3] = b11 * det; | |
out.elements[4] = (a22 * a00 - a02 * a20) * det; | |
out.elements[5] = (-a12 * a00 + a02 * a10) * det; | |
out.elements[6] = b21 * det; | |
out.elements[7] = (-a21 * a00 + a01 * a20) * det; | |
out.elements[8] = (a11 * a00 - a01 * a10) * det; | |
return out; | |
}, | |
/** | |
* Set a scalar onto the matrix. | |
*/ | |
scale: function(sx, sy) { | |
var l = arguments.length; | |
if (l <= 1) { | |
sy = sx; | |
} | |
return this.multiply(sx, 0, 0, 0, sy, 0, 0, 0, 1); | |
}, | |
/** | |
* Rotate the matrix. | |
*/ | |
rotate: function(radians) { | |
var c = cos(radians); | |
var s = sin(radians); | |
return this.multiply(c, -s, 0, s, c, 0, 0, 0, 1); | |
}, | |
/** | |
* Translate the matrix. | |
*/ | |
translate: function(x, y) { | |
return this.multiply(1, 0, x, 0, 1, y, 0, 0, 1); | |
}, | |
/* | |
* Skew the matrix by an angle in the x axis direction. | |
*/ | |
skewX: function(radians) { | |
var a = tan(radians); | |
return this.multiply(1, a, 0, 0, 1, 0, 0, 0, 1); | |
}, | |
/* | |
* Skew the matrix by an angle in the y axis direction. | |
*/ | |
skewY: function(radians) { | |
var a = tan(radians); | |
return this.multiply(1, 0, 0, a, 1, 0, 0, 0, 1); | |
}, | |
/** | |
* Create a transform string to be used with rendering apis. | |
*/ | |
toString: function(fullMatrix) { | |
var temp = []; | |
this.toArray(fullMatrix, temp); | |
return temp.join(' '); | |
}, | |
/** | |
* Create a transform array to be used with rendering apis. | |
*/ | |
toArray: function(fullMatrix, output) { | |
var elements = this.elements; | |
var hasOutput = !!output; | |
var a = parseFloat(elements[0].toFixed(3)); | |
var b = parseFloat(elements[1].toFixed(3)); | |
var c = parseFloat(elements[2].toFixed(3)); | |
var d = parseFloat(elements[3].toFixed(3)); | |
var e = parseFloat(elements[4].toFixed(3)); | |
var f = parseFloat(elements[5].toFixed(3)); | |
if (!!fullMatrix) { | |
var g = parseFloat(elements[6].toFixed(3)); | |
var h = parseFloat(elements[7].toFixed(3)); | |
var i = parseFloat(elements[8].toFixed(3)); | |
if (hasOutput) { | |
output[0] = a; | |
output[1] = d; | |
output[2] = g; | |
output[3] = b; | |
output[4] = e; | |
output[5] = h; | |
output[6] = c; | |
output[7] = f; | |
output[8] = i; | |
return; | |
} | |
return [ | |
a, d, g, b, e, h, c, f, i | |
]; | |
} | |
if (hasOutput) { | |
output[0] = a; | |
output[1] = d; | |
output[2] = b; | |
output[3] = e; | |
output[4] = c; | |
output[5] = f; | |
return; | |
} | |
return [ | |
a, d, b, e, c, f // Specific format see LN:19 | |
]; | |
}, | |
/** | |
* Clone the current matrix. | |
*/ | |
clone: function() { | |
var a, b, c, d, e, f, g, h, i; | |
a = this.elements[0]; | |
b = this.elements[1]; | |
c = this.elements[2]; | |
d = this.elements[3]; | |
e = this.elements[4]; | |
f = this.elements[5]; | |
g = this.elements[6]; | |
h = this.elements[7]; | |
i = this.elements[8]; | |
return new Two.Matrix(a, b, c, d, e, f, g, h, i); | |
} | |
}); | |
})(); | |
(function() { | |
// Localize variables | |
var mod = Two.Utils.mod; | |
var svg = { | |
version: 1.1, | |
ns: 'http://www.w3.org/2000/svg', | |
xlink: 'http://www.w3.org/1999/xlink', | |
/** | |
* Create an svg namespaced element. | |
*/ | |
createElement: function(name, attrs) { | |
var tag = name; | |
var elem = document.createElementNS(this.ns, tag); | |
if (tag === 'svg') { | |
attrs = _.defaults(attrs || {}, { | |
version: this.version | |
}); | |
} | |
if (_.isObject(attrs)) { | |
svg.setAttributes(elem, attrs); | |
} | |
return elem; | |
}, | |
/** | |
* Add attributes from an svg element. | |
*/ | |
setAttributes: function(elem, attrs) { | |
for (var key in attrs) { | |
elem.setAttribute(key, attrs[key]); | |
} | |
return this; | |
}, | |
/** | |
* Remove attributes from an svg element. | |
*/ | |
removeAttributes: function(elem, attrs) { | |
for (var key in attrs) { | |
elem.removeAttribute(key); | |
} | |
return this; | |
}, | |
/** | |
* Turn a set of vertices into a string for the d property of a path | |
* element. It is imperative that the string collation is as fast as | |
* possible, because this call will be happening multiple times a | |
* second. | |
*/ | |
toString: function(points, closed) { | |
var l = points.length, | |
last = l - 1, | |
d; // The elusive last Two.Commands.move point | |
return _.map(points, function(b, i) { | |
var command; | |
var prev = closed ? mod(i - 1, l) : Math.max(i - 1, 0); | |
var next = closed ? mod(i + 1, l) : Math.min(i + 1, last); | |
var a = points[prev]; | |
var c = points[next]; | |
var vx, vy, ux, uy, ar, bl, br, cl; | |
var x = b.x.toFixed(3); | |
var y = b.y.toFixed(3); | |
switch (b._command) { | |
case Two.Commands.close: | |
command = Two.Commands.close; | |
break; | |
case Two.Commands.curve: | |
ar = (a.controls && a.controls.right) || a; | |
bl = (b.controls && b.controls.left) || b; | |
if (a._relative) { | |
vx = (ar.x + a.x).toFixed(3); | |
vy = (ar.y + a.y).toFixed(3); | |
} else { | |
vx = ar.x.toFixed(3); | |
vy = ar.y.toFixed(3); | |
} | |
if (b._relative) { | |
ux = (bl.x + b.x).toFixed(3); | |
uy = (bl.y + b.y).toFixed(3); | |
} else { | |
ux = bl.x.toFixed(3); | |
uy = bl.y.toFixed(3); | |
} | |
command = ((i === 0) ? Two.Commands.move : Two.Commands.curve) + | |
' ' + vx + ' ' + vy + ' ' + ux + ' ' + uy + ' ' + x + ' ' + y; | |
break; | |
case Two.Commands.move: | |
d = b; | |
command = Two.Commands.move + ' ' + x + ' ' + y; | |
break; | |
default: | |
command = b._command + ' ' + x + ' ' + y; | |
} | |
// Add a final point and close it off | |
if (i >= last && closed) { | |
if (b._command === Two.Commands.curve) { | |
// Make sure we close to the most previous Two.Commands.move | |
c = d; | |
br = (b.controls && b.controls.right) || b; | |
cl = (c.controls && c.controls.left) || c; | |
if (b._relative) { | |
vx = (br.x + b.x).toFixed(3); | |
vy = (br.y + b.y).toFixed(3); | |
} else { | |
vx = br.x.toFixed(3); | |
vy = br.y.toFixed(3); | |
} | |
if (c._relative) { | |
ux = (cl.x + c.x).toFixed(3); | |
uy = (cl.y + c.y).toFixed(3); | |
} else { | |
ux = cl.x.toFixed(3); | |
uy = cl.y.toFixed(3); | |
} | |
x = c.x.toFixed(3); | |
y = c.y.toFixed(3); | |
command += | |
' C ' + vx + ' ' + vy + ' ' + ux + ' ' + uy + ' ' + x + ' ' + y; | |
} | |
command += ' Z'; | |
} | |
return command; | |
}).join(' '); | |
}, | |
group: { | |
// TODO: Can speed up. | |
appendChild: function(id) { | |
var elem = this.domElement.querySelector('#' + id); | |
if (elem) { | |
this.elem.appendChild(elem); | |
} | |
}, | |
// TODO: Can speed up. | |
removeChild: function(id) { | |
var elem = this.elem.querySelector('#' + id); | |
if (elem) { | |
this.elem.removeChild(elem); | |
} | |
}, | |
renderChild: function(child) { | |
svg[child._renderer.type].render.call(child, this); | |
}, | |
render: function(domElement) { | |
this._update(); | |
if (!this._renderer.elem) { | |
this._renderer.elem = svg.createElement('g', { | |
id: this.id | |
}); | |
domElement.appendChild(this._renderer.elem); | |
} | |
// _Update styles for the <g> | |
var flagMatrix = this._matrix.manual || this._flagMatrix; | |
var context = { | |
domElement: domElement, | |
elem: this._renderer.elem | |
}; | |
if (flagMatrix) { | |
this._renderer.elem.setAttribute('transform', 'matrix(' + this._matrix.toString() + ')'); | |
} | |
for (var id in this.children) { | |
svg.group.renderChild.call(domElement, this.children[id]); | |
} | |
if (this._flagAdditions) { | |
_.each(this.additions, svg.group.appendChild, context); | |
} | |
if (this._flagSubtractions) { | |
_.each(this.subtractions, svg.group.removeChild, context); | |
} | |
return this.flagReset(); | |
} | |
}, | |
polygon: { | |
render: function(domElement) { | |
this._update(); | |
// Collect any attribute that needs to be changed here | |
var changed = {}; | |
var flagMatrix = this._matrix.manual || this._flagMatrix; | |
if (flagMatrix) { | |
changed.transform = 'matrix(' + this._matrix.toString() + ')'; | |
} | |
if (this._flagVertices) { | |
var vertices = svg.toString(this._vertices, this._closed); | |
changed.d = vertices; | |
} | |
if (this._flagFill) { | |
changed.fill = this._fill; | |
} | |
if (this._flagStroke) { | |
changed.stroke = this._stroke; | |
} | |
if (this._flagLinewidth) { | |
changed['stroke-width'] = this._linewidth; | |
} | |
if (this._flagOpacity) { | |
changed['stroke-opacity'] = this._opacity; | |
changed['fill-opacity'] = this._opacity; | |
} | |
if (this._flagVisible) { | |
changed.visibility = this._visible ? 'visible' : 'hidden'; | |
} | |
if (this._flagCap) { | |
changed['stroke-linecap'] = this._cap; | |
} | |
if (this._flagJoin) { | |
changed['stroke-linejoin'] = this._join; | |
} | |
if (this._flagMiter) { | |
changed['stroke-miterlimit'] = this.miter; | |
} | |
// If there is no attached DOM element yet, | |
// create it with all necessary attributes. | |
if (!this._renderer.elem) { | |
changed.id = this.id; | |
this._renderer.elem = svg.createElement('path', changed); | |
domElement.appendChild(this._renderer.elem); | |
// Otherwise apply all pending attributes | |
} else { | |
svg.setAttributes(this._renderer.elem, changed); | |
} | |
return this.flagReset(); | |
} | |
} | |
}; | |
/** | |
* @class | |
*/ | |
var Renderer = Two[Two.Types.svg] = function(params) { | |
this.domElement = params.domElement || svg.createElement('svg'); | |
this.scene = new Two.Group(); | |
this.scene._renderer.elem = this.domElement; | |
this.scene.parent = this; | |
}; | |
_.extend(Renderer, { | |
Utils: svg | |
}); | |
_.extend(Renderer.prototype, Backbone.Events, { | |
setSize: function(width, height) { | |
this.width = width; | |
this.height = height; | |
svg.setAttributes(this.domElement, { | |
width: width, | |
height: height | |
}); | |
return this; | |
}, | |
render: function() { | |
svg.group.render.call(this.scene, this.domElement); | |
return this; | |
} | |
}); | |
})(); | |
(function() { | |
/** | |
* Constants | |
*/ | |
var mod = Two.Utils.mod; | |
var getRatio = Two.Utils.getRatio; | |
var canvas = { | |
group: { | |
renderChild: function(child) { | |
canvas[child._renderer.type].render.call(child, this); | |
}, | |
render: function(ctx) { | |
// TODO: Add a check here to only invoke _update if need be. | |
this._update(); | |
var matrix = this._matrix.elements; | |
ctx.save(); | |
ctx.transform( | |
matrix[0], matrix[3], matrix[1], matrix[4], matrix[2], matrix[5]); | |
_.each(this.children, canvas.group.renderChild, ctx); | |
ctx.restore(); | |
return this.flagReset(); | |
} | |
}, | |
polygon: { | |
render: function(ctx) { | |
var matrix, stroke, linewidth, fill, opacity, visible, cap, join, miter, | |
closed, commands, length, last, next, prev, a, c, d, ux, uy, vx, vy, | |
ar, bl, br, cl, x, y; | |
// TODO: Add a check here to only invoke _update if need be. | |
this._update(); | |
matrix = this._matrix.elements; | |
stroke = this.stroke; | |
linewidth = this.linewidth; | |
fill = this.fill; | |
opacity = this.opacity; | |
visible = this.visible; | |
cap = this.cap; | |
join = this.join; | |
miter = this.miter; | |
closed = this.closed; | |
commands = this._vertices; // Commands | |
length = commands.length; | |
last = length - 1; | |
if (!visible) { | |
return this; | |
} | |
// Transform | |
ctx.save(); | |
if (matrix) { | |
ctx.transform( | |
matrix[0], matrix[3], matrix[1], matrix[4], matrix[2], matrix[5]); | |
} | |
// Styles | |
if (fill) { | |
ctx.fillStyle = fill; | |
} | |
if (stroke) { | |
ctx.strokeStyle = stroke; | |
} | |
if (linewidth) { | |
ctx.lineWidth = linewidth; | |
} | |
if (miter) { | |
ctx.miterLimit = miter; | |
} | |
if (join) { | |
ctx.lineJoin = join; | |
} | |
if (cap) { | |
ctx.lineCap = cap; | |
} | |
if (_.isNumber(opacity)) { | |
ctx.globalAlpha = opacity; | |
} | |
ctx.beginPath(); | |
commands.forEach(function(b, i) { | |
x = b.x.toFixed(3); | |
y = b.y.toFixed(3); | |
switch (b._command) { | |
case Two.Commands.close: | |
ctx.closePath(); | |
break; | |
case Two.Commands.curve: | |
prev = closed ? mod(i - 1, length) : Math.max(i - 1, 0); | |
next = closed ? mod(i + 1, length) : Math.min(i + 1, last); | |
a = commands[prev]; | |
c = commands[next]; | |
ar = (a.controls && a.controls.right) || a; | |
bl = (b.controls && b.controls.left) || b; | |
if (a._relative) { | |
vx = (ar.x + a.x).toFixed(3); | |
vy = (ar.y + a.y).toFixed(3); | |
} else { | |
vx = ar.x.toFixed(3); | |
vy = ar.y.toFixed(3); | |
} | |
if (b._relative) { | |
ux = (bl.x + b.x).toFixed(3); | |
uy = (bl.y + b.y).toFixed(3); | |
} else { | |
ux = bl.x.toFixed(3); | |
uy = bl.y.toFixed(3); | |
} | |
ctx.bezierCurveTo(vx, vy, ux, uy, x, y); | |
if (i >= last && closed) { | |
c = d; | |
br = (b.controls && b.controls.right) || b; | |
cl = (c.controls && c.controls.left) || c; | |
if (b._relative) { | |
vx = (br.x + b.x).toFixed(3); | |
vy = (br.y + b.y).toFixed(3); | |
} else { | |
vx = br.x.toFixed(3); | |
vy = br.y.toFixed(3); | |
} | |
if (c._relative) { | |
ux = (cl.x + c.x).toFixed(3); | |
uy = (cl.y + c.y).toFixed(3); | |
} else { | |
ux = cl.x.toFixed(3); | |
uy = cl.y.toFixed(3); | |
} | |
x = c.x.toFixed(3); | |
y = c.y.toFixed(3); | |
ctx.bezierCurveTo(vx, vy, ux, uy, x, y); | |
} | |
break; | |
case Two.Commands.line: | |
ctx.lineTo(x, y); | |
break; | |
case Two.Commands.move: | |
d = b; | |
ctx.moveTo(x, y); | |
break; | |
} | |
}); | |
// Loose ends | |
if (closed) { | |
ctx.closePath(); | |
} | |
ctx.fill(); | |
ctx.stroke(); | |
ctx.restore(); | |
return this.flagReset(); | |
} | |
} | |
}; | |
var Renderer = Two[Two.Types.canvas] = function(params) { | |
this.domElement = params.domElement || document.createElement('canvas'); | |
this.ctx = this.domElement.getContext('2d'); | |
this.overdraw = params.overdraw || false; | |
// Everything drawn on the canvas needs to be added to the scene. | |
this.scene = new Two.Group(); | |
this.scene.parent = this; | |
}; | |
_.extend(Renderer, { | |
Utils: canvas | |
}); | |
_.extend(Renderer.prototype, Backbone.Events, { | |
setSize: function(width, height, ratio) { | |
this.width = width; | |
this.height = height; | |
this.ratio = _.isUndefined(ratio) ? getRatio(this.ctx) : ratio; | |
this.domElement.width = width * this.ratio; | |
this.domElement.height = height * this.ratio; | |
_.extend(this.domElement.style, { | |
width: width + 'px', | |
height: height + 'px' | |
}); | |
return this; | |
}, | |
render: function() { | |
var isOne = this.ratio === 1; | |
if (!isOne) { | |
this.ctx.save(); | |
this.ctx.scale(this.ratio, this.ratio); | |
} | |
if (!this.overdraw) { | |
this.ctx.clearRect(0, 0, this.width, this.height); | |
} | |
canvas.group.render.call(this.scene, this.ctx); | |
if (!isOne) { | |
this.ctx.restore(); | |
} | |
return this; | |
} | |
}); | |
function resetTransform(ctx) { | |
ctx.setTransform(1, 0, 0, 1, 0, 0); | |
} | |
})(); | |
(function() { | |
/** | |
* Constants | |
*/ | |
var multiplyMatrix = Two.Matrix.Multiply, | |
mod = Two.Utils.mod, | |
identity = [1, 0, 0, 0, 1, 0, 0, 0, 1], | |
transformation = new Two.Array(9), | |
getRatio = Two.Utils.getRatio; | |
var webgl = { | |
canvas: document.createElement('canvas'), | |
uv: new Two.Array([ | |
0, 0, | |
1, 0, | |
0, 1, | |
0, 1, | |
1, 0, | |
1, 1 | |
]), | |
group: { | |
renderChild: function(child) { | |
webgl[child._renderer.type].render.call(child, this.gl, this.program); | |
}, | |
render: function(gl, program) { | |
this._update(); | |
var parent = this.parent; | |
var flagParentMatrix = (parent._matrix && parent._matrix.manual) || parent._flagMatrix; | |
var flagMatrix = this._matrix.manual || this._flagMatrix; | |
if (flagParentMatrix || flagMatrix) { | |
if (!this._renderer.matrix) { | |
this._renderer.matrix = new Two.Array(9); | |
} | |
// Reduce amount of object / array creation / deletion | |
this._matrix.toArray(true, transformation); | |
multiplyMatrix(transformation, parent._renderer.matrix, this._renderer.matrix); | |
this._renderer.scale = this._scale * parent._renderer.scale; | |
if (flagParentMatrix) { | |
this._flagMatrix = true; | |
} | |
} | |
_.each(this.children, webgl.group.renderChild, { | |
gl: gl, | |
program: program | |
}); | |
return this.flagReset(); | |
} | |
}, | |
polygon: { | |
render: function(gl, program) { | |
if (!this._visible || !this._opacity) { | |
return this; | |
} | |
// Calculate what changed | |
var parent = this.parent; | |
var flagParentMatrix = parent._matrix.manual || parent._flagMatrix; | |
var flagMatrix = this._matrix.manual || this._flagMatrix; | |
var flagTexture = this._flagVertices || this._flagFill | |
|| this._flagStroke || this._flagLinewidth || this._flagOpacity | |
|| this._flagVisible || this._flagCap || this._flagJoin | |
|| this._flagMiter || this._flagScale; | |
this._update(); | |
if (flagParentMatrix || flagMatrix) { | |
if (!this._renderer.matrix) { | |
this._renderer.matrix = new Two.Array(9); | |
} | |
// Reduce amount of object / array creation / deletion | |
this._matrix.toArray(true, transformation); | |
multiplyMatrix(transformation, parent._renderer.matrix, this._renderer.matrix); | |
this._renderer.scale = this._scale * parent._renderer.scale; | |
} | |
if (flagTexture) { | |
if (!this._renderer.rect) { | |
this._renderer.rect = {}; | |
} | |
if (!this._renderer.triangles) { | |
this._renderer.triangles = new Two.Array(12); | |
} | |
webgl.getBoundingClientRect(this._vertices, this._linewidth, this._renderer.rect); | |
webgl.getTriangles(this._renderer.rect, this._renderer.triangles); | |
webgl.updateBuffer(gl, this, program); | |
webgl.updateTexture(gl, this); | |
} | |
// Draw Texture | |
gl.bindBuffer(gl.ARRAY_BUFFER, this._renderer.textureCoordsBuffer); | |
gl.vertexAttribPointer(program.textureCoords, 2, gl.FLOAT, false, 0, 0); | |
gl.bindTexture(gl.TEXTURE_2D, this._renderer.texture); | |
// Draw Rect | |
gl.uniformMatrix3fv(program.matrix, false, this._renderer.matrix); | |
gl.bindBuffer(gl.ARRAY_BUFFER, this._renderer.buffer); | |
gl.vertexAttribPointer(program.position, 2, gl.FLOAT, false, 0, 0); | |
gl.drawArrays(gl.TRIANGLES, 0, 6); | |
return this.flagReset(); | |
} | |
}, | |
/** | |
* Returns the rect of a set of verts. Typically takes vertices that are | |
* "centered" around 0 and returns them to be anchored upper-left. | |
*/ | |
getBoundingClientRect: function(vertices, border, rect) { | |
var left = Infinity, right = -Infinity, | |
top = Infinity, bottom = -Infinity, | |
width, height; | |
vertices.forEach(function(v) { | |
var x = v.x, y = v.y, controls = v.controls; | |
var a, b, c, d, cl, cr; | |
top = Math.min(y, top); | |
left = Math.min(x, left); | |
right = Math.max(x, right); | |
bottom = Math.max(y, bottom); | |
if (!v.controls) { | |
return; | |
} | |
cl = controls.left; | |
cr = controls.right; | |
if (!cl || !cr) { | |
return; | |
} | |
a = v._relative ? cl.x + x : cl.x; | |
b = v._relative ? cl.y + y : cl.y; | |
c = v._relative ? cr.x + x : cr.x; | |
d = v._relative ? cr.y + y : cr.y; | |
if (!a || !b || !c || !d) { | |
return; | |
} | |
top = Math.min(b, d, top); | |
left = Math.min(a, c, left); | |
right = Math.max(a, c, right); | |
bottom = Math.max(b, d, bottom); | |
}); | |
// Expand borders | |
if (_.isNumber(border)) { | |
top -= border; | |
left -= border; | |
right += border; | |
bottom += border; | |
} | |
width = right - left; | |
height = bottom - top; | |
rect.top = top; | |
rect.left = left; | |
rect.right = right; | |
rect.bottom = bottom; | |
rect.width = width; | |
rect.height = height; | |
if (!rect.centroid) { | |
rect.centroid = {}; | |
} | |
rect.centroid.x = - left; | |
rect.centroid.y = - top; | |
}, | |
getTriangles: function(rect, triangles) { | |
var top = rect.top, | |
left = rect.left, | |
right = rect.right, | |
bottom = rect.bottom; | |
// First Triangle | |
triangles[0] = left; | |
triangles[1] = top; | |
triangles[2] = right; | |
triangles[3] = top; | |
triangles[4] = left; | |
triangles[5] = bottom; | |
// Second Triangle | |
triangles[6] = left; | |
triangles[7] = bottom; | |
triangles[8] = right; | |
triangles[9] = top; | |
triangles[10] = right; | |
triangles[11] = bottom; | |
}, | |
updateCanvas: function(elem) { | |
var commands = elem._vertices; | |
var canvas = this.canvas; | |
var ctx = this.ctx; | |
// Styles | |
var scale = elem._renderer.scale; | |
var stroke = elem._stroke; | |
var linewidth = elem._linewidth * scale; | |
var fill = elem._fill; | |
var opacity = elem._opacity; | |
var cap = elem._cap; | |
var join = elem._join; | |
var miter = elem._miter; | |
var closed = elem._closed; | |
var length = commands.length; | |
var last = length - 1; | |
canvas.width = Math.max(Math.ceil(elem._renderer.rect.width * scale), 1); | |
canvas.height = Math.max(Math.ceil(elem._renderer.rect.height * scale), 1); | |
var centroid = elem._renderer.rect.centroid; | |
var cx = centroid.x * scale; | |
var cy = centroid.y * scale; | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
if (fill) { | |
ctx.fillStyle = fill; | |
} | |
if (stroke) { | |
ctx.strokeStyle = stroke; | |
} | |
if (linewidth) { | |
ctx.lineWidth = linewidth; | |
} | |
if (miter) { | |
ctx.miterLimit = miter; | |
} | |
if (join) { | |
ctx.lineJoin = join; | |
} | |
if (cap) { | |
ctx.lineCap = cap; | |
} | |
if (_.isNumber(opacity)) { | |
ctx.globalAlpha = opacity; | |
} | |
var d; | |
ctx.beginPath(); | |
commands.forEach(function(b, i) { | |
var next, prev, a, c, ux, uy, vx, vy, ar, bl, br, cl, x, y; | |
x = (b.x * scale + cx).toFixed(3); | |
y = (b.y * scale + cy).toFixed(3); | |
switch (b._command) { | |
case Two.Commands.close: | |
ctx.closePath(); | |
break; | |
case Two.Commands.curve: | |
prev = closed ? mod(i - 1, length) : Math.max(i - 1, 0); | |
next = closed ? mod(i + 1, length) : Math.min(i + 1, last); | |
a = commands[prev]; | |
c = commands[next]; | |
ar = (a.controls && a.controls.right) || a; | |
bl = (b.controls && b.controls.left) || b; | |
if (a._relative) { | |
vx = ((ar.x + a.x) * scale + cx).toFixed(3); | |
vy = ((ar.y + a.y) * scale + cy).toFixed(3); | |
} else { | |
vx = (ar.x * scale + cx).toFixed(3); | |
vy = (ar.y * scale + cy).toFixed(3); | |
} | |
if (b._relative) { | |
ux = ((bl.x + b.x) * scale + cx).toFixed(3); | |
uy = ((bl.y + b.y) * scale + cy).toFixed(3); | |
} else { | |
ux = (bl.x * scale + cx).toFixed(3); | |
uy = (bl.y * scale + cy).toFixed(3); | |
} | |
ctx.bezierCurveTo(vx, vy, ux, uy, x, y); | |
if (i >= last && closed) { | |
// FIXME: d is undefined here? | |
c = d; | |
br = (b.controls && b.controls.right) || b; | |
cl = (c.controls && c.controls.left) || c; | |
if (b._relative) { | |
vx = ((br.x + b.x) * scale + cx).toFixed(3); | |
vy = ((br.y + b.y) * scale + cy).toFixed(3); | |
} else { | |
vx = (br.x * scale + cx).toFixed(3); | |
vy = (br.y * scale + cy).toFixed(3); | |
} | |
if (c._relative) { | |
ux = ((cl.x + c.x) * scale + cx).toFixed(3); | |
uy = ((cl.y + c.y) * scale + cx).toFixed(3); | |
} else { | |
ux = (cl.x * scale + cx).toFixed(3); | |
uy = (cl.y * scale + cy).toFixed(3); | |
} | |
x = (c.x * scale + cx).toFixed(3); | |
y = (c.y * scale + cy).toFixed(3); | |
ctx.bezierCurveTo(vx, vy, ux, uy, x, y); | |
} | |
break; | |
case Two.Commands.line: | |
ctx.lineTo(x, y); | |
break; | |
case Two.Commands.move: | |
d = b; | |
ctx.moveTo(x, y); | |
break; | |
} | |
}); | |
// Loose ends | |
if (closed) { | |
ctx.closePath(); | |
} | |
ctx.fill(); | |
ctx.stroke(); | |
}, | |
updateTexture: function(gl, elem) { | |
this.updateCanvas(elem); | |
if (elem._renderer.texture) { | |
gl.deleteTexture(elem._renderer.texture); | |
} | |
gl.bindBuffer(gl.ARRAY_BUFFER, elem._renderer.textureCoordsBuffer); | |
elem._renderer.texture = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, elem._renderer.texture); | |
// Set the parameters so we can render any size image. | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); | |
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); | |
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
if (this.canvas.width <= 0 || this.canvas.height <= 0) { | |
return; | |
} | |
// Upload the image into the texture. | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.canvas); | |
}, | |
updateBuffer: function(gl, elem, program) { | |
if (_.isObject(elem._renderer.buffer)) { | |
gl.deleteBuffer(elem._renderer.buffer); | |
} | |
elem._renderer.buffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ARRAY_BUFFER, elem._renderer.buffer); | |
gl.enableVertexAttribArray(program.position); | |
gl.bufferData(gl.ARRAY_BUFFER, elem._renderer.triangles, gl.STATIC_DRAW); | |
if (_.isObject(elem._renderer.textureCoordsBuffer)) { | |
gl.deleteBuffer(elem._renderer.textureCoordsBuffer); | |
} | |
elem._renderer.textureCoordsBuffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ARRAY_BUFFER, elem._renderer.textureCoordsBuffer); | |
gl.enableVertexAttribArray(program.textureCoords); | |
gl.bufferData(gl.ARRAY_BUFFER, this.uv, gl.STATIC_DRAW); | |
}, | |
program: { | |
create: function(gl, shaders) { | |
var program, linked, error; | |
program = gl.createProgram(); | |
_.each(shaders, function(s) { | |
gl.attachShader(program, s); | |
}); | |
gl.linkProgram(program); | |
linked = gl.getProgramParameter(program, gl.LINK_STATUS); | |
if (!linked) { | |
error = gl.getProgramInfoLog(program); | |
gl.deleteProgram(program); | |
throw new Two.Utils.Error('unable to link program: ' + error); | |
} | |
return program; | |
} | |
}, | |
shaders: { | |
create: function(gl, source, type) { | |
var shader, compiled, error; | |
shader = gl.createShader(gl[type]); | |
gl.shaderSource(shader, source); | |
gl.compileShader(shader); | |
compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); | |
if (!compiled) { | |
error = gl.getShaderInfoLog(shader); | |
gl.deleteShader(shader); | |
throw new Two.Utils.Error('unable to compile shader ' + shader + ': ' + error); | |
} | |
return shader; | |
}, | |
types: { | |
vertex: 'VERTEX_SHADER', | |
fragment: 'FRAGMENT_SHADER' | |
}, | |
vertex: [ | |
'attribute vec2 a_position;', | |
'attribute vec2 a_textureCoords;', | |
'', | |
'uniform mat3 u_matrix;', | |
'uniform vec2 u_resolution;', | |
'', | |
'varying vec2 v_textureCoords;', | |
'', | |
'void main() {', | |
' vec2 projected = (u_matrix * vec3(a_position, 1.0)).xy;', | |
' vec2 normal = projected / u_resolution;', | |
' vec2 clipspace = (normal * 2.0) - 1.0;', | |
'', | |
' gl_Position = vec4(clipspace * vec2(1.0, -1.0), 0.0, 1.0);', | |
' v_textureCoords = a_textureCoords;', | |
'}' | |
].join('\n'), | |
fragment: [ | |
'precision mediump float;', | |
'', | |
'uniform sampler2D u_image;', | |
'varying vec2 v_textureCoords;', | |
'', | |
'void main() {', | |
' gl_FragColor = texture2D(u_image, v_textureCoords);', | |
'}' | |
].join('\n') | |
} | |
}; | |
webgl.ctx = webgl.canvas.getContext('2d'); | |
var Renderer = Two[Two.Types.webgl] = function(options) { | |
var params, gl, vs, fs; | |
this.domElement = options.domElement || document.createElement('canvas'); | |
// Everything drawn on the canvas needs to come from the stage. | |
this.scene = new Two.Group(); | |
this.scene.parent = this; | |
this._renderer = { | |
matrix: new Two.Array(identity), | |
scale: 1 | |
}; | |
this._flagMatrix = true; | |
// http://games.greggman.com/game/webgl-and-alpha/ | |
// http://www.khronos.org/registry/webgl/specs/latest/#5.2 | |
params = _.defaults(options || {}, { | |
antialias: false, | |
alpha: true, | |
premultipliedAlpha: true, | |
stencil: true, | |
preserveDrawingBuffer: true, | |
overdraw: false | |
}); | |
this.overdraw = params.overdraw; | |
gl = this.ctx = this.domElement.getContext('webgl', params) || | |
this.domElement.getContext('experimental-webgl', params); | |
if (!this.ctx) { | |
throw new Two.Utils.Error( | |
'unable to create a webgl context. Try using another renderer.'); | |
} | |
// Compile Base Shaders to draw in pixel space. | |
vs = webgl.shaders.create( | |
gl, webgl.shaders.vertex, webgl.shaders.types.vertex); | |
fs = webgl.shaders.create( | |
gl, webgl.shaders.fragment, webgl.shaders.types.fragment); | |
this.program = webgl.program.create(gl, [vs, fs]); | |
gl.useProgram(this.program); | |
// Create and bind the drawing buffer | |
// look up where the vertex data needs to go. | |
this.program.position = gl.getAttribLocation(this.program, 'a_position'); | |
this.program.matrix = gl.getUniformLocation(this.program, 'u_matrix'); | |
this.program.textureCoords = gl.getAttribLocation(this.program, 'a_textureCoords'); | |
// Copied from Three.js WebGLRenderer | |
gl.disable(gl.DEPTH_TEST); | |
// Setup some initial statements of the gl context | |
gl.enable(gl.BLEND); | |
// https://code.google.com/p/chromium/issues/detail?id=316393 | |
// gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, gl.TRUE); | |
gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); | |
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, | |
gl.ONE, gl.ONE_MINUS_SRC_ALPHA ); | |
}; | |
_.extend(Renderer.prototype, Backbone.Events, { | |
setSize: function(width, height, ratio) { | |
this.width = width; | |
this.height = height; | |
this.ratio = _.isUndefined(ratio) ? getRatio(this.ctx) : ratio; | |
this.domElement.width = width * this.ratio; | |
this.domElement.height = height * this.ratio; | |
_.extend(this.domElement.style, { | |
width: width + 'px', | |
height: height + 'px' | |
}); | |
width *= this.ratio; | |
height *= this.ratio; | |
// Set for this.stage parent scaling to account for HDPI | |
this._renderer.matrix[0] = this._renderer.matrix[4] = this._renderer.scale = this.ratio; | |
this._flagMatrix = true; | |
this.ctx.viewport(0, 0, width, height); | |
var resolutionLocation = this.ctx.getUniformLocation( | |
this.program, 'u_resolution'); | |
this.ctx.uniform2f(resolutionLocation, width, height); | |
return this; | |
}, | |
render: function() { | |
var gl = this.ctx; | |
if (!this.overdraw) { | |
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); | |
} | |
webgl.group.render.call(this.scene, gl, this.program); | |
this._flagMatrix = false; | |
return this; | |
} | |
}); | |
})(); | |
(function() { | |
var Shape = Two.Shape = function() { | |
// Private object for renderer specific variables. | |
this._renderer = {}; | |
this.id = Two.Identifier + Two.uniqueId(); | |
this.classList = []; | |
// Define matrix properties which all inherited | |
// objects of Shape have. | |
this._matrix = new Two.Matrix(); | |
this.translation = new Two.Vector(); | |
this.translation.bind(Two.Events.change, _.bind(Shape.FlagMatrix, this)); | |
this.rotation = 0; | |
this.scale = 1; | |
}; | |
_.extend(Shape, Backbone.Events, { | |
FlagMatrix: function() { | |
this._flagMatrix = true; | |
}, | |
MakeObservable: function(object) { | |
Object.defineProperty(object, 'rotation', { | |
get: function() { | |
return this._rotation; | |
}, | |
set: function(v) { | |
this._rotation = v; | |
this._flagMatrix = true; | |
} | |
}); | |
Object.defineProperty(object, 'scale', { | |
get: function() { | |
return this._scale; | |
}, | |
set: function(v) { | |
this._scale = v; | |
this._flagMatrix = true; | |
this._flagScale = true; | |
} | |
}); | |
} | |
}); | |
_.extend(Shape.prototype, { | |
// Flags | |
_flagMatrix: true, | |
// Underlying Properties | |
_rotation: 0, | |
_scale: 1, | |
addTo: function(group) { | |
group.add(this); | |
return this; | |
}, | |
clone: function() { | |
var clone = new Shape(); | |
clone.translation.copy(this.translation); | |
clone.rotation = this.rotation; | |
clone.scale = this.scale; | |
_.each(Shape.Properties, function(k) { | |
clone[k] = this[k]; | |
}, this); | |
return clone._update(); | |
}, | |
/** | |
* Set the parent of this object to another object | |
* and updates parent-child relationships | |
* Calling with no arguments will simply remove the parenting | |
*/ | |
replaceParent: function(newParent) { | |
var id = this.id, index; | |
// Release object from previous parent. | |
if (this.parent) { | |
delete this.parent.children[id]; | |
index = _.indexOf(parent.additions, id); | |
if (index >= 0) { | |
this.parent.additions.splice(index, 1); | |
} | |
this.parent.subtractions.push(id); | |
this._flagSubtractions = true; | |
} | |
if (newParent) { | |
// Add it to this group and update parent-child relationship. | |
newParent.children[id] = this; | |
this.parent = newParent; | |
newParent.additions.push(id); | |
newParent._flagAdditions = true; | |
} else { | |
delete this.parent; | |
} | |
return this; | |
}, | |
/** | |
* To be called before render that calculates and collates all information | |
* to be as up-to-date as possible for the render. Called once a frame. | |
*/ | |
_update: function() { | |
if (!this._matrix.manual && this._flagMatrix) { | |
this._matrix | |
.identity() | |
.translate(this.translation.x, this.translation.y) | |
.scale(this.scale) | |
.rotate(this.rotation); | |
} | |
// Bubble up to parents mainly for `getBoundingClientRect` method. | |
if (this.parent && this.parent._update) { | |
this.parent._update(); | |
} | |
return this; | |
}, | |
flagReset: function() { | |
this._flagMatrix = false; | |
this._flagScale = false; | |
return this; | |
} | |
}); | |
Shape.MakeObservable(Shape.prototype); | |
})(); | |
(function() { | |
/** | |
* Constants | |
*/ | |
var min = Math.min, max = Math.max, round = Math.round, | |
getComputedMatrix = Two.Utils.getComputedMatrix; | |
var commands = {}; | |
_.each(Two.Commands, function(v, k) { | |
commands[k] = new RegExp(v); | |
}); | |
var Polygon = Two.Polygon = function(vertices, closed, curved, manual) { | |
Two.Shape.call(this); | |
this._renderer.type = 'polygon'; | |
this._closed = !!closed; | |
this._curved = !!curved; | |
this.beginning = 0; | |
this.ending = 1; | |
// Style properties | |
this.fill = '#fff'; | |
this.stroke = '#000'; | |
this.linewidth = 1.0; | |
this.opacity = 1.0; | |
this.visible = true; | |
this.cap = 'butt'; // Default of Adobe Illustrator | |
this.join = 'miter'; // Default of Adobe Illustrator | |
this.miter = 4; // Default of Adobe Illustrator | |
this._vertices = []; | |
this.vertices = vertices; | |
// Determines whether or not two.js should calculate curves, lines, and | |
// commands automatically for you or to let the developer manipulate them | |
// for themselves. | |
this.automatic = !manual; | |
}; | |
_.extend(Polygon, { | |
Properties: [ | |
'fill', | |
'stroke', | |
'linewidth', | |
'opacity', | |
'visible', | |
'cap', | |
'join', | |
'miter', // Order matters here! See LN:388 | |
'closed', | |
'curved', | |
'automatic', | |
'beginning', | |
'ending' | |
], | |
FlagVertices: function() { | |
this._flagVertices = true; | |
this._flagLength = true; | |
}, | |
MakeObservable: function(object) { | |
Two.Shape.MakeObservable(object); | |
// Only the first 8 properties are flagged like this. The subsequent | |
// properties behave differently and need to be hand written. | |
_.each(Polygon.Properties.slice(0, 8), function(property) { | |
var secret = '_' + property; | |
var flag = '_flag' + property.charAt(0).toUpperCase() + property.slice(1); | |
Object.defineProperty(object, property, { | |
get: function() { | |
return this[secret]; | |
}, | |
set: function(v) { | |
this[secret] = v; | |
this[flag] = true; | |
} | |
}); | |
}); | |
Object.defineProperty(object, 'length', { | |
get: function() { | |
if (this._flagLength) { | |
this._updateLength(); | |
} | |
return this._length; | |
} | |
}); | |
Object.defineProperty(object, 'closed', { | |
get: function() { | |
return this._closed; | |
}, | |
set: function(v) { | |
this._closed = !!v; | |
this._flagVertices = true; | |
} | |
}); | |
Object.defineProperty(object, 'curved', { | |
get: function() { | |
return this._curved; | |
}, | |
set: function(v) { | |
this._curved = !!v; | |
this._flagVertices = true; | |
} | |
}); | |
Object.defineProperty(Polygon.prototype, 'automatic', { | |
get: function() { | |
return this._automatic; | |
}, | |
set: function(v) { | |
if (v === this._automatic) { | |
return; | |
} | |
this._automatic = !!v; | |
var method = this._automatic ? 'ignore' : 'listen'; | |
_.each(this.vertices, function(v) { | |
v[method](); | |
}); | |
} | |
}); | |
Object.defineProperty(object, 'beginning', { | |
get: function() { | |
return this._beginning; | |
}, | |
set: function(v) { | |
this._beginning = min(max(v, 0.0), this._ending); | |
this._flagVertices = true; | |
} | |
}); | |
Object.defineProperty(object, 'ending', { | |
get: function() { | |
return this._ending; | |
}, | |
set: function(v) { | |
this._ending = min(max(v, this._beginning), 1.0); | |
this._flagVertices = true; | |
} | |
}); | |
Object.defineProperty(object, 'vertices', { | |
get: function() { | |
return this._collection; | |
}, | |
set: function(vertices) { | |
var updateVertices = _.bind(Polygon.FlagVertices, this); | |
var bindVerts = _.bind(function(items) { | |
// This function is called a lot | |
// when importing a large SVG | |
var i = items.length; | |
while(i--) { | |
items[i].bind(Two.Events.change, updateVertices); | |
} | |
updateVertices(); | |
}, this); | |
var unbindVerts = _.bind(function(items) { | |
_.each(items, function(v) { | |
v.unbind(Two.Events.change, updateVertices); | |
}, this); | |
updateVertices(); | |
}, this); | |
// Remove previous listeners | |
if (this._collection) { | |
this._collection.unbind(); | |
} | |
// Create new Collection with copy of vertices | |
this._collection = new Two.Utils.Collection(vertices.slice(0)); | |
// Listen for Collection changes and bind / unbind | |
this._collection.bind(Two.Events.insert, bindVerts); | |
this._collection.bind(Two.Events.remove, unbindVerts); | |
// Bind Initial Vertices | |
bindVerts(this._collection); | |
} | |
}); | |
} | |
}); | |
_.extend(Polygon.prototype, Two.Shape.prototype, { | |
// Flags | |
// http://en.wikipedia.org/wiki/Flag | |
_flagVertices: true, | |
_flagLength: true, | |
_flagFill: true, | |
_flagStroke: true, | |
_flagLinewidth: true, | |
_flagOpacity: true, | |
_flagVisible: true, | |
_flagCap: true, | |
_flagJoin: true, | |
_flagMiter: true, | |
// Underlying Properties | |
_length: 0, | |
_fill: '#fff', | |
_stroke: '#000', | |
_linewidth: 1.0, | |
_opacity: 1.0, | |
_visible: true, | |
_cap: 'round', | |
_join: 'round', | |
_miter: 4, | |
_closed: true, | |
_curved: false, | |
_automatic: true, | |
_beginning: 0, | |
_ending: 1.0, | |
clone: function(parent) { | |
parent = parent || this.parent; | |
var points = _.map(this.vertices, function(v) { | |
return v.clone(); | |
}); | |
var clone = new Polygon(points, this.closed, this.curved, !this.automatic); | |
_.each(Two.Shape.Properties, function(k) { | |
clone[k] = this[k]; | |
}, this); | |
clone.translation.copy(this.translation); | |
clone.rotation = this.rotation; | |
clone.scale = this.scale; | |
parent.add(clone); | |
return clone; | |
}, | |
toObject: function() { | |
var result = { | |
vertices: _.map(this.vertices, function(v) { | |
return v.toObject(); | |
}) | |
}; | |
_.each(Two.Shape.Properties, function(k) { | |
result[k] = this[k]; | |
}, this); | |
result.translation = this.translation.toObject; | |
result.rotation = this.rotation; | |
result.scale = this.scale; | |
return result; | |
}, | |
noFill: function() { | |
this.fill = 'transparent'; | |
return this; | |
}, | |
noStroke: function() { | |
this.stroke = 'transparent'; | |
return this; | |
}, | |
/** | |
* Orient the vertices of the shape to the upper lefthand | |
* corner of the polygon. | |
*/ | |
corner: function() { | |
var rect = this.getBoundingClientRect(true); | |
rect.centroid = { | |
x: rect.left + rect.width / 2, | |
y: rect.top + rect.height / 2 | |
}; | |
_.each(this.vertices, function(v) { | |
v.addSelf(rect.centroid); | |
}); | |
return this; | |
}, | |
/** | |
* Orient the vertices of the shape to the center of the | |
* polygon. | |
*/ | |
center: function() { | |
var rect = this.getBoundingClientRect(true); | |
rect.centroid = { | |
x: rect.left + rect.width / 2, | |
y: rect.top + rect.height / 2 | |
}; | |
_.each(this.vertices, function(v) { | |
v.subSelf(rect.centroid); | |
}); | |
// this.translation.addSelf(rect.centroid); | |
return this; | |
}, | |
/** | |
* Remove self from the scene / parent. | |
*/ | |
remove: function() { | |
if (!this.parent) { | |
return this; | |
} | |
this.parent.remove(this); | |
return this; | |
}, | |
/** | |
* Return an object with top, left, right, bottom, width, and height | |
* parameters of the group. | |
*/ | |
getBoundingClientRect: function(shallow) { | |
// TODO: Update this to not __always__ update. Just when it needs to. | |
this._update(); | |
var matrix = !!shallow ? this._matrix : getComputedMatrix(this); | |
var border = this.linewidth / 2, x, y; | |
var left = Infinity, right = -Infinity, | |
top = Infinity, bottom = -Infinity; | |
_.each(this._vertices, function(v) { | |
x = v.x; | |
y = v.y; | |
v = matrix.multiply(x, y , 1); | |
top = min(v.y - border, top); | |
left = min(v.x - border, left); | |
right = max(v.x + border, right); | |
bottom = max(v.y + border, bottom); | |
}); | |
return { | |
top: top, | |
left: left, | |
right: right, | |
bottom: bottom, | |
width: right - left, | |
height: bottom - top | |
}; | |
}, | |
/** | |
* Given a float `t` from 0 to 1, return a point or assign a passed `obj`'s | |
* coordinates to that percentage on this Two.Polygon's curve. | |
*/ | |
getPointAt: function(t, obj) { | |
var x, x1, x2, x3, x4, y, y1, y2, y3, y4, left, right; | |
var target = this.length * Math.min(Math.max(t, 0), 1); | |
var length = this.vertices.length; | |
var last = length - 1; | |
var a = null; | |
var b = null; | |
for (var i = 0, l = this._lengths.length, sum = 0; i < l; i++) { | |
if (sum + this._lengths[i] > target) { | |
a = this.vertices[this.closed ? Two.Utils.mod(i, length) : i]; | |
b = this.vertices[Math.min(Math.max(i - 1, 0), last)]; | |
target -= sum; | |
t = target / this._lengths[i]; | |
break; | |
} | |
sum += this._lengths[i]; | |
} | |
if (_.isNull(a) || _.isNull(b)) { | |
return null; | |
} | |
right = b.controls && b.controls.right; | |
left = a.controls && a.controls.left; | |
x1 = b.x; | |
y1 = b.y; | |
x2 = (right || b).x; | |
y2 = (right || b).y; | |
x3 = (left || a).x; | |
y3 = (left || a).y; | |
x4 = a.x; | |
y4 = a.y; | |
if (right && b._relative) { | |
x2 += b.x; | |
y2 += b.y; | |
} | |
if (left && a._relative) { | |
x3 += a.x; | |
y3 += a.y; | |
} | |
x = Two.Utils.getPointOnCubicBezier(t, x1, x2, x3, x4); | |
y = Two.Utils.getPointOnCubicBezier(t, y1, y2, y3, y4); | |
if (_.isObject(obj)) { | |
obj.x = x; | |
obj.y = y; | |
return obj; | |
} | |
return new Two.Vector(x, y); | |
}, | |
/** | |
* Based on closed / curved and sorting of vertices plot where all points | |
* should be and where the respective handles should be too. | |
*/ | |
plot: function() { | |
if (this.curved) { | |
Two.Utils.getCurveFromPoints(this._vertices, this.closed); | |
return this; | |
} | |
_.each(this._vertices, function(p, i) { | |
p._command = i === 0 ? Two.Commands.move : Two.Commands.line; | |
}, this); | |
return this; | |
}, | |
subdivide: function(limit) { | |
//TODO: DRYness (function below) | |
this._update(); | |
var last = this.vertices.length - 1; | |
var b = this.vertices[last]; | |
var closed = this._closed || this.vertices[last]._command === Two.Commands.close; | |
var points = []; | |
_.each(this.vertices, function(a, i) { | |
if (i <= 0 && !closed) { | |
b = a; | |
return; | |
} | |
if (a.command === Two.Commands.move) { | |
points.push(new Two.Anchor(b.x, b.y)); | |
if (i > 0) { | |
points[points.length - 1].command = Two.Commands.line; | |
} | |
b = a; | |
return; | |
} | |
var verts = getSubdivisions(a, b, limit); | |
points = points.concat(verts); | |
// Assign commands to all the verts | |
_.each(verts, function(v, i) { | |
if (i <= 0 && b.command === Two.Commands.move) { | |
v.command = Two.Commands.move; | |
} else { | |
v.command = Two.Commands.line; | |
} | |
}); | |
if (i >= last) { | |
// TODO: Add check if the two vectors in question are the same values. | |
if (this._closed && this._automatic) { | |
b = a; | |
verts = getSubdivisions(a, b, limit); | |
points = points.concat(verts); | |
// Assign commands to all the verts | |
_.each(verts, function(v, i) { | |
if (i <= 0 && b.command === Two.Commands.move) { | |
v.command = Two.Commands.move; | |
} else { | |
v.command = Two.Commands.line; | |
} | |
}); | |
} else if (closed) { | |
points.push(new Two.Anchor(a.x, a.y)); | |
} | |
points[points.length - 1].command = closed ? Two.Commands.close : Two.Commands.line; | |
} | |
b = a; | |
}, this); | |
this._automatic = false; | |
this._curved = false; | |
this.vertices = points; | |
return this; | |
}, | |
_updateLength: function(limit) { | |
//TODO: DRYness (function above) | |
this._update(); | |
var last = this.vertices.length - 1; | |
var b = this.vertices[last]; | |
var closed = this._closed || this.vertices[last]._command === Two.Commands.close; | |
var sum = 0; | |
if (_.isUndefined(this._lengths)) { | |
this._lengths = []; | |
} | |
_.each(this.vertices, function(a, i) { | |
if ((i <= 0 && !closed) || a.command === Two.Commands.move) { | |
b = a; | |
this._lengths[i] = 0; | |
return; | |
} | |
this._lengths[i] = getCurveLength(a, b, limit); | |
sum += this._lengths[i]; | |
if (i >= last && closed) { | |
b = a; | |
this._lengths[i + 1] = getCurveLength(a, b, limit); | |
sum += this._lengths[i + 1]; | |
} | |
b = a; | |
}, this); | |
this._length = sum; | |
return this; | |
}, | |
_update: function() { | |
if (this._flagVertices) { | |
var l = this.vertices.length; | |
var last = l - 1, v; | |
var ia = round((this._beginning) * last); | |
var ib = round((this._ending) * last); | |
this._vertices.length = 0; | |
for (var i = ia; i < ib + 1; i++) { | |
v = this.vertices[i]; | |
this._vertices.push(v); | |
} | |
if (this._automatic) { | |
this.plot(); | |
} | |
} | |
Two.Shape.prototype._update.call(this); | |
return this; | |
}, | |
flagReset: function() { | |
this._flagVertices = this._flagFill = this._flagStroke = | |
this._flagLinewidth = this._flagOpacity = this._flagVisible = | |
this._flagCap = this._flagJoin = this._flagMiter = false; | |
Two.Shape.prototype.flagReset.call(this); | |
return this; | |
} | |
}); | |
Polygon.MakeObservable(Polygon.prototype); | |
function getCurveLength(a, b, limit) { | |
// TODO: DRYness | |
var x1, x2, x3, x4, y1, y2, y3, y4; | |
var right = b.controls && b.controls.right; | |
var left = a.controls && a.controls.left; | |
x1 = b.x; | |
y1 = b.y; | |
x2 = (right || b).x; | |
y2 = (right || b).y; | |
x3 = (left || a).x; | |
y3 = (left || a).y; | |
x4 = a.x; | |
y4 = a.y; | |
if (right && b._relative) { | |
x2 += b.x; | |
y2 += b.y; | |
} | |
if (left && a._relative) { | |
x3 += a.x; | |
y3 += a.y; | |
} | |
return Two.Utils.getCurveLength(x1, y1, x2, y2, x3, y3, x4, y4, limit); | |
} | |
function getSubdivisions(a, b, limit) { | |
// TODO: DRYness | |
var x1, x2, x3, x4, y1, y2, y3, y4; | |
var right = b.controls && b.controls.right; | |
var left = a.controls && a.controls.left; | |
x1 = b.x; | |
y1 = b.y; | |
x2 = (right || b).x; | |
y2 = (right || b).y; | |
x3 = (left || a).x; | |
y3 = (left || a).y; | |
x4 = a.x; | |
y4 = a.y; | |
if (right && b._relative) { | |
x2 += b.x; | |
y2 += b.y; | |
} | |
if (left && a._relative) { | |
x3 += a.x; | |
y3 += a.y; | |
} | |
return Two.Utils.subdivide(x1, y1, x2, y2, x3, y3, x4, y4, limit); | |
} | |
})(); | |
(function() { | |
/** | |
* Constants | |
*/ | |
var min = Math.min, max = Math.max; | |
var Group = Two.Group = function() { | |
Two.Shape.call(this, true); | |
this._renderer.type = 'group'; | |
this.additions = []; | |
this.subtractions = []; | |
this.children = {}; | |
}; | |
_.extend(Group, { | |
MakeObservable: function(object) { | |
Two.Shape.MakeObservable(object); | |
Group.MakeGetterSetters(object, Two.Polygon.Properties); | |
}, | |
MakeGetterSetters: function(group, properties) { | |
if (!_.isArray(properties)) { | |
properties = [properties]; | |
} | |
_.each(properties, function(k) { | |
Group.MakeGetterSetter(group, k); | |
}); | |
}, | |
MakeGetterSetter: function(group, k) { | |
var secret = '_' + k; | |
Object.defineProperty(group, k, { | |
get: function() { | |
return this[secret]; | |
}, | |
set: function(v) { | |
this[secret] = v; | |
_.each(this.children, function(child) { // Trickle down styles | |
child[k] = v; | |
}); | |
} | |
}); | |
} | |
}); | |
_.extend(Group.prototype, Two.Shape.prototype, { | |
// Flags | |
// http://en.wikipedia.org/wiki/Flag | |
_flagAdditions: false, | |
_flagSubtractions: false, | |
// Underlying Properties | |
_fill: '#fff', | |
_stroke: '#000', | |
_linewidth: 1.0, | |
_opacity: 1.0, | |
_visible: true, | |
_cap: 'round', | |
_join: 'round', | |
_miter: 4, | |
_closed: true, | |
_curved: false, | |
_automatic: true, | |
_beginning: 0, | |
_ending: 1.0, | |
/** | |
* Group has a gotcha in that it's at the moment required to be bound to | |
* an instance of two in order to add elements correctly. This needs to | |
* be rethought and fixed. | |
*/ | |
clone: function(parent) { | |
parent = parent || this.parent; | |
var group = new Group(); | |
parent.add(group); | |
var children = _.map(this.children, function(child) { | |
return child.clone(group); | |
}); | |
group.translation.copy(this.translation); | |
group.rotation = this.rotation; | |
group.scale = this.scale; | |
return group; | |
}, | |
toObject: function() { | |
var result = { | |
children: {}, | |
translation: this.translation.toObject(), | |
rotation: this.rotation, | |
scale: this.scale | |
}; | |
_.each(this.children, function(child, i) { | |
result.children[i] = child.toObject(); | |
}, this); | |
return result; | |
}, | |
/** | |
* Anchor all children to the upper left hand corner | |
* of the group. | |
*/ | |
corner: function() { | |
var rect = this.getBoundingClientRect(true), | |
corner = { x: rect.left, y: rect.top }; | |
_.each(this.children, function(child) { | |
child.translation.subSelf(corner); | |
}); | |
return this; | |
}, | |
/** | |
* Anchors all children around the center of the group, | |
* effectively placing the shape around the unit circle. | |
*/ | |
center: function() { | |
var rect = this.getBoundingClientRect(true); | |
rect.centroid = { | |
x: rect.left + rect.width / 2, | |
y: rect.top + rect.height / 2 | |
}; | |
_.each(this.children, function(child) { | |
child.translation.subSelf(rect.centroid); | |
}); | |
// this.translation.copy(rect.centroid); | |
return this; | |
}, | |
/** | |
* Recursively search for id. Returns the first element found. | |
* Returns null if none found. | |
*/ | |
getById: function (id) { | |
var search = function (node, id) { | |
if (node.id === id) { | |
return node; | |
} | |
for (var child in node.children) { | |
var found = search(node.children[child], id); | |
if (found) return found; | |
} | |
}; | |
return search(this, id) || null; | |
}, | |
/** | |
* Recursively search for classes. Returns an array of matching elements. | |
* Empty array if none found. | |
*/ | |
getByClassName: function (cl) { | |
var found = []; | |
var search = function (node, cl) { | |
if (node.classList.indexOf(cl) != -1) { | |
found.push(node); | |
} | |
for (var child in node.children) { | |
search(node.children[child], cl); | |
} | |
return found; | |
}; | |
return search(this, cl); | |
}, | |
/** | |
* Recursively search for children of a specific type, | |
* e.g. Two.Polygon. Pass a reference to this type as the param. | |
* Returns an empty array if none found. | |
*/ | |
getByType: function(type) { | |
var found = []; | |
var search = function (node, type) { | |
for (var id in node.children) { | |
if (node.children[id] instanceof type) { | |
found.push(node.children[id]); | |
} else if (node.children[id] instanceof Two.Group) { | |
search(node.children[id], type); | |
} | |
} | |
return found; | |
}; | |
return search(this, type); | |
}, | |
/** | |
* Add objects to the group. | |
*/ | |
add: function(objects) { | |
var l = arguments.length, | |
children = this.children, | |
grandparent = this.parent, | |
ids = this.additions, | |
id, parent, index; | |
if (!_.isArray(objects)) { | |
objects = _.toArray(arguments); | |
} | |
// Add the objects | |
_.each(objects, function(object) { | |
if (!object) { | |
return; | |
} | |
id = object.id; | |
parent = object.parent; | |
if (_.isUndefined(children[id])) { | |
// Release object from previous parent. | |
if (parent) { | |
delete parent.children[id]; | |
index = _.indexOf(parent.additions, id); | |
if (index >= 0) { | |
parent.additions.splice(index, 1); | |
} | |
} | |
// Add it to this group and update parent-child relationship. | |
children[id] = object; | |
object.parent = this; | |
ids.push(id); | |
this._flagAdditions = true; | |
} | |
}, this); | |
return this; | |
}, | |
/** | |
* Remove objects from the group. | |
*/ | |
remove: function(objects) { | |
var l = arguments.length, | |
children = this.children, | |
grandparent = this.parent, | |
ids = this.subtractions, | |
id, parent, index, grandchildren; | |
if (l <= 0 && grandparent) { | |
grandparent.remove(this); | |
return this; | |
} | |
if (!_.isArray(objects)) { | |
objects = _.toArray(arguments); | |
} | |
_.each(objects, function(object) { | |
id = object.id; | |
grandchildren = object.children; | |
parent = object.parent; | |
if (!(id in children)) { | |
return; | |
} | |
delete children[id]; | |
delete object.parent; | |
index = _.indexOf(parent.additions, id); | |
if (index >= 0) { | |
parent.additions.splice(index, 1); | |
} | |
ids.push(id); | |
this._flagSubtractions = true; | |
}, this); | |
return this; | |
}, | |
/** | |
* Return an object with top, left, right, bottom, width, and height | |
* parameters of the group. | |
*/ | |
getBoundingClientRect: function() { | |
var rect; | |
// TODO: Update this to not __always__ update. Just when it needs to. | |
this._update(); | |
// Variables need to be defined here, because of nested nature of groups. | |
var left = Infinity, right = -Infinity, | |
top = Infinity, bottom = -Infinity; | |
_.each(this.children, function(child) { | |
rect = child.getBoundingClientRect(); | |
if (!_.isNumber(rect.top) || !_.isNumber(rect.left) || | |
!_.isNumber(rect.right) || !_.isNumber(rect.bottom)) { | |
return; | |
} | |
top = min(rect.top, top); | |
left = min(rect.left, left); | |
right = max(rect.right, right); | |
bottom = max(rect.bottom, bottom); | |
}, this); | |
return { | |
top: top, | |
left: left, | |
right: right, | |
bottom: bottom, | |
width: right - left, | |
height: bottom - top | |
}; | |
}, | |
/** | |
* Trickle down of noFill | |
*/ | |
noFill: function() { | |
_.each(this.children, function(child) { | |
child.noFill(); | |
}); | |
return this; | |
}, | |
/** | |
* Trickle down of noStroke | |
*/ | |
noStroke: function() { | |
_.each(this.children, function(child) { | |
child.noStroke(); | |
}); | |
return this; | |
}, | |
/** | |
* Trickle down subdivide | |
*/ | |
subdivide: function() { | |
var args = arguments; | |
_.each(this.children, function(child) { | |
child.subdivide.apply(child, args); | |
}); | |
return this; | |
}, | |
flagReset: function() { | |
if (this._flagAdditions) { | |
this.additions.length = 0; | |
this._flagAdditions = false; | |
} | |
if (this._flagSubtractions) { | |
this.subtractions.length = 0; | |
this._flagSubtractions = false; | |
} | |
Two.Shape.prototype.flagReset.call(this); | |
return this; | |
} | |
}); | |
Group.MakeObservable(Group.prototype); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment