diff --git a/.travis.yml b/.travis.yml index 0ab1ff7b..57b654b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,4 +41,4 @@ script: - gulp minify - gulp test - gulp zip -- '[ "${TRAVIS_BRANCH}" = "develop" ] && [ "${TRAVIS_NODE_VERSION}" = "stable" ] && travis/deploy-prebuilt.sh' +- '[ "${TRAVIS_BRANCH}" = "develop" ] && [ "${TRAVIS_NODE_VERSION}" = "stable" ] && travis/deploy-prebuilt.sh || true' diff --git a/README.md b/README.md index 923e3dca..ca80c52c 100644 --- a/README.md +++ b/README.md @@ -51,17 +51,17 @@ generally not recommended to install Node.js through OS-supplied package managers, as the its development cycles move fast and these versions are often out-of-date. +On macOS, [Homebrew](http://brew.sh/) is a good option if one version of +Node.js that is kept up to date with `brew upgrade` is enough: + + [NVM](https://github.com/creationix/nvm) can be used instead to install and maintain multiple versions of Node.js on the same platform, as often required by different projects: -on OSX, [Homebrew](http://brew.sh/) is also a good option if one version of -Node.js that is kept up to date with `brew update` is enough: - - -Homebrew is recommended on OSX also if you intend to install Paper.js for -Node.js, as described in the next paragraph. +Homebrew is recommended on macOS also if you intend to install Paper.js with +rendering to the Canvas on Node.js, as described in the next paragraph. For Linux, see to locate 32-bit and 64-bit Node.js binaries as well as sources, or use NVM, as described in the paragraph above. @@ -83,14 +83,14 @@ different one: In order to install `paper-jsdom-canvas`, you need the [Cairo Graphics library](http://cairographics.org/) installed in your system: -##### Installing Cairo and Pango on OSX: +##### Installing Cairo and Pango on macOS: The easiest way to install Cairo is through [Homebrew](http://brew.sh/), by issuing the command: brew install cairo pango -Note that currently there is an issue on OSX with Cairo. If the above causes +Note that currently there is an issue on macOS with Cairo. If the above causes errors, the following will most likely fix it: PKG_CONFIG_PATH=/opt/X11/lib/pkgconfig/ npm install paper diff --git a/dist/paper-core.js b/dist/paper-core.js index 947f9f7d..9ccd6c7e 100644 --- a/dist/paper-core.js +++ b/dist/paper-core.js @@ -1,5 +1,5 @@ /*! - * Paper.js v0.11.2 - The Swiss Army Knife of Vector Graphics Scripting. + * Paper.js v0.11.3 - The Swiss Army Knife of Vector Graphics Scripting. * http://paperjs.org/ * * Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey @@ -9,7 +9,7 @@ * * All rights reserved. * - * Date: Thu Apr 20 19:14:30 2017 +0200 + * Date: Sat Apr 22 20:01:34 2017 +0200 * *** * @@ -98,12 +98,13 @@ var Base = new function() { && (bean = name.match(/^([gs]et|is)(([A-Z])(.*))$/))) beansNames[bean[3].toLowerCase() + bean[4]] = bean[2]; if (!res || isFunc || !res.get || typeof res.get !== 'function' - || !Base.isPlainObject(res)) + || !Base.isPlainObject(res)) { res = { value: res, writable: true }; + } if ((describe(dest, name) || { configurable: true }).configurable) { res.configurable = true; - res.enumerable = enumerable; + res.enumerable = enumerable != null ? enumerable : !bean; } define(dest, name, res); } @@ -141,7 +142,7 @@ var Base = new function() { preserve = src.preserve; if (statics !== src) inject(this.prototype, src, src.enumerable, beans, preserve); - inject(this, statics, true, beans, preserve); + inject(this, statics, null, beans, preserve); } for (var i = 1, l = arguments.length; i < l; i++) this.inject(arguments[i]); @@ -164,13 +165,15 @@ var Base = new function() { proto = ctor.prototype = proto || create(this.prototype); define(proto, 'constructor', { value: ctor, writable: true, configurable: true }); - inject(ctor, this, true); + inject(ctor, this); if (arguments.length) this.inject.apply(ctor, arguments); ctor.base = base; return ctor; } - }, true).inject({ + }).inject({ + enumerable: false, + initialize: Base, set: Base, @@ -230,6 +233,8 @@ if (typeof module !== 'undefined') module.exports = Base; Base.inject({ + enumerable: false, + toString: function() { return this._id != null ? (this._class || 'Object') + (this._name @@ -265,329 +270,333 @@ Base.inject({ if (props) Base.filter(this, props, exclude, this._prioritize); return this; + } +}, { + +beans: false, +statics: { + exports: {}, + + extend: function extend() { + var res = extend.base.apply(this, arguments), + name = res.prototype._class; + if (name && !Base.exports[name]) + Base.exports[name] = res; + return res; }, - statics: { - - exports: { - enumerable: true - }, - - extend: function extend() { - var res = extend.base.apply(this, arguments), - name = res.prototype._class; - if (name && !Base.exports[name]) - Base.exports[name] = res; - return res; - }, - - equals: function(obj1, obj2) { - if (obj1 === obj2) - return true; - if (obj1 && obj1.equals) - return obj1.equals(obj2); - if (obj2 && obj2.equals) - return obj2.equals(obj1); - if (obj1 && obj2 - && typeof obj1 === 'object' && typeof obj2 === 'object') { - if (Array.isArray(obj1) && Array.isArray(obj2)) { - var length = obj1.length; - if (length !== obj2.length) + equals: function(obj1, obj2) { + if (obj1 === obj2) + return true; + if (obj1 && obj1.equals) + return obj1.equals(obj2); + if (obj2 && obj2.equals) + return obj2.equals(obj1); + if (obj1 && obj2 + && typeof obj1 === 'object' && typeof obj2 === 'object') { + if (Array.isArray(obj1) && Array.isArray(obj2)) { + var length = obj1.length; + if (length !== obj2.length) + return false; + while (length--) { + if (!Base.equals(obj1[length], obj2[length])) return false; - while (length--) { - if (!Base.equals(obj1[length], obj2[length])) - return false; - } - } else { - var keys = Object.keys(obj1), - length = keys.length; - if (length !== Object.keys(obj2).length) + } + } else { + var keys = Object.keys(obj1), + length = keys.length; + if (length !== Object.keys(obj2).length) + return false; + while (length--) { + var key = keys[length]; + if (!(obj2.hasOwnProperty(key) + && Base.equals(obj1[key], obj2[key]))) return false; - while (length--) { - var key = keys[length]; - if (!(obj2.hasOwnProperty(key) - && Base.equals(obj1[key], obj2[key]))) - return false; - } - } - return true; - } - return false; - }, - - read: function(list, start, options, amount) { - if (this === Base) { - var value = this.peek(list, start); - list.__index++; - return value; - } - var proto = this.prototype, - readIndex = proto._readIndex, - begin = start || readIndex && list.__index || 0, - length = list.length, - obj = list[begin]; - amount = amount || length - begin; - if (obj instanceof this - || options && options.readNull && obj == null && amount <= 1) { - if (readIndex) - list.__index = begin + 1; - return obj && options && options.clone ? obj.clone() : obj; - } - obj = Base.create(proto); - if (readIndex) - obj.__read = true; - obj = obj.initialize.apply(obj, begin > 0 || begin + amount < length - ? Base.slice(list, begin, begin + amount) - : list) || obj; - if (readIndex) { - list.__index = begin + obj.__read; - obj.__read = undefined; - } - return obj; - }, - - peek: function(list, start) { - return list[list.__index = start || list.__index || 0]; - }, - - remain: function(list) { - return list.length - (list.__index || 0); - }, - - readList: function(list, start, options, amount) { - var res = [], - entry, - begin = start || 0, - end = amount ? begin + amount : list.length; - for (var i = begin; i < end; i++) { - res.push(Array.isArray(entry = list[i]) - ? this.read(entry, 0, options) - : this.read(list, i, options, 1)); - } - return res; - }, - - readNamed: function(list, name, start, options, amount) { - var value = this.getNamed(list, name), - hasObject = value !== undefined; - if (hasObject) { - var filtered = list._filtered; - if (!filtered) { - filtered = list._filtered = Base.create(list[0]); - filtered._unfiltered = list[0]; - } - filtered[name] = undefined; - } - return this.read(hasObject ? [value] : list, start, options, amount); - }, - - getNamed: function(list, name) { - var arg = list[0]; - if (list._hasObject === undefined) - list._hasObject = list.length === 1 && Base.isPlainObject(arg); - if (list._hasObject) - return name ? arg[name] : list._filtered || arg; - }, - - hasNamed: function(list, name) { - return !!this.getNamed(list, name); - }, - - filter: function(dest, source, exclude, prioritize) { - var processed; - - function handleKey(key) { - if (!(exclude && key in exclude) && - !(processed && key in processed)) { - var value = source[key]; - if (value !== undefined) - dest[key] = value; } } - - if (prioritize) { - var keys = {}; - for (var i = 0, key, l = prioritize.length; i < l; i++) { - if ((key = prioritize[i]) in source) { - handleKey(key); - keys[key] = true; - } - } - processed = keys; - } - - Object.keys(source._unfiltered || source).forEach(handleKey); - return dest; - }, - - isPlainValue: function(obj, asString) { - return Base.isPlainObject(obj) || Array.isArray(obj) - || asString && typeof obj === 'string'; - }, - - serialize: function(obj, options, compact, dictionary) { - options = options || {}; - - var isRoot = !dictionary, - res; - if (isRoot) { - options.formatter = new Formatter(options.precision); - dictionary = { - length: 0, - definitions: {}, - references: {}, - add: function(item, create) { - var id = '#' + item._id, - ref = this.references[id]; - if (!ref) { - this.length++; - var res = create.call(item), - name = item._class; - if (name && res[0] !== name) - res.unshift(name); - this.definitions[id] = res; - ref = this.references[id] = [id]; - } - return ref; - } - }; - } - if (obj && obj._serialize) { - res = obj._serialize(options, dictionary); - var name = obj._class; - if (name && !obj._compactSerialize && (isRoot || !compact) - && res[0] !== name) { - res.unshift(name); - } - } else if (Array.isArray(obj)) { - res = []; - for (var i = 0, l = obj.length; i < l; i++) - res[i] = Base.serialize(obj[i], options, compact, - dictionary); - } else if (Base.isPlainObject(obj)) { - res = {}; - var keys = Object.keys(obj); - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - res[key] = Base.serialize(obj[key], options, compact, - dictionary); - } - } else if (typeof obj === 'number') { - res = options.formatter.number(obj, options.precision); - } else { - res = obj; - } - return isRoot && dictionary.length > 0 - ? [['dictionary', dictionary.definitions], res] - : res; - }, - - deserialize: function(json, create, _data, _setDictionary, _isRoot) { - var res = json, - isFirst = !_data, - hasDictionary = isFirst && json && json.length - && json[0][0] === 'dictionary'; - _data = _data || {}; - if (Array.isArray(json)) { - var type = json[0], - isDictionary = type === 'dictionary'; - if (json.length == 1 && /^#/.test(type)) { - return _data.dictionary[type]; - } - type = Base.exports[type]; - res = []; - for (var i = type ? 1 : 0, l = json.length; i < l; i++) { - res.push(Base.deserialize(json[i], create, _data, - isDictionary, hasDictionary)); - } - if (type) { - var args = res; - if (create) { - res = create(type, args, isFirst || _isRoot); - } else { - res = Base.create(type.prototype); - type.apply(res, args); - } - } - } else if (Base.isPlainObject(json)) { - res = {}; - if (_setDictionary) - _data.dictionary = res; - for (var key in json) - res[key] = Base.deserialize(json[key], create, _data); - } - return hasDictionary ? res[1] : res; - }, - - exportJSON: function(obj, options) { - var json = Base.serialize(obj, options); - return options && options.asString == false - ? json - : JSON.stringify(json); - }, - - importJSON: function(json, target) { - return Base.deserialize( - typeof json === 'string' ? JSON.parse(json) : json, - function(ctor, args, isRoot) { - var useTarget = isRoot && target - && target.constructor === ctor, - obj = useTarget ? target - : Base.create(ctor.prototype); - if (args.length === 1 && obj instanceof Item - && (useTarget || !(obj instanceof Layer))) { - var arg = args[0]; - if (Base.isPlainObject(arg)) - arg.insert = false; - } - (useTarget ? obj.set : ctor).apply(obj, args); - if (useTarget) - target = null; - return obj; - }); - }, - - splice: function(list, items, index, remove) { - var amount = items && items.length, - append = index === undefined; - index = append ? list.length : index; - if (index > list.length) - index = list.length; - for (var i = 0; i < amount; i++) - items[i]._index = index + i; - if (append) { - list.push.apply(list, items); - return []; - } else { - var args = [index, remove]; - if (items) - args.push.apply(args, items); - var removed = list.splice.apply(list, args); - for (var i = 0, l = removed.length; i < l; i++) - removed[i]._index = undefined; - for (var i = index + amount, l = list.length; i < l; i++) - list[i]._index = i; - return removed; - } - }, - - capitalize: function(str) { - return str.replace(/\b[a-z]/g, function(match) { - return match.toUpperCase(); - }); - }, - - camelize: function(str) { - return str.replace(/-(.)/g, function(match, chr) { - return chr.toUpperCase(); - }); - }, - - hyphenate: function(str) { - return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + return true; } + return false; + }, + + read: function(list, start, options, amount) { + if (this === Base) { + var value = this.peek(list, start); + list.__index++; + return value; + } + var proto = this.prototype, + readIndex = proto._readIndex, + begin = start || readIndex && list.__index || 0, + length = list.length, + obj = list[begin]; + amount = amount || length - begin; + if (obj instanceof this + || options && options.readNull && obj == null && amount <= 1) { + if (readIndex) + list.__index = begin + 1; + return obj && options && options.clone ? obj.clone() : obj; + } + obj = Base.create(proto); + if (readIndex) + obj.__read = true; + obj = obj.initialize.apply(obj, begin > 0 || begin + amount < length + ? Base.slice(list, begin, begin + amount) + : list) || obj; + if (readIndex) { + list.__index = begin + obj.__read; + var filtered = obj.__filtered; + if (filtered) { + list.__filtered = filtered; + obj.__filtered = undefined; + } + obj.__read = undefined; + } + return obj; + }, + + peek: function(list, start) { + return list[list.__index = start || list.__index || 0]; + }, + + remain: function(list) { + return list.length - (list.__index || 0); + }, + + readList: function(list, start, options, amount) { + var res = [], + entry, + begin = start || 0, + end = amount ? begin + amount : list.length; + for (var i = begin; i < end; i++) { + res.push(Array.isArray(entry = list[i]) + ? this.read(entry, 0, options) + : this.read(list, i, options, 1)); + } + return res; + }, + + readNamed: function(list, name, start, options, amount) { + var value = this.getNamed(list, name), + hasObject = value !== undefined; + if (hasObject) { + var filtered = list.__filtered; + if (!filtered) { + filtered = list.__filtered = Base.create(list[0]); + filtered.__unfiltered = list[0]; + } + filtered[name] = undefined; + } + var l = hasObject ? [value] : list, + res = this.read(l, start, options, amount); + return res; + }, + + getNamed: function(list, name) { + var arg = list[0]; + if (list._hasObject === undefined) + list._hasObject = list.length === 1 && Base.isPlainObject(arg); + if (list._hasObject) + return name ? arg[name] : list.__filtered || arg; + }, + + hasNamed: function(list, name) { + return !!this.getNamed(list, name); + }, + + filter: function(dest, source, exclude, prioritize) { + var processed; + + function handleKey(key) { + if (!(exclude && key in exclude) && + !(processed && key in processed)) { + var value = source[key]; + if (value !== undefined) + dest[key] = value; + } + } + + if (prioritize) { + var keys = {}; + for (var i = 0, key, l = prioritize.length; i < l; i++) { + if ((key = prioritize[i]) in source) { + handleKey(key); + keys[key] = true; + } + } + processed = keys; + } + + Object.keys(source.__unfiltered || source).forEach(handleKey); + return dest; + }, + + isPlainValue: function(obj, asString) { + return Base.isPlainObject(obj) || Array.isArray(obj) + || asString && typeof obj === 'string'; + }, + + serialize: function(obj, options, compact, dictionary) { + options = options || {}; + + var isRoot = !dictionary, + res; + if (isRoot) { + options.formatter = new Formatter(options.precision); + dictionary = { + length: 0, + definitions: {}, + references: {}, + add: function(item, create) { + var id = '#' + item._id, + ref = this.references[id]; + if (!ref) { + this.length++; + var res = create.call(item), + name = item._class; + if (name && res[0] !== name) + res.unshift(name); + this.definitions[id] = res; + ref = this.references[id] = [id]; + } + return ref; + } + }; + } + if (obj && obj._serialize) { + res = obj._serialize(options, dictionary); + var name = obj._class; + if (name && !obj._compactSerialize && (isRoot || !compact) + && res[0] !== name) { + res.unshift(name); + } + } else if (Array.isArray(obj)) { + res = []; + for (var i = 0, l = obj.length; i < l; i++) + res[i] = Base.serialize(obj[i], options, compact, dictionary); + } else if (Base.isPlainObject(obj)) { + res = {}; + var keys = Object.keys(obj); + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + res[key] = Base.serialize(obj[key], options, compact, + dictionary); + } + } else if (typeof obj === 'number') { + res = options.formatter.number(obj, options.precision); + } else { + res = obj; + } + return isRoot && dictionary.length > 0 + ? [['dictionary', dictionary.definitions], res] + : res; + }, + + deserialize: function(json, create, _data, _setDictionary, _isRoot) { + var res = json, + isFirst = !_data, + hasDictionary = isFirst && json && json.length + && json[0][0] === 'dictionary'; + _data = _data || {}; + if (Array.isArray(json)) { + var type = json[0], + isDictionary = type === 'dictionary'; + if (json.length == 1 && /^#/.test(type)) { + return _data.dictionary[type]; + } + type = Base.exports[type]; + res = []; + for (var i = type ? 1 : 0, l = json.length; i < l; i++) { + res.push(Base.deserialize(json[i], create, _data, + isDictionary, hasDictionary)); + } + if (type) { + var args = res; + if (create) { + res = create(type, args, isFirst || _isRoot); + } else { + res = Base.create(type.prototype); + type.apply(res, args); + } + } + } else if (Base.isPlainObject(json)) { + res = {}; + if (_setDictionary) + _data.dictionary = res; + for (var key in json) + res[key] = Base.deserialize(json[key], create, _data); + } + return hasDictionary ? res[1] : res; + }, + + exportJSON: function(obj, options) { + var json = Base.serialize(obj, options); + return options && options.asString == false + ? json + : JSON.stringify(json); + }, + + importJSON: function(json, target) { + return Base.deserialize( + typeof json === 'string' ? JSON.parse(json) : json, + function(ctor, args, isRoot) { + var useTarget = isRoot && target + && target.constructor === ctor, + obj = useTarget ? target + : Base.create(ctor.prototype); + if (args.length === 1 && obj instanceof Item + && (useTarget || !(obj instanceof Layer))) { + var arg = args[0]; + if (Base.isPlainObject(arg)) + arg.insert = false; + } + (useTarget ? obj.set : ctor).apply(obj, args); + if (useTarget) + target = null; + return obj; + }); + }, + + splice: function(list, items, index, remove) { + var amount = items && items.length, + append = index === undefined; + index = append ? list.length : index; + if (index > list.length) + index = list.length; + for (var i = 0; i < amount; i++) + items[i]._index = index + i; + if (append) { + list.push.apply(list, items); + return []; + } else { + var args = [index, remove]; + if (items) + args.push.apply(args, items); + var removed = list.splice.apply(list, args); + for (var i = 0, l = removed.length; i < l; i++) + removed[i]._index = undefined; + for (var i = index + amount, l = list.length; i < l; i++) + list[i]._index = i; + return removed; + } + }, + + capitalize: function(str) { + return str.replace(/\b[a-z]/g, function(match) { + return match.toUpperCase(); + }); + }, + + camelize: function(str) { + return str.replace(/-(.)/g, function(match, chr) { + return chr.toUpperCase(); + }); + }, + + hyphenate: function(str) { + return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } -}); +}}); var Emitter = { on: function(type, func) { @@ -769,7 +778,7 @@ var PaperScope = Base.extend({ } }, - version: "0.11.2", + version: "0.11.3", getView: function() { var project = this.project; @@ -1739,6 +1748,9 @@ var Rectangle = Base.extend({ } this._set(x, y, width, height); read = arguments.__index; + var filtered = arguments.__filtered; + if (filtered) + this.__filtered = filtered; } if (this.__read) this.__read = read; @@ -2262,7 +2274,7 @@ var Matrix = Base.extend({ return this.shear(shear, center); }, - append: function(mx) { + append: function(mx, _dontNotify) { if (mx) { var a1 = this._a, b1 = this._b, @@ -2280,12 +2292,13 @@ var Matrix = Base.extend({ this._d = b2 * b1 + d2 * d1; this._tx += tx2 * a1 + ty2 * c1; this._ty += tx2 * b1 + ty2 * d1; - this._changed(); + if (!_dontNotify) + this._changed(); } return this; }, - prepend: function(mx) { + prepend: function(mx, _dontNotify) { if (mx) { var a1 = this._a, b1 = this._b, @@ -2305,7 +2318,8 @@ var Matrix = Base.extend({ this._d = c2 * c1 + d2 * d1; this._tx = a2 * tx1 + b2 * ty1 + tx2; this._ty = c2 * tx1 + d2 * ty1 + ty2; - this._changed(); + if (!_dontNotify) + this._changed(); } return this; }, @@ -3236,11 +3250,11 @@ new function() { this._boundsOptions); if (!opts.stroke || this.getStrokeScaling()) opts.cacheItem = this; - var bounds = this._getCachedBounds(hasMatrix && matrix, opts); + var rect = this._getCachedBounds(hasMatrix && matrix, opts).rect; return !arguments.length - ? new LinkedRectangle(bounds.x, bounds.y, bounds.width, - bounds.height, this, 'setBounds') - : bounds; + ? new LinkedRectangle(rect.x, rect.y, rect.width, rect.height, + this, 'setBounds') + : rect; }, setBounds: function() { @@ -3273,29 +3287,49 @@ new function() { return Item._getBounds(children, matrix, options); }, + _getBoundsCacheKey: function(options, internal) { + return [ + options.stroke ? 1 : 0, + options.handle ? 1 : 0, + internal ? 1 : 0 + ].join(''); + }, + _getCachedBounds: function(matrix, options, noInternal) { matrix = matrix && matrix._orNullIfIdentity(); var internal = options.internal && !noInternal, cacheItem = options.cacheItem, _matrix = internal ? null : this._matrix._orNullIfIdentity(), - cacheKey = cacheItem && (!matrix || matrix.equals(_matrix)) && [ - options.stroke ? 1 : 0, - options.handle ? 1 : 0, - internal ? 1 : 0 - ].join(''); + cacheKey = cacheItem && (!matrix || matrix.equals(_matrix)) + && this._getBoundsCacheKey(options, internal), + bounds = this._bounds; Item._updateBoundsCache(this._parent || this._symbol, cacheItem); - if (cacheKey && this._bounds && cacheKey in this._bounds) - return this._bounds[cacheKey].rect.clone(); - var bounds = this._getBounds(matrix || _matrix, options); + if (cacheKey && bounds && cacheKey in bounds) { + var cached = bounds[cacheKey]; + return { + rect: cached.rect.clone(), + nonscaling: cached.nonscaling + }; + } + var res = this._getBounds(matrix || _matrix, options), + rect = res.rect || res, + style = this._style, + nonscaling = res.nonscaling || style.hasStroke() + && !style.getStrokeScaling(); if (cacheKey) { - if (!this._bounds) - this._bounds = {}; - var cached = this._bounds[cacheKey] = { - rect: bounds.clone(), + if (!bounds) { + this._bounds = bounds = {}; + } + var cached = bounds[cacheKey] = { + rect: rect.clone(), + nonscaling: nonscaling, internal: internal }; } - return bounds; + return { + rect: rect, + nonscaling: nonscaling + }; }, _getStrokeMatrix: function(matrix, options) { @@ -3340,22 +3374,29 @@ new function() { var x1 = Infinity, x2 = -x1, y1 = x1, - y2 = x2; + y2 = x2, + nonscaling = false; options = options || {}; for (var i = 0, l = items.length; i < l; i++) { var item = items[i]; if (item._visible && !item.isEmpty()) { - var rect = item._getCachedBounds( - matrix && matrix.appended(item._matrix), options, true); + var bounds = item._getCachedBounds( + matrix && matrix.appended(item._matrix), options, true), + rect = bounds.rect; x1 = Math.min(rect.x, x1); y1 = Math.min(rect.y, y1); x2 = Math.max(rect.x + rect.width, x2); y2 = Math.max(rect.y + rect.height, y2); + if (bounds.nonscaling) + nonscaling = true; } } - return isFinite(x1) + return { + rect: isFinite(x1) ? new Rectangle(x1, y1, x2 - x1, y2 - y1) - : new Rectangle(); + : new Rectangle(), + nonscaling: nonscaling + }; } } @@ -3395,8 +3436,18 @@ new function() { var current = this.getScaling(), scaling = Point.read(arguments, 0, { clone: true, readNull: true }); if (current && scaling && !current.equals(scaling)) { - var decomposed = this._decomposed; - this.scale(scaling.x / current.x, scaling.y / current.y); + var rotation = this.getRotation(), + decomposed = this._decomposed, + matrix = new Matrix(), + center = this.getPosition(true); + matrix.translate(center); + if (rotation) + matrix.rotate(rotation); + matrix.scale(scaling.x / current.x, scaling.y / current.y); + if (rotation) + matrix.rotate(-rotation); + matrix.translate(center.negate()); + this.transform(matrix); if (decomposed) { decomposed.scaling = scaling; this._decomposed = decomposed; @@ -4218,8 +4269,6 @@ new function() { transform: function(matrix, _applyMatrix, _applyRecursively, _setApplyMatrix) { - if (matrix && matrix.isIdentity()) - matrix = null; var _matrix = this._matrix, transform = matrix && !matrix.isIdentity(), applyMatrix = (_applyMatrix || this._applyMatrix) @@ -4230,22 +4279,7 @@ new function() { if (transform) { if (!matrix.isInvertible() && _matrix.isInvertible()) _matrix._backup = _matrix.getValues(); - _matrix.prepend(matrix); - } - if (applyMatrix) { - if (this._transformContent(_matrix, _applyRecursively, - _setApplyMatrix)) { - var pivot = this._pivot; - if (pivot) - _matrix._transformPoint(pivot, pivot, true); - _matrix.reset(true); - if (_setApplyMatrix && this._canApplyMatrix) - this._applyMatrix = true; - } else { - applyMatrix = transform = false; - } - } - if (transform) { + _matrix.prepend(matrix, true); var style = this._style, fillColor = style.getFillColor(true), strokeColor = style.getStrokeColor(true); @@ -4254,24 +4288,38 @@ new function() { if (strokeColor) strokeColor.transform(matrix); } + if (applyMatrix && (applyMatrix = this._transformContent(_matrix, + _applyRecursively, _setApplyMatrix))) { + var pivot = this._pivot; + if (pivot) + _matrix._transformPoint(pivot, pivot, true); + _matrix.reset(true); + if (_setApplyMatrix && this._canApplyMatrix) + this._applyMatrix = true; + } var bounds = this._bounds, position = this._position; - this._changed(9); - var decomp = bounds && matrix && matrix.decompose(); - if (decomp && !decomp.shearing && decomp.rotation % 90 === 0) { + if (transform || applyMatrix) { + this._changed(9); + } + var decomp = transform && bounds && matrix.decompose(); + if (decomp && decomp.skewing.isZero() && decomp.rotation % 90 === 0) { for (var key in bounds) { var cache = bounds[key]; - if (applyMatrix || !cache.internal) { + if (cache.nonscaling) { + delete bounds[key]; + } else if (applyMatrix || !cache.internal) { var rect = cache.rect; matrix._transformBounds(rect, rect); } } - var getter = this._boundsGetter, - rect = bounds[getter && getter.getBounds || getter || 'getBounds']; - if (rect) - this._position = rect.getCenter(true); this._bounds = bounds; - } else if (matrix && position) { + var cached = bounds[this._getBoundsCacheKey( + this._boundsOptions || {})]; + if (cached) { + this._position = cached.rect.getCenter(true); + } + } else if (transform && position && this._pivot) { this._position = matrix._transformPoint(position, position); } return this; @@ -5483,10 +5531,8 @@ var HitResult = Base.extend({ initialize: function HitResult(type, item, values) { this.type = type; this.item = item; - if (values) { - values.enumerable = true; + if (values) this.inject(values); - } }, statics: { @@ -7006,7 +7052,7 @@ new function() { cos = Math.cos(angle), rv = [], roots = []; - for(var i = 0; i < 8; i += 2) { + for (var i = 0; i < 8; i += 2) { var x = v[i] - px, y = v[i + 1] - py; rv.push( @@ -11016,10 +11062,10 @@ var PointText = TextItem.extend({ x = 0; if (justification !== 'left') x -= width / (justification === 'center' ? 2: 1); - var bounds = new Rectangle(x, + var rect = new Rectangle(x, numLines ? - 0.75 * leading : 0, width, numLines * leading); - return matrix ? matrix._transformBounds(bounds, bounds) : bounds; + return matrix ? matrix._transformBounds(rect, rect) : rect; } }); @@ -12694,8 +12740,8 @@ new function() { point, prevPoint) || hitItem && hitItem !== dragItem && !hitItem.isDescendant(dragItem) - && emitMouseEvent(hitItem, null, fallbacks[type] || type, event, - point, prevPoint, dragItem) + && emitMouseEvent(hitItem, null, type, event, point, prevPoint, + dragItem) || emitMouseEvent(view, dragItem || hitItem || view, type, event, point, prevPoint)); } @@ -13807,9 +13853,9 @@ new function() { if (!Numerical.isZero(scale.x - 1) || !Numerical.isZero(scale.y - 1)) parts.push('scale(' + formatter.point(scale) +')'); - if (skew && skew.x) + if (skew.x) parts.push('skewX(' + formatter.number(skew.x) + ')'); - if (skew && skew.y) + if (skew.y) parts.push('skewY(' + formatter.number(skew.y) + ')'); attrs.transform = parts.join(' '); } else { @@ -13870,8 +13916,9 @@ new function() { if (length > 2) { type = item._closed ? 'polygon' : 'polyline'; var parts = []; - for(var i = 0; i < length; i++) + for (var i = 0; i < length; i++) { parts.push(formatter.point(segments[i]._point)); + } attrs.points = parts.join(' '); } else { type = 'line'; @@ -14142,6 +14189,7 @@ new function() { ? new Rectangle([0, 0], view.getViewSize()) : bounds === 'content' ? Item._getBounds(children, matrix, { stroke: true }) + .rect : Rectangle.read([bounds], 0, { readNull: true }), attrs = { version: '1.1', @@ -14715,7 +14763,6 @@ new function() { }; paper = new (PaperScope.inject(Base.exports, { - enumerable: true, Base: Base, Numerical: Numerical, Key: Key, diff --git a/dist/paper-full.js b/dist/paper-full.js index 4aea3d0c..034d5f0c 100644 --- a/dist/paper-full.js +++ b/dist/paper-full.js @@ -1,5 +1,5 @@ /*! - * Paper.js v0.11.2 - The Swiss Army Knife of Vector Graphics Scripting. + * Paper.js v0.11.3 - The Swiss Army Knife of Vector Graphics Scripting. * http://paperjs.org/ * * Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey @@ -9,7 +9,7 @@ * * All rights reserved. * - * Date: Thu Apr 20 19:14:30 2017 +0200 + * Date: Sat Apr 22 20:01:34 2017 +0200 * *** * @@ -98,12 +98,13 @@ var Base = new function() { && (bean = name.match(/^([gs]et|is)(([A-Z])(.*))$/))) beansNames[bean[3].toLowerCase() + bean[4]] = bean[2]; if (!res || isFunc || !res.get || typeof res.get !== 'function' - || !Base.isPlainObject(res)) + || !Base.isPlainObject(res)) { res = { value: res, writable: true }; + } if ((describe(dest, name) || { configurable: true }).configurable) { res.configurable = true; - res.enumerable = enumerable; + res.enumerable = enumerable != null ? enumerable : !bean; } define(dest, name, res); } @@ -141,7 +142,7 @@ var Base = new function() { preserve = src.preserve; if (statics !== src) inject(this.prototype, src, src.enumerable, beans, preserve); - inject(this, statics, true, beans, preserve); + inject(this, statics, null, beans, preserve); } for (var i = 1, l = arguments.length; i < l; i++) this.inject(arguments[i]); @@ -164,13 +165,15 @@ var Base = new function() { proto = ctor.prototype = proto || create(this.prototype); define(proto, 'constructor', { value: ctor, writable: true, configurable: true }); - inject(ctor, this, true); + inject(ctor, this); if (arguments.length) this.inject.apply(ctor, arguments); ctor.base = base; return ctor; } - }, true).inject({ + }).inject({ + enumerable: false, + initialize: Base, set: Base, @@ -230,6 +233,8 @@ if (typeof module !== 'undefined') module.exports = Base; Base.inject({ + enumerable: false, + toString: function() { return this._id != null ? (this._class || 'Object') + (this._name @@ -265,329 +270,333 @@ Base.inject({ if (props) Base.filter(this, props, exclude, this._prioritize); return this; + } +}, { + +beans: false, +statics: { + exports: {}, + + extend: function extend() { + var res = extend.base.apply(this, arguments), + name = res.prototype._class; + if (name && !Base.exports[name]) + Base.exports[name] = res; + return res; }, - statics: { - - exports: { - enumerable: true - }, - - extend: function extend() { - var res = extend.base.apply(this, arguments), - name = res.prototype._class; - if (name && !Base.exports[name]) - Base.exports[name] = res; - return res; - }, - - equals: function(obj1, obj2) { - if (obj1 === obj2) - return true; - if (obj1 && obj1.equals) - return obj1.equals(obj2); - if (obj2 && obj2.equals) - return obj2.equals(obj1); - if (obj1 && obj2 - && typeof obj1 === 'object' && typeof obj2 === 'object') { - if (Array.isArray(obj1) && Array.isArray(obj2)) { - var length = obj1.length; - if (length !== obj2.length) + equals: function(obj1, obj2) { + if (obj1 === obj2) + return true; + if (obj1 && obj1.equals) + return obj1.equals(obj2); + if (obj2 && obj2.equals) + return obj2.equals(obj1); + if (obj1 && obj2 + && typeof obj1 === 'object' && typeof obj2 === 'object') { + if (Array.isArray(obj1) && Array.isArray(obj2)) { + var length = obj1.length; + if (length !== obj2.length) + return false; + while (length--) { + if (!Base.equals(obj1[length], obj2[length])) return false; - while (length--) { - if (!Base.equals(obj1[length], obj2[length])) - return false; - } - } else { - var keys = Object.keys(obj1), - length = keys.length; - if (length !== Object.keys(obj2).length) + } + } else { + var keys = Object.keys(obj1), + length = keys.length; + if (length !== Object.keys(obj2).length) + return false; + while (length--) { + var key = keys[length]; + if (!(obj2.hasOwnProperty(key) + && Base.equals(obj1[key], obj2[key]))) return false; - while (length--) { - var key = keys[length]; - if (!(obj2.hasOwnProperty(key) - && Base.equals(obj1[key], obj2[key]))) - return false; - } - } - return true; - } - return false; - }, - - read: function(list, start, options, amount) { - if (this === Base) { - var value = this.peek(list, start); - list.__index++; - return value; - } - var proto = this.prototype, - readIndex = proto._readIndex, - begin = start || readIndex && list.__index || 0, - length = list.length, - obj = list[begin]; - amount = amount || length - begin; - if (obj instanceof this - || options && options.readNull && obj == null && amount <= 1) { - if (readIndex) - list.__index = begin + 1; - return obj && options && options.clone ? obj.clone() : obj; - } - obj = Base.create(proto); - if (readIndex) - obj.__read = true; - obj = obj.initialize.apply(obj, begin > 0 || begin + amount < length - ? Base.slice(list, begin, begin + amount) - : list) || obj; - if (readIndex) { - list.__index = begin + obj.__read; - obj.__read = undefined; - } - return obj; - }, - - peek: function(list, start) { - return list[list.__index = start || list.__index || 0]; - }, - - remain: function(list) { - return list.length - (list.__index || 0); - }, - - readList: function(list, start, options, amount) { - var res = [], - entry, - begin = start || 0, - end = amount ? begin + amount : list.length; - for (var i = begin; i < end; i++) { - res.push(Array.isArray(entry = list[i]) - ? this.read(entry, 0, options) - : this.read(list, i, options, 1)); - } - return res; - }, - - readNamed: function(list, name, start, options, amount) { - var value = this.getNamed(list, name), - hasObject = value !== undefined; - if (hasObject) { - var filtered = list._filtered; - if (!filtered) { - filtered = list._filtered = Base.create(list[0]); - filtered._unfiltered = list[0]; - } - filtered[name] = undefined; - } - return this.read(hasObject ? [value] : list, start, options, amount); - }, - - getNamed: function(list, name) { - var arg = list[0]; - if (list._hasObject === undefined) - list._hasObject = list.length === 1 && Base.isPlainObject(arg); - if (list._hasObject) - return name ? arg[name] : list._filtered || arg; - }, - - hasNamed: function(list, name) { - return !!this.getNamed(list, name); - }, - - filter: function(dest, source, exclude, prioritize) { - var processed; - - function handleKey(key) { - if (!(exclude && key in exclude) && - !(processed && key in processed)) { - var value = source[key]; - if (value !== undefined) - dest[key] = value; } } - - if (prioritize) { - var keys = {}; - for (var i = 0, key, l = prioritize.length; i < l; i++) { - if ((key = prioritize[i]) in source) { - handleKey(key); - keys[key] = true; - } - } - processed = keys; - } - - Object.keys(source._unfiltered || source).forEach(handleKey); - return dest; - }, - - isPlainValue: function(obj, asString) { - return Base.isPlainObject(obj) || Array.isArray(obj) - || asString && typeof obj === 'string'; - }, - - serialize: function(obj, options, compact, dictionary) { - options = options || {}; - - var isRoot = !dictionary, - res; - if (isRoot) { - options.formatter = new Formatter(options.precision); - dictionary = { - length: 0, - definitions: {}, - references: {}, - add: function(item, create) { - var id = '#' + item._id, - ref = this.references[id]; - if (!ref) { - this.length++; - var res = create.call(item), - name = item._class; - if (name && res[0] !== name) - res.unshift(name); - this.definitions[id] = res; - ref = this.references[id] = [id]; - } - return ref; - } - }; - } - if (obj && obj._serialize) { - res = obj._serialize(options, dictionary); - var name = obj._class; - if (name && !obj._compactSerialize && (isRoot || !compact) - && res[0] !== name) { - res.unshift(name); - } - } else if (Array.isArray(obj)) { - res = []; - for (var i = 0, l = obj.length; i < l; i++) - res[i] = Base.serialize(obj[i], options, compact, - dictionary); - } else if (Base.isPlainObject(obj)) { - res = {}; - var keys = Object.keys(obj); - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - res[key] = Base.serialize(obj[key], options, compact, - dictionary); - } - } else if (typeof obj === 'number') { - res = options.formatter.number(obj, options.precision); - } else { - res = obj; - } - return isRoot && dictionary.length > 0 - ? [['dictionary', dictionary.definitions], res] - : res; - }, - - deserialize: function(json, create, _data, _setDictionary, _isRoot) { - var res = json, - isFirst = !_data, - hasDictionary = isFirst && json && json.length - && json[0][0] === 'dictionary'; - _data = _data || {}; - if (Array.isArray(json)) { - var type = json[0], - isDictionary = type === 'dictionary'; - if (json.length == 1 && /^#/.test(type)) { - return _data.dictionary[type]; - } - type = Base.exports[type]; - res = []; - for (var i = type ? 1 : 0, l = json.length; i < l; i++) { - res.push(Base.deserialize(json[i], create, _data, - isDictionary, hasDictionary)); - } - if (type) { - var args = res; - if (create) { - res = create(type, args, isFirst || _isRoot); - } else { - res = Base.create(type.prototype); - type.apply(res, args); - } - } - } else if (Base.isPlainObject(json)) { - res = {}; - if (_setDictionary) - _data.dictionary = res; - for (var key in json) - res[key] = Base.deserialize(json[key], create, _data); - } - return hasDictionary ? res[1] : res; - }, - - exportJSON: function(obj, options) { - var json = Base.serialize(obj, options); - return options && options.asString == false - ? json - : JSON.stringify(json); - }, - - importJSON: function(json, target) { - return Base.deserialize( - typeof json === 'string' ? JSON.parse(json) : json, - function(ctor, args, isRoot) { - var useTarget = isRoot && target - && target.constructor === ctor, - obj = useTarget ? target - : Base.create(ctor.prototype); - if (args.length === 1 && obj instanceof Item - && (useTarget || !(obj instanceof Layer))) { - var arg = args[0]; - if (Base.isPlainObject(arg)) - arg.insert = false; - } - (useTarget ? obj.set : ctor).apply(obj, args); - if (useTarget) - target = null; - return obj; - }); - }, - - splice: function(list, items, index, remove) { - var amount = items && items.length, - append = index === undefined; - index = append ? list.length : index; - if (index > list.length) - index = list.length; - for (var i = 0; i < amount; i++) - items[i]._index = index + i; - if (append) { - list.push.apply(list, items); - return []; - } else { - var args = [index, remove]; - if (items) - args.push.apply(args, items); - var removed = list.splice.apply(list, args); - for (var i = 0, l = removed.length; i < l; i++) - removed[i]._index = undefined; - for (var i = index + amount, l = list.length; i < l; i++) - list[i]._index = i; - return removed; - } - }, - - capitalize: function(str) { - return str.replace(/\b[a-z]/g, function(match) { - return match.toUpperCase(); - }); - }, - - camelize: function(str) { - return str.replace(/-(.)/g, function(match, chr) { - return chr.toUpperCase(); - }); - }, - - hyphenate: function(str) { - return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + return true; } + return false; + }, + + read: function(list, start, options, amount) { + if (this === Base) { + var value = this.peek(list, start); + list.__index++; + return value; + } + var proto = this.prototype, + readIndex = proto._readIndex, + begin = start || readIndex && list.__index || 0, + length = list.length, + obj = list[begin]; + amount = amount || length - begin; + if (obj instanceof this + || options && options.readNull && obj == null && amount <= 1) { + if (readIndex) + list.__index = begin + 1; + return obj && options && options.clone ? obj.clone() : obj; + } + obj = Base.create(proto); + if (readIndex) + obj.__read = true; + obj = obj.initialize.apply(obj, begin > 0 || begin + amount < length + ? Base.slice(list, begin, begin + amount) + : list) || obj; + if (readIndex) { + list.__index = begin + obj.__read; + var filtered = obj.__filtered; + if (filtered) { + list.__filtered = filtered; + obj.__filtered = undefined; + } + obj.__read = undefined; + } + return obj; + }, + + peek: function(list, start) { + return list[list.__index = start || list.__index || 0]; + }, + + remain: function(list) { + return list.length - (list.__index || 0); + }, + + readList: function(list, start, options, amount) { + var res = [], + entry, + begin = start || 0, + end = amount ? begin + amount : list.length; + for (var i = begin; i < end; i++) { + res.push(Array.isArray(entry = list[i]) + ? this.read(entry, 0, options) + : this.read(list, i, options, 1)); + } + return res; + }, + + readNamed: function(list, name, start, options, amount) { + var value = this.getNamed(list, name), + hasObject = value !== undefined; + if (hasObject) { + var filtered = list.__filtered; + if (!filtered) { + filtered = list.__filtered = Base.create(list[0]); + filtered.__unfiltered = list[0]; + } + filtered[name] = undefined; + } + var l = hasObject ? [value] : list, + res = this.read(l, start, options, amount); + return res; + }, + + getNamed: function(list, name) { + var arg = list[0]; + if (list._hasObject === undefined) + list._hasObject = list.length === 1 && Base.isPlainObject(arg); + if (list._hasObject) + return name ? arg[name] : list.__filtered || arg; + }, + + hasNamed: function(list, name) { + return !!this.getNamed(list, name); + }, + + filter: function(dest, source, exclude, prioritize) { + var processed; + + function handleKey(key) { + if (!(exclude && key in exclude) && + !(processed && key in processed)) { + var value = source[key]; + if (value !== undefined) + dest[key] = value; + } + } + + if (prioritize) { + var keys = {}; + for (var i = 0, key, l = prioritize.length; i < l; i++) { + if ((key = prioritize[i]) in source) { + handleKey(key); + keys[key] = true; + } + } + processed = keys; + } + + Object.keys(source.__unfiltered || source).forEach(handleKey); + return dest; + }, + + isPlainValue: function(obj, asString) { + return Base.isPlainObject(obj) || Array.isArray(obj) + || asString && typeof obj === 'string'; + }, + + serialize: function(obj, options, compact, dictionary) { + options = options || {}; + + var isRoot = !dictionary, + res; + if (isRoot) { + options.formatter = new Formatter(options.precision); + dictionary = { + length: 0, + definitions: {}, + references: {}, + add: function(item, create) { + var id = '#' + item._id, + ref = this.references[id]; + if (!ref) { + this.length++; + var res = create.call(item), + name = item._class; + if (name && res[0] !== name) + res.unshift(name); + this.definitions[id] = res; + ref = this.references[id] = [id]; + } + return ref; + } + }; + } + if (obj && obj._serialize) { + res = obj._serialize(options, dictionary); + var name = obj._class; + if (name && !obj._compactSerialize && (isRoot || !compact) + && res[0] !== name) { + res.unshift(name); + } + } else if (Array.isArray(obj)) { + res = []; + for (var i = 0, l = obj.length; i < l; i++) + res[i] = Base.serialize(obj[i], options, compact, dictionary); + } else if (Base.isPlainObject(obj)) { + res = {}; + var keys = Object.keys(obj); + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + res[key] = Base.serialize(obj[key], options, compact, + dictionary); + } + } else if (typeof obj === 'number') { + res = options.formatter.number(obj, options.precision); + } else { + res = obj; + } + return isRoot && dictionary.length > 0 + ? [['dictionary', dictionary.definitions], res] + : res; + }, + + deserialize: function(json, create, _data, _setDictionary, _isRoot) { + var res = json, + isFirst = !_data, + hasDictionary = isFirst && json && json.length + && json[0][0] === 'dictionary'; + _data = _data || {}; + if (Array.isArray(json)) { + var type = json[0], + isDictionary = type === 'dictionary'; + if (json.length == 1 && /^#/.test(type)) { + return _data.dictionary[type]; + } + type = Base.exports[type]; + res = []; + for (var i = type ? 1 : 0, l = json.length; i < l; i++) { + res.push(Base.deserialize(json[i], create, _data, + isDictionary, hasDictionary)); + } + if (type) { + var args = res; + if (create) { + res = create(type, args, isFirst || _isRoot); + } else { + res = Base.create(type.prototype); + type.apply(res, args); + } + } + } else if (Base.isPlainObject(json)) { + res = {}; + if (_setDictionary) + _data.dictionary = res; + for (var key in json) + res[key] = Base.deserialize(json[key], create, _data); + } + return hasDictionary ? res[1] : res; + }, + + exportJSON: function(obj, options) { + var json = Base.serialize(obj, options); + return options && options.asString == false + ? json + : JSON.stringify(json); + }, + + importJSON: function(json, target) { + return Base.deserialize( + typeof json === 'string' ? JSON.parse(json) : json, + function(ctor, args, isRoot) { + var useTarget = isRoot && target + && target.constructor === ctor, + obj = useTarget ? target + : Base.create(ctor.prototype); + if (args.length === 1 && obj instanceof Item + && (useTarget || !(obj instanceof Layer))) { + var arg = args[0]; + if (Base.isPlainObject(arg)) + arg.insert = false; + } + (useTarget ? obj.set : ctor).apply(obj, args); + if (useTarget) + target = null; + return obj; + }); + }, + + splice: function(list, items, index, remove) { + var amount = items && items.length, + append = index === undefined; + index = append ? list.length : index; + if (index > list.length) + index = list.length; + for (var i = 0; i < amount; i++) + items[i]._index = index + i; + if (append) { + list.push.apply(list, items); + return []; + } else { + var args = [index, remove]; + if (items) + args.push.apply(args, items); + var removed = list.splice.apply(list, args); + for (var i = 0, l = removed.length; i < l; i++) + removed[i]._index = undefined; + for (var i = index + amount, l = list.length; i < l; i++) + list[i]._index = i; + return removed; + } + }, + + capitalize: function(str) { + return str.replace(/\b[a-z]/g, function(match) { + return match.toUpperCase(); + }); + }, + + camelize: function(str) { + return str.replace(/-(.)/g, function(match, chr) { + return chr.toUpperCase(); + }); + }, + + hyphenate: function(str) { + return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } -}); +}}); var Emitter = { on: function(type, func) { @@ -769,7 +778,7 @@ var PaperScope = Base.extend({ } }, - version: "0.11.2", + version: "0.11.3", getView: function() { var project = this.project; @@ -1739,6 +1748,9 @@ var Rectangle = Base.extend({ } this._set(x, y, width, height); read = arguments.__index; + var filtered = arguments.__filtered; + if (filtered) + this.__filtered = filtered; } if (this.__read) this.__read = read; @@ -2262,7 +2274,7 @@ var Matrix = Base.extend({ return this.shear(shear, center); }, - append: function(mx) { + append: function(mx, _dontNotify) { if (mx) { var a1 = this._a, b1 = this._b, @@ -2280,12 +2292,13 @@ var Matrix = Base.extend({ this._d = b2 * b1 + d2 * d1; this._tx += tx2 * a1 + ty2 * c1; this._ty += tx2 * b1 + ty2 * d1; - this._changed(); + if (!_dontNotify) + this._changed(); } return this; }, - prepend: function(mx) { + prepend: function(mx, _dontNotify) { if (mx) { var a1 = this._a, b1 = this._b, @@ -2305,7 +2318,8 @@ var Matrix = Base.extend({ this._d = c2 * c1 + d2 * d1; this._tx = a2 * tx1 + b2 * ty1 + tx2; this._ty = c2 * tx1 + d2 * ty1 + ty2; - this._changed(); + if (!_dontNotify) + this._changed(); } return this; }, @@ -3236,11 +3250,11 @@ new function() { this._boundsOptions); if (!opts.stroke || this.getStrokeScaling()) opts.cacheItem = this; - var bounds = this._getCachedBounds(hasMatrix && matrix, opts); + var rect = this._getCachedBounds(hasMatrix && matrix, opts).rect; return !arguments.length - ? new LinkedRectangle(bounds.x, bounds.y, bounds.width, - bounds.height, this, 'setBounds') - : bounds; + ? new LinkedRectangle(rect.x, rect.y, rect.width, rect.height, + this, 'setBounds') + : rect; }, setBounds: function() { @@ -3273,29 +3287,49 @@ new function() { return Item._getBounds(children, matrix, options); }, + _getBoundsCacheKey: function(options, internal) { + return [ + options.stroke ? 1 : 0, + options.handle ? 1 : 0, + internal ? 1 : 0 + ].join(''); + }, + _getCachedBounds: function(matrix, options, noInternal) { matrix = matrix && matrix._orNullIfIdentity(); var internal = options.internal && !noInternal, cacheItem = options.cacheItem, _matrix = internal ? null : this._matrix._orNullIfIdentity(), - cacheKey = cacheItem && (!matrix || matrix.equals(_matrix)) && [ - options.stroke ? 1 : 0, - options.handle ? 1 : 0, - internal ? 1 : 0 - ].join(''); + cacheKey = cacheItem && (!matrix || matrix.equals(_matrix)) + && this._getBoundsCacheKey(options, internal), + bounds = this._bounds; Item._updateBoundsCache(this._parent || this._symbol, cacheItem); - if (cacheKey && this._bounds && cacheKey in this._bounds) - return this._bounds[cacheKey].rect.clone(); - var bounds = this._getBounds(matrix || _matrix, options); + if (cacheKey && bounds && cacheKey in bounds) { + var cached = bounds[cacheKey]; + return { + rect: cached.rect.clone(), + nonscaling: cached.nonscaling + }; + } + var res = this._getBounds(matrix || _matrix, options), + rect = res.rect || res, + style = this._style, + nonscaling = res.nonscaling || style.hasStroke() + && !style.getStrokeScaling(); if (cacheKey) { - if (!this._bounds) - this._bounds = {}; - var cached = this._bounds[cacheKey] = { - rect: bounds.clone(), + if (!bounds) { + this._bounds = bounds = {}; + } + var cached = bounds[cacheKey] = { + rect: rect.clone(), + nonscaling: nonscaling, internal: internal }; } - return bounds; + return { + rect: rect, + nonscaling: nonscaling + }; }, _getStrokeMatrix: function(matrix, options) { @@ -3340,22 +3374,29 @@ new function() { var x1 = Infinity, x2 = -x1, y1 = x1, - y2 = x2; + y2 = x2, + nonscaling = false; options = options || {}; for (var i = 0, l = items.length; i < l; i++) { var item = items[i]; if (item._visible && !item.isEmpty()) { - var rect = item._getCachedBounds( - matrix && matrix.appended(item._matrix), options, true); + var bounds = item._getCachedBounds( + matrix && matrix.appended(item._matrix), options, true), + rect = bounds.rect; x1 = Math.min(rect.x, x1); y1 = Math.min(rect.y, y1); x2 = Math.max(rect.x + rect.width, x2); y2 = Math.max(rect.y + rect.height, y2); + if (bounds.nonscaling) + nonscaling = true; } } - return isFinite(x1) + return { + rect: isFinite(x1) ? new Rectangle(x1, y1, x2 - x1, y2 - y1) - : new Rectangle(); + : new Rectangle(), + nonscaling: nonscaling + }; } } @@ -3395,8 +3436,18 @@ new function() { var current = this.getScaling(), scaling = Point.read(arguments, 0, { clone: true, readNull: true }); if (current && scaling && !current.equals(scaling)) { - var decomposed = this._decomposed; - this.scale(scaling.x / current.x, scaling.y / current.y); + var rotation = this.getRotation(), + decomposed = this._decomposed, + matrix = new Matrix(), + center = this.getPosition(true); + matrix.translate(center); + if (rotation) + matrix.rotate(rotation); + matrix.scale(scaling.x / current.x, scaling.y / current.y); + if (rotation) + matrix.rotate(-rotation); + matrix.translate(center.negate()); + this.transform(matrix); if (decomposed) { decomposed.scaling = scaling; this._decomposed = decomposed; @@ -4218,8 +4269,6 @@ new function() { transform: function(matrix, _applyMatrix, _applyRecursively, _setApplyMatrix) { - if (matrix && matrix.isIdentity()) - matrix = null; var _matrix = this._matrix, transform = matrix && !matrix.isIdentity(), applyMatrix = (_applyMatrix || this._applyMatrix) @@ -4230,22 +4279,7 @@ new function() { if (transform) { if (!matrix.isInvertible() && _matrix.isInvertible()) _matrix._backup = _matrix.getValues(); - _matrix.prepend(matrix); - } - if (applyMatrix) { - if (this._transformContent(_matrix, _applyRecursively, - _setApplyMatrix)) { - var pivot = this._pivot; - if (pivot) - _matrix._transformPoint(pivot, pivot, true); - _matrix.reset(true); - if (_setApplyMatrix && this._canApplyMatrix) - this._applyMatrix = true; - } else { - applyMatrix = transform = false; - } - } - if (transform) { + _matrix.prepend(matrix, true); var style = this._style, fillColor = style.getFillColor(true), strokeColor = style.getStrokeColor(true); @@ -4254,24 +4288,38 @@ new function() { if (strokeColor) strokeColor.transform(matrix); } + if (applyMatrix && (applyMatrix = this._transformContent(_matrix, + _applyRecursively, _setApplyMatrix))) { + var pivot = this._pivot; + if (pivot) + _matrix._transformPoint(pivot, pivot, true); + _matrix.reset(true); + if (_setApplyMatrix && this._canApplyMatrix) + this._applyMatrix = true; + } var bounds = this._bounds, position = this._position; - this._changed(9); - var decomp = bounds && matrix && matrix.decompose(); - if (decomp && !decomp.shearing && decomp.rotation % 90 === 0) { + if (transform || applyMatrix) { + this._changed(9); + } + var decomp = transform && bounds && matrix.decompose(); + if (decomp && decomp.skewing.isZero() && decomp.rotation % 90 === 0) { for (var key in bounds) { var cache = bounds[key]; - if (applyMatrix || !cache.internal) { + if (cache.nonscaling) { + delete bounds[key]; + } else if (applyMatrix || !cache.internal) { var rect = cache.rect; matrix._transformBounds(rect, rect); } } - var getter = this._boundsGetter, - rect = bounds[getter && getter.getBounds || getter || 'getBounds']; - if (rect) - this._position = rect.getCenter(true); this._bounds = bounds; - } else if (matrix && position) { + var cached = bounds[this._getBoundsCacheKey( + this._boundsOptions || {})]; + if (cached) { + this._position = cached.rect.getCenter(true); + } + } else if (transform && position && this._pivot) { this._position = matrix._transformPoint(position, position); } return this; @@ -5483,10 +5531,8 @@ var HitResult = Base.extend({ initialize: function HitResult(type, item, values) { this.type = type; this.item = item; - if (values) { - values.enumerable = true; + if (values) this.inject(values); - } }, statics: { @@ -7006,7 +7052,7 @@ new function() { cos = Math.cos(angle), rv = [], roots = []; - for(var i = 0; i < 8; i += 2) { + for (var i = 0; i < 8; i += 2) { var x = v[i] - px, y = v[i + 1] - py; rv.push( @@ -11016,10 +11062,10 @@ var PointText = TextItem.extend({ x = 0; if (justification !== 'left') x -= width / (justification === 'center' ? 2: 1); - var bounds = new Rectangle(x, + var rect = new Rectangle(x, numLines ? - 0.75 * leading : 0, width, numLines * leading); - return matrix ? matrix._transformBounds(bounds, bounds) : bounds; + return matrix ? matrix._transformBounds(rect, rect) : rect; } }); @@ -12694,8 +12740,8 @@ new function() { point, prevPoint) || hitItem && hitItem !== dragItem && !hitItem.isDescendant(dragItem) - && emitMouseEvent(hitItem, null, fallbacks[type] || type, event, - point, prevPoint, dragItem) + && emitMouseEvent(hitItem, null, type, event, point, prevPoint, + dragItem) || emitMouseEvent(view, dragItem || hitItem || view, type, event, point, prevPoint)); } @@ -13807,9 +13853,9 @@ new function() { if (!Numerical.isZero(scale.x - 1) || !Numerical.isZero(scale.y - 1)) parts.push('scale(' + formatter.point(scale) +')'); - if (skew && skew.x) + if (skew.x) parts.push('skewX(' + formatter.number(skew.x) + ')'); - if (skew && skew.y) + if (skew.y) parts.push('skewY(' + formatter.number(skew.y) + ')'); attrs.transform = parts.join(' '); } else { @@ -13870,8 +13916,9 @@ new function() { if (length > 2) { type = item._closed ? 'polygon' : 'polyline'; var parts = []; - for(var i = 0; i < length; i++) + for (var i = 0; i < length; i++) { parts.push(formatter.point(segments[i]._point)); + } attrs.points = parts.join(' '); } else { type = 'line'; @@ -14142,6 +14189,7 @@ new function() { ? new Rectangle([0, 0], view.getViewSize()) : bounds === 'content' ? Item._getBounds(children, matrix, { stroke: true }) + .rect : Rectangle.read([bounds], 0, { readNull: true }), attrs = { version: '1.1', @@ -16413,7 +16461,6 @@ Base.exports.PaperScript = function() { }.call(this); paper = new (PaperScope.inject(Base.exports, { - enumerable: true, Base: Base, Numerical: Numerical, Key: Key, diff --git a/examples/JSON/Transform Test 1.html b/examples/JSON/Transform Test 1.html index feea0a62..9f2cb6ee 100644 --- a/examples/JSON/Transform Test 1.html +++ b/examples/JSON/Transform Test 1.html @@ -13,7 +13,7 @@ var clones = 30; var angle = 360 / clones; - for(var i = 0; i < clones; i++) { + for (var i = 0; i < clones; i++) { var clonedPath = circlePath.clone(); clonedPath.rotate(angle * i, circlePath.bounds.topLeft); }; diff --git a/examples/Paperjs.org/SatieLikedToDraw.html b/examples/Paperjs.org/SatieLikedToDraw.html index 9c3e2850..14dfb65b 100644 --- a/examples/Paperjs.org/SatieLikedToDraw.html +++ b/examples/Paperjs.org/SatieLikedToDraw.html @@ -120,7 +120,7 @@ function getEqualizerBands(data) { var bands = []; var amount = Math.sqrt(data.length) / 2; - for(var i = 0; i < amount; i++) { + for (var i = 0; i < amount; i++) { var start = Math.pow(2, i) - 1; var end = start * 2 + 1; var sum = 0; diff --git a/examples/Paperjs.org/Voronoi.html b/examples/Paperjs.org/Voronoi.html index d556d663..5e4ef552 100644 --- a/examples/Paperjs.org/Voronoi.html +++ b/examples/Paperjs.org/Voronoi.html @@ -55,7 +55,7 @@ function removeSmallBits(path) { var averageLength = path.length / path.segments.length; var min = path.length / 50; - for(var i = path.segments.length - 1; i >= 0; i--) { + for (var i = path.segments.length - 1; i >= 0; i--) { var segment = path.segments[i]; var cur = segment.point; var nextSegment = segment.next; @@ -69,8 +69,8 @@ function generateBeeHivePoints(size, loose) { var points = []; var col = view.size / size; - for(var i = -1; i < size.width + 1; i++) { - for(var j = -1; j < size.height + 1; j++) { + for (var i = -1; i < size.width + 1; i++) { + for (var j = -1; j < size.height + 1; j++) { var point = new Point(i, j) / new Point(size) * view.size + col / 2; if (j % 2) point += new Point(col.width / 2, 0); diff --git a/examples/SVG Export/Transform Test 1.html b/examples/SVG Export/Transform Test 1.html index 3a96ca67..def8966f 100644 --- a/examples/SVG Export/Transform Test 1.html +++ b/examples/SVG Export/Transform Test 1.html @@ -15,7 +15,7 @@ var clones = 30; var angle = 360 / clones; - for(var i = 0; i < clones; i++) { + for (var i = 0; i < clones; i++) { var clonedPath = circlePath.clone(); clonedPath.rotate(angle * i, circlePath.bounds.topLeft); }; diff --git a/gulp/tasks/publish.js b/gulp/tasks/publish.js index 3f154a36..621727de 100644 --- a/gulp/tasks/publish.js +++ b/gulp/tasks/publish.js @@ -36,13 +36,15 @@ gulp.task('publish', function() { if (options.branch !== 'develop') { throw new Error('Publishing is only allowed on the develop branch.'); } + // publish:website comes before publish:release, so paperjs.zip file is gone + // before npm publish: return run( 'publish:json', 'publish:dist', 'publish:packages', 'publish:commit', - 'publish:release', 'publish:website', + 'publish:release', 'publish:load' ); }); @@ -115,8 +117,18 @@ gulp.task('publish:website', function() { } }); -gulp.task('publish:website:build', - ['publish:website:docs', 'publish:website:zip', 'publish:website:lib']); +gulp.task('publish:website:build', [ + 'publish:website:json', 'publish:website:docs', + 'publish:website:zip', 'publish:website:assets' +]); + +gulp.task('publish:website:json', ['publish:version'], function() { + return gulp.src([sitePath + '/package.json']) + .pipe(jsonEditor({ + version: options.version + }, jsonOptions)) + .pipe(gulp.dest(sitePath)); +}); gulp.task('publish:website:docs:clean', function() { return del([ referencePath + '/*' ], { force: true }); @@ -135,7 +147,10 @@ gulp.task('publish:website:zip', ['publish:version'], function() { .pipe(gulp.dest(downloadPath)); }); -gulp.task('publish:website:lib', ['publish:version'], function() { +gulp.task('publish:website:assets', function() { + // Always delete the old asset first, in case it's a symlink which Gulp + // doesn't handle well. + fs.unlinkSync(assetPath + '/paper.js'); return gulp.src('dist/paper-full.js') .pipe(rename({ basename: 'paper' })) .pipe(gulp.dest(assetPath)); diff --git a/package.json b/package.json index 5940006d..4c5f5781 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "paper", - "version": "0.11.2", + "version": "0.11.3", "description": "The Swiss Army Knife of Vector Graphics Scripting", "license": "MIT", "homepage": "http://paperjs.org", "repository": { "type": "git", - "url": "git://github.com/paperjs/paper.js" + "url": "https://github.com/paperjs/paper.js" }, "bugs": "https://github.com/paperjs/paper.js/issues", "contributors": [ @@ -69,7 +69,7 @@ "run-sequence": "^1.2.2", "source-map-support": "^0.4.0", "stats.js": "0.16.0", - "straps": "^2.1.0" + "straps": "^3.0.1" }, "browser": { "canvas": false, diff --git a/packages/paper-jsdom b/packages/paper-jsdom index bab27f25..fc7ac578 160000 --- a/packages/paper-jsdom +++ b/packages/paper-jsdom @@ -1 +1 @@ -Subproject commit bab27f25fed8d78f072d8f9a9f68da61e7d1e975 +Subproject commit fc7ac57828aefadff29f7559a5e39f88d35b0c66 diff --git a/packages/paper-jsdom-canvas b/packages/paper-jsdom-canvas index 2e257a43..18feab4d 160000 --- a/packages/paper-jsdom-canvas +++ b/packages/paper-jsdom-canvas @@ -1 +1 @@ -Subproject commit 2e257a436e1cfec74ca6ffe4828a761ec058b42f +Subproject commit 18feab4d8968339c60d8610584ab3574c49d7a91 diff --git a/src/basic/Matrix.js b/src/basic/Matrix.js index 90d8f0ff..7bf833cf 100644 --- a/src/basic/Matrix.js +++ b/src/basic/Matrix.js @@ -377,7 +377,7 @@ var Matrix = Base.extend(/** @lends Matrix# */{ * @param {Matrix} matrix the matrix to append * @return {Matrix} this matrix, modified */ - append: function(mx) { + append: function(mx, _dontNotify) { if (mx) { var a1 = this._a, b1 = this._b, @@ -395,7 +395,8 @@ var Matrix = Base.extend(/** @lends Matrix# */{ this._d = b2 * b1 + d2 * d1; this._tx += tx2 * a1 + ty2 * c1; this._ty += tx2 * b1 + ty2 * d1; - this._changed(); + if (!_dontNotify) + this._changed(); } return this; }, @@ -407,7 +408,7 @@ var Matrix = Base.extend(/** @lends Matrix# */{ * @param {Matrix} matrix the matrix to prepend * @return {Matrix} this matrix, modified */ - prepend: function(mx) { + prepend: function(mx, _dontNotify) { if (mx) { var a1 = this._a, b1 = this._b, @@ -427,7 +428,8 @@ var Matrix = Base.extend(/** @lends Matrix# */{ this._d = c2 * c1 + d2 * d1; this._tx = a2 * tx1 + b2 * ty1 + tx2; this._ty = c2 * tx1 + d2 * ty1 + ty2; - this._changed(); + if (!_dontNotify) + this._changed(); } return this; }, @@ -671,7 +673,7 @@ var Matrix = Base.extend(/** @lends Matrix# */{ /** * Attempts to decompose the affine transformation described by this matrix - * into `scaling`, `rotation` and `shearing`, and returns an object with + * into `scaling`, `rotation` and `skewing`, and returns an object with * these properties if it succeeded, `null` otherwise. * * @return {Object} the decomposed matrix, or `null` if decomposition is not diff --git a/src/basic/Rectangle.js b/src/basic/Rectangle.js index c97ce863..7e86fdfc 100644 --- a/src/basic/Rectangle.js +++ b/src/basic/Rectangle.js @@ -141,6 +141,12 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{ } this._set(x, y, width, height); read = arguments.__index; + // arguments.__filtered wouldn't survive the function call even if a + // previous arguments list was passed through Function#apply(). + // Return it on the object instead, see Base.read() + var filtered = arguments.__filtered; + if (filtered) + this.__filtered = filtered; } if (this.__read) this.__read = read; diff --git a/src/core/Base.js b/src/core/Base.js index e3a86f6b..51837bcd 100644 --- a/src/core/Base.js +++ b/src/core/Base.js @@ -17,6 +17,8 @@ */ // Extend Base with utility functions used across the library. Base.inject(/** @lends Base# */{ + enumerable: false, + /** * Renders base objects to strings in object literal notation. */ @@ -92,537 +94,541 @@ Base.inject(/** @lends Base# */{ if (props) Base.filter(this, props, exclude, this._prioritize); return this; + } +}, /** @lends Base# */{ +// Mess with indentation in order to get more line-space for the statics below. +// Explicitly deactivate the creation of beans, as we have functions here +// that look like bean getters but actually read arguments, see getNamed(). +beans: false, +statics: /** @lends Base */{ + // Keep track of all named classes for serialization and exporting. + exports: {}, + + extend: function extend() { + // Override Base.extend() to register named classes in Base.exports, + // for deserialization and injection into PaperScope. + var res = extend.base.apply(this, arguments), + name = res.prototype._class; + if (name && !Base.exports[name]) + Base.exports[name] = res; + return res; }, - statics: /** @lends Base */{ - - // Keep track of all named classes for serialization and exporting. - exports: { - enumerable: true // For PaperScope.inject() in export.js - }, - - extend: function extend() { - // Override Base.extend() to register named classes in Base.exports, - // for deserialization and injection into PaperScope. - var res = extend.base.apply(this, arguments), - name = res.prototype._class; - if (name && !Base.exports[name]) - Base.exports[name] = res; - return res; - }, - - /** - * Checks if two values or objects are equals to each other, by using - * their equals() methods if available, and also comparing elements of - * arrays and properties of objects. - */ - equals: function(obj1, obj2) { - if (obj1 === obj2) - return true; - // Call #equals() on both obj1 and obj2 - if (obj1 && obj1.equals) - return obj1.equals(obj2); - if (obj2 && obj2.equals) - return obj2.equals(obj1); - // Deep compare objects or arrays - if (obj1 && obj2 - && typeof obj1 === 'object' && typeof obj2 === 'object') { - // Compare arrays - if (Array.isArray(obj1) && Array.isArray(obj2)) { - var length = obj1.length; - if (length !== obj2.length) + /** + * Checks if two values or objects are equals to each other, by using their + * equals() methods if available, and also comparing elements of arrays and + * properties of objects. + */ + equals: function(obj1, obj2) { + if (obj1 === obj2) + return true; + // Call #equals() on both obj1 and obj2 + if (obj1 && obj1.equals) + return obj1.equals(obj2); + if (obj2 && obj2.equals) + return obj2.equals(obj1); + // Deep compare objects or arrays + if (obj1 && obj2 + && typeof obj1 === 'object' && typeof obj2 === 'object') { + // Compare arrays + if (Array.isArray(obj1) && Array.isArray(obj2)) { + var length = obj1.length; + if (length !== obj2.length) + return false; + while (length--) { + if (!Base.equals(obj1[length], obj2[length])) return false; - while (length--) { - if (!Base.equals(obj1[length], obj2[length])) - return false; - } - } else { - // Deep compare objects. - var keys = Object.keys(obj1), - length = keys.length; - // Ensure that both objects contain the same number of - // properties before comparing deep equality. - if (length !== Object.keys(obj2).length) + } + } else { + // Deep compare objects. + var keys = Object.keys(obj1), + length = keys.length; + // Ensure that both objects contain the same number of + // properties before comparing deep equality. + if (length !== Object.keys(obj2).length) + return false; + while (length--) { + // Deep compare each member + var key = keys[length]; + if (!(obj2.hasOwnProperty(key) + && Base.equals(obj1[key], obj2[key]))) return false; - while (length--) { - // Deep compare each member - var key = keys[length]; - if (!(obj2.hasOwnProperty(key) - && Base.equals(obj1[key], obj2[key]))) - return false; - } - } - return true; - } - return false; - }, - - /** - * When called on a subclass of Base, it reads arguments of the type of - * the subclass from the passed arguments list or array, at the given - * index, up to the specified length. - * When called directly on Base, it reads any value without conversion - * from the passed arguments list or array. - * This is used in argument conversion, e.g. by all basic types (Point, - * Size, Rectangle) and also higher classes such as Color and Segment. - * - * @param {Array} list the list to read from, either an arguments object - * or a normal array - * @param {Number} start the index at which to start reading in the list - * @param {Object} options `options.readNull` controls whether null is - * returned or converted. `options.clone` controls whether passed - * objects should be cloned if they are already provided in the - * required type - * @param {Number} length the amount of elements that can be read - */ - read: function(list, start, options, amount) { - // See if it's called directly on Base, and if so, read value and - // return without object conversion. - if (this === Base) { - var value = this.peek(list, start); - list.__index++; - return value; - } - var proto = this.prototype, - readIndex = proto._readIndex, - begin = start || readIndex && list.__index || 0, - length = list.length, - obj = list[begin]; - amount = amount || length - begin; - // When read() is called on a sub-class of which the object is - // already an instance, or when there is only one value in the list - // and it's null or undefined, return the obj. - if (obj instanceof this - || options && options.readNull && obj == null && amount <= 1) { - if (readIndex) - list.__index = begin + 1; - return obj && options && options.clone ? obj.clone() : obj; - } - // Otherwise, create a new object and read through its initialize - // function. - obj = Base.create(proto); - if (readIndex) - obj.__read = true; - obj = obj.initialize.apply(obj, begin > 0 || begin + amount < length - ? Base.slice(list, begin, begin + amount) - : list) || obj; - if (readIndex) { - list.__index = begin + obj.__read; - obj.__read = undefined; - } - return obj; - }, - - /** - * Allows peeking ahead in reading of values and objects from arguments - * list through Base.read(). - * - * @param {Array} list the list to read from, either an arguments object - * or a normal array - * @param {Number} start the index at which to start reading in the list - */ - peek: function(list, start) { - return list[list.__index = start || list.__index || 0]; - }, - - /** - * Returns how many arguments remain to be read in the argument list. - */ - remain: function(list) { - return list.length - (list.__index || 0); - }, - - /** - * Reads all readable arguments from the list, handling nested arrays - * separately. - * - * @param {Array} list the list to read from, either an arguments object - * or a normal array - * @param {Number} start the index at which to start reading in the list - * @param {Object} options `options.readNull` controls whether null is - * returned or converted. `options.clone` controls whether passed - * objects should be cloned if they are already provided in the - * required type - * @param {Number} amount the amount of elements that should be read - */ - readList: function(list, start, options, amount) { - var res = [], - entry, - begin = start || 0, - end = amount ? begin + amount : list.length; - for (var i = begin; i < end; i++) { - res.push(Array.isArray(entry = list[i]) - ? this.read(entry, 0, options) - : this.read(list, i, options, 1)); - } - return res; - }, - - /** - * Allows using of Base.read() mechanism in combination with reading - * named arguments form a passed property object literal. Calling - * Base.readNamed() can read both from such named properties and normal - * unnamed arguments through Base.read(). In use for example for the - * various Path.Constructors. - * - * @param {Array} list the list to read from, either an arguments object - * or a normal array - * @param {String} name the property name to read from - * @param {Number} start the index at which to start reading in the list - * @param {Object} options `options.readNull` controls whether null is - * returned or converted. `options.clone` controls whether passed - * objects should be cloned if they are already provided in the - * required type - * @param {Number} amount the amount of elements that can be read - */ - readNamed: function(list, name, start, options, amount) { - var value = this.getNamed(list, name), - hasObject = value !== undefined; - if (hasObject) { - // Create a _filtered object that inherits from list[0], and - // override all fields that were already read with undefined. - var filtered = list._filtered; - if (!filtered) { - filtered = list._filtered = Base.create(list[0]); - // Point _unfiltered to the original so Base#_set() can - // execute hasOwnProperty on it. - filtered._unfiltered = list[0]; - } - // delete wouldn't work since the masked parent's value would - // shine through. - filtered[name] = undefined; - } - return this.read(hasObject ? [value] : list, start, options, amount); - }, - - /** - * @return the named value if the list provides an arguments object, - * `null` if the named value is `null` or `undefined`, and - * `undefined` if there is no arguments object If no name is - * provided, it returns the whole arguments object - */ - getNamed: function(list, name) { - var arg = list[0]; - if (list._hasObject === undefined) - list._hasObject = list.length === 1 && Base.isPlainObject(arg); - if (list._hasObject) - // Return the whole arguments object if no name is provided. - return name ? arg[name] : list._filtered || arg; - }, - - /** - * Checks if the argument list has a named argument with the given name. - * If name is `null`, it returns `true` if there are any named - * arguments. - */ - hasNamed: function(list, name) { - return !!this.getNamed(list, name); - }, - - /** - * Copies all properties from `source` over to `dest`, supporting - * `_filtered` handling as required by {@link Base.readNamed()} - * mechanism, as well as a way to exclude and prioritize properties. - * - * @param {Object} dest the destination that is to receive the - * properties - * @param {Object} source the source from where to retrieve the - * properties to be copied - * @param {Object} [exclude] an object that can define any properties - * as `true` that should be excluded when copying - * @param {String[]} [prioritize] a list of keys that should be - * prioritized when copying, if they are defined in `source`, - * processed in the order of appearance - */ - filter: function(dest, source, exclude, prioritize) { - var processed; - - function handleKey(key) { - if (!(exclude && key in exclude) && - !(processed && key in processed)) { - // Due to the _filtered inheritance trick, undefined is used - // to mask already consumed named arguments. - var value = source[key]; - if (value !== undefined) - dest[key] = value; } } - - // If there are prioritized keys, process them first. - if (prioritize) { - var keys = {}; - for (var i = 0, key, l = prioritize.length; i < l; i++) { - if ((key = prioritize[i]) in source) { - handleKey(key); - keys[key] = true; - } - } - // Now reference the processed keys as processed, so that - // handleKey() will not set them again below. - processed = keys; - } - - // If source is a filtered object, we get the keys from the - // the original object (it's parent / prototype). See _filtered - // inheritance trick in the argument reading code. - Object.keys(source._unfiltered || source).forEach(handleKey); - return dest; - }, - - /** - * Returns true if obj is either a plain object or an array, as used by - * many argument reading methods. - */ - isPlainValue: function(obj, asString) { - return Base.isPlainObject(obj) || Array.isArray(obj) - || asString && typeof obj === 'string'; - }, - - /** - * Serializes the passed object into a format that can be passed to - * JSON.stringify() for JSON serialization. - */ - serialize: function(obj, options, compact, dictionary) { - options = options || {}; - - var isRoot = !dictionary, - res; - if (isRoot) { - options.formatter = new Formatter(options.precision); - // Create a simple dictionary object that handles all the - // storing and retrieving of dictionary definitions and - // references, e.g. for symbols and gradients. Items that want - // to support this need to define globally unique _id attribute. - /** - * @namespace - * @private - */ - dictionary = { - length: 0, - definitions: {}, - references: {}, - add: function(item, create) { - // See if we have reference entry with the given id - // already. If not, call create on the item to allow it - // to create the definition, then store the reference - // to it and return it. - var id = '#' + item._id, - ref = this.references[id]; - if (!ref) { - this.length++; - var res = create.call(item), - name = item._class; - // Also automatically insert class for dictionary - // entries. - if (name && res[0] !== name) - res.unshift(name); - this.definitions[id] = res; - ref = this.references[id] = [id]; - } - return ref; - } - }; - } - if (obj && obj._serialize) { - res = obj._serialize(options, dictionary); - // If we don't serialize to compact form (meaning no type - // identifier), see if _serialize didn't already add the class, - // e.g. for classes that do not support compact form. - var name = obj._class; - // Enforce class names on root level, except if the class - // explicitly asks to be serialized in compact form (Project). - if (name && !obj._compactSerialize && (isRoot || !compact) - && res[0] !== name) { - res.unshift(name); - } - } else if (Array.isArray(obj)) { - res = []; - for (var i = 0, l = obj.length; i < l; i++) - res[i] = Base.serialize(obj[i], options, compact, - dictionary); - } else if (Base.isPlainObject(obj)) { - res = {}; - var keys = Object.keys(obj); - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - res[key] = Base.serialize(obj[key], options, compact, - dictionary); - } - } else if (typeof obj === 'number') { - res = options.formatter.number(obj, options.precision); - } else { - res = obj; - } - return isRoot && dictionary.length > 0 - ? [['dictionary', dictionary.definitions], res] - : res; - }, - - /** - * Deserializes from parsed JSON data. A simple convention is followed: - * Array values with a string at the first position are links to - * deserializable types through Base.exports, and the values following - * in the array are the arguments to their initialize function. - * Any other value is passed on unmodified. - * The passed json data is recoursively traversed and converted, leaves - * first - */ - deserialize: function(json, create, _data, _setDictionary, _isRoot) { - var res = json, - isFirst = !_data, - hasDictionary = isFirst && json && json.length - && json[0][0] === 'dictionary'; - // A _data side-car to deserialize that can hold any kind of - // 'global' data across a deserialization. It's currently only used - // to hold dictionary definitions. - _data = _data || {}; - if (Array.isArray(json)) { - // See if it's a serialized type. If so, the rest of the array - // are the arguments to #initialize(). Either way, we simply - // deserialize all elements of the array. - var type = json[0], - // Handle stored dictionary specially, since we need to - // keep a lookup table to retrieve referenced items from. - isDictionary = type === 'dictionary'; - // First see if this is perhaps a dictionary reference, and - // if so return its definition instead. - if (json.length == 1 && /^#/.test(type)) { - return _data.dictionary[type]; - } - type = Base.exports[type]; - res = []; - // Skip first type entry for arguments - // Pass true for _isRoot in children if we have a dictionary, - // in which case we need to shift the root level one down. - for (var i = type ? 1 : 0, l = json.length; i < l; i++) { - res.push(Base.deserialize(json[i], create, _data, - isDictionary, hasDictionary)); - } - if (type) { - // Create serialized type and pass collected arguments to - // constructor(). - var args = res; - // If a create method is provided, handle our own - // creation. This is used in #importJSON() to pass - // on insert = false to all items except layers. - if (create) { - res = create(type, args, isFirst || _isRoot); - } else { - res = Base.create(type.prototype); - type.apply(res, args); - } - } - } else if (Base.isPlainObject(json)) { - res = {}; - // We need to set the dictionary object before further - // deserialization, because serialized symbols may contain - // references to serialized gradients - if (_setDictionary) - _data.dictionary = res; - for (var key in json) - res[key] = Base.deserialize(json[key], create, _data); - } - // Filter out deserialized dictionary: - return hasDictionary ? res[1] : res; - }, - - exportJSON: function(obj, options) { - var json = Base.serialize(obj, options); - return options && options.asString == false - ? json - : JSON.stringify(json); - }, - - importJSON: function(json, target) { - return Base.deserialize( - typeof json === 'string' ? JSON.parse(json) : json, - // Provide our own create function to handle target and - // insertion. - function(ctor, args, isRoot) { - // If a target is provided and its of the right type - // for the root item, import right into it. - var useTarget = isRoot && target - && target.constructor === ctor, - obj = useTarget ? target - : Base.create(ctor.prototype); - // NOTE: We don't set insert false for layers since we - // want these to be created on the fly in the active - // project into which we're importing (except for if - // it's a preexisting target layer). - if (args.length === 1 && obj instanceof Item - && (useTarget || !(obj instanceof Layer))) { - var arg = args[0]; - if (Base.isPlainObject(arg)) - arg.insert = false; - } - // When reusing an object, initialize it through #set() - // instead of the constructor function: - (useTarget ? obj.set : ctor).apply(obj, args); - // Clear target to only use it once. - if (useTarget) - target = null; - return obj; - }); - }, - - /** - * Utility function for adding and removing items from a list of which - * each entry keeps a reference to its index in the list in the private - * _index property. Used for PaperScope#projects and Item#children. - */ - splice: function(list, items, index, remove) { - var amount = items && items.length, - append = index === undefined; - index = append ? list.length : index; - if (index > list.length) - index = list.length; - // Update _index on the items to be added first. - for (var i = 0; i < amount; i++) - items[i]._index = index + i; - if (append) { - // Append them all at the end by using push - list.push.apply(list, items); - // Nothing removed, and nothing to adjust above - return []; - } else { - // Insert somewhere else and/or remove - var args = [index, remove]; - if (items) - args.push.apply(args, items); - var removed = list.splice.apply(list, args); - // Erase the indices of the removed items - for (var i = 0, l = removed.length; i < l; i++) - removed[i]._index = undefined; - // Adjust the indices of the items above. - for (var i = index + amount, l = list.length; i < l; i++) - list[i]._index = i; - return removed; - } - }, - - /** - * Capitalizes the passed string: hello world -> Hello World - */ - capitalize: function(str) { - return str.replace(/\b[a-z]/g, function(match) { - return match.toUpperCase(); - }); - }, - - /** - * Camelizes the passed hyphenated string: caps-lock -> capsLock - */ - camelize: function(str) { - return str.replace(/-(.)/g, function(match, chr) { - return chr.toUpperCase(); - }); - }, - - /** - * Converts camelized strings to hyphenated ones: CapsLock -> caps-lock - */ - hyphenate: function(str) { - return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + return true; } + return false; + }, + + /** + * When called on a subclass of Base, it reads arguments of the type of the + * subclass from the passed arguments list or array, at the given index, up + * to the specified length. When called directly on Base, it reads any value + * without conversion from the passed arguments list or array. This is used + * in argument conversion, e.g. by all basic types (Point, Size, Rectangle) + * and also higher classes such as Color and Segment. + * + * @param {Array} list the list to read from, either an arguments object or + * a normal array + * @param {Number} start the index at which to start reading in the list + * @param {Object} options `options.readNull` controls whether null is + * returned or converted. `options.clone` controls whether passed + * objects should be cloned if they are already provided in the required + * type + * @param {Number} length the amount of elements that can be read + */ + read: function(list, start, options, amount) { + // See if it's called directly on Base, and if so, read value and return + // without object conversion. + if (this === Base) { + var value = this.peek(list, start); + list.__index++; + return value; + } + var proto = this.prototype, + readIndex = proto._readIndex, + begin = start || readIndex && list.__index || 0, + length = list.length, + obj = list[begin]; + amount = amount || length - begin; + // When read() is called on a sub-class of which the object is already + // an instance, or when there is only one value in the list and it's + // null or undefined, return the obj. + if (obj instanceof this + || options && options.readNull && obj == null && amount <= 1) { + if (readIndex) + list.__index = begin + 1; + return obj && options && options.clone ? obj.clone() : obj; + } + // Otherwise, create a new object and read through its initialize + // function. + obj = Base.create(proto); + if (readIndex) + obj.__read = true; + obj = obj.initialize.apply(obj, begin > 0 || begin + amount < length + ? Base.slice(list, begin, begin + amount) + : list) || obj; + if (readIndex) { + list.__index = begin + obj.__read; + // This is only in use in Rectangle so far: Nested calls to + // `Base.readNamed()` would loose __filtered if it wasn't returned + // on the object. + var filtered = obj.__filtered; + if (filtered) { + list.__filtered = filtered; + obj.__filtered = undefined; + } + obj.__read = undefined; + } + return obj; + }, + + /** + * Allows peeking ahead in reading of values and objects from arguments + * list through Base.read(). + * + * @param {Array} list the list to read from, either an arguments object + * or a normal array + * @param {Number} start the index at which to start reading in the list + */ + peek: function(list, start) { + return list[list.__index = start || list.__index || 0]; + }, + + /** + * Returns how many arguments remain to be read in the argument list. + */ + remain: function(list) { + return list.length - (list.__index || 0); + }, + + /** + * Reads all readable arguments from the list, handling nested arrays + * separately. + * + * @param {Array} list the list to read from, either an arguments object + * or a normal array + * @param {Number} start the index at which to start reading in the list + * @param {Object} options `options.readNull` controls whether null is + * returned or converted. `options.clone` controls whether passed + * objects should be cloned if they are already provided in the + * required type + * @param {Number} amount the amount of elements that should be read + */ + readList: function(list, start, options, amount) { + var res = [], + entry, + begin = start || 0, + end = amount ? begin + amount : list.length; + for (var i = begin; i < end; i++) { + res.push(Array.isArray(entry = list[i]) + ? this.read(entry, 0, options) + : this.read(list, i, options, 1)); + } + return res; + }, + + /** + * Allows using of Base.read() mechanism in combination with reading named + * arguments form a passed property object literal. Calling Base.readNamed() + * can read both from such named properties and normal unnamed arguments + * through Base.read(). In use for example for the various + * Path.Constructors. + * + * @param {Array} list the list to read from, either an arguments object or + * a normal array + * @param {String} name the property name to read from + * @param {Number} start the index at which to start reading in the list + * @param {Object} options `options.readNull` controls whether null is + * returned or converted. `options.clone` controls whether passed + * objects should be cloned if they are already provided in the required + * type + * @param {Number} amount the amount of elements that can be read + */ + readNamed: function(list, name, start, options, amount) { + var value = this.getNamed(list, name), + hasObject = value !== undefined; + if (hasObject) { + // Create a _filtered object that inherits from list[0], and + // override all fields that were already read with undefined. + var filtered = list.__filtered; + if (!filtered) { + filtered = list.__filtered = Base.create(list[0]); + // Point _unfiltered to the original so Base#_set() can + // execute hasOwnProperty on it. + filtered.__unfiltered = list[0]; + } + // delete wouldn't work since the masked parent's value would + // shine through. + filtered[name] = undefined; + } + var l = hasObject ? [value] : list, + res = this.read(l, start, options, amount); + return res; + }, + + /** + * @return the named value if the list provides an arguments object, + * `null` if the named value is `null` or `undefined`, and + * `undefined` if there is no arguments object If no name is + * provided, it returns the whole arguments object + */ + getNamed: function(list, name) { + var arg = list[0]; + if (list._hasObject === undefined) + list._hasObject = list.length === 1 && Base.isPlainObject(arg); + if (list._hasObject) + // Return the whole arguments object if no name is provided. + return name ? arg[name] : list.__filtered || arg; + }, + + /** + * Checks if the argument list has a named argument with the given name. If + * name is `null`, it returns `true` if there are any named arguments. + */ + hasNamed: function(list, name) { + return !!this.getNamed(list, name); + }, + + /** + * Copies all properties from `source` over to `dest`, supporting + * `_filtered` handling as required by {@link Base.readNamed()} mechanism, + * as well as a way to exclude and prioritize properties. + * + * @param {Object} dest the destination that is to receive the properties + * @param {Object} source the source from where to retrieve the properties + * to be copied + * @param {Object} [exclude] an object that can define any properties as + * `true` that should be excluded when copying + * @param {String[]} [prioritize] a list of keys that should be prioritized + * when copying, if they are defined in `source`, processed in the order + * of appearance + */ + filter: function(dest, source, exclude, prioritize) { + var processed; + + function handleKey(key) { + if (!(exclude && key in exclude) && + !(processed && key in processed)) { + // Due to the _filtered inheritance trick, undefined is used + // to mask already consumed named arguments. + var value = source[key]; + if (value !== undefined) + dest[key] = value; + } + } + + // If there are prioritized keys, process them first. + if (prioritize) { + var keys = {}; + for (var i = 0, key, l = prioritize.length; i < l; i++) { + if ((key = prioritize[i]) in source) { + handleKey(key); + keys[key] = true; + } + } + // Now reference the processed keys as processed, so that + // handleKey() will not set them again below. + processed = keys; + } + + // If source is a filtered object, we get the keys from the the original + // object (it's parent / prototype). See _filtered inheritance trick in + // the argument reading code. + Object.keys(source.__unfiltered || source).forEach(handleKey); + return dest; + }, + + /** + * Returns true if obj is either a plain object or an array, as used by many + * argument reading methods. + */ + isPlainValue: function(obj, asString) { + return Base.isPlainObject(obj) || Array.isArray(obj) + || asString && typeof obj === 'string'; + }, + + /** + * Serializes the passed object into a format that can be passed to + * `JSON.stringify()` for JSON serialization. + */ + serialize: function(obj, options, compact, dictionary) { + options = options || {}; + + var isRoot = !dictionary, + res; + if (isRoot) { + options.formatter = new Formatter(options.precision); + // Create a simple dictionary object that handles all the storing + // and retrieving of dictionary definitions and references, e.g. for + // symbols and gradients. Items that want to support this need to + // define globally unique _id attribute. + /** + * @namespace + * @private + */ + dictionary = { + length: 0, + definitions: {}, + references: {}, + add: function(item, create) { + // See if we have reference entry with the given id already. + // If not, call create on the item to allow it to create the + // definition, then store the reference to it and return it. + var id = '#' + item._id, + ref = this.references[id]; + if (!ref) { + this.length++; + var res = create.call(item), + name = item._class; + // Also automatically insert class for dictionary + // entries. + if (name && res[0] !== name) + res.unshift(name); + this.definitions[id] = res; + ref = this.references[id] = [id]; + } + return ref; + } + }; + } + if (obj && obj._serialize) { + res = obj._serialize(options, dictionary); + // If we don't serialize to compact form (meaning no type + // identifier), see if _serialize didn't already add the class, e.g. + // for classes that do not support compact form. + var name = obj._class; + // Enforce class names on root level, except if the class explicitly + // asks to be serialized in compact form (Project). + if (name && !obj._compactSerialize && (isRoot || !compact) + && res[0] !== name) { + res.unshift(name); + } + } else if (Array.isArray(obj)) { + res = []; + for (var i = 0, l = obj.length; i < l; i++) + res[i] = Base.serialize(obj[i], options, compact, dictionary); + } else if (Base.isPlainObject(obj)) { + res = {}; + var keys = Object.keys(obj); + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + res[key] = Base.serialize(obj[key], options, compact, + dictionary); + } + } else if (typeof obj === 'number') { + res = options.formatter.number(obj, options.precision); + } else { + res = obj; + } + return isRoot && dictionary.length > 0 + ? [['dictionary', dictionary.definitions], res] + : res; + }, + + /** + * Deserializes from parsed JSON data. A simple convention is followed: + * Array values with a string at the first position are links to + * deserializable types through Base.exports, and the values following in + * the array are the arguments to their initialize function. Any other value + * is passed on unmodified. The passed json data is recoursively traversed + * and converted, leaves first. + */ + deserialize: function(json, create, _data, _setDictionary, _isRoot) { + var res = json, + isFirst = !_data, + hasDictionary = isFirst && json && json.length + && json[0][0] === 'dictionary'; + // A _data side-car to deserialize that can hold any kind of 'global' + // data across a deserialization. It's currently only used to hold + // dictionary definitions. + _data = _data || {}; + if (Array.isArray(json)) { + // See if it's a serialized type. If so, the rest of the array are + // the arguments to #initialize(). Either way, we simply deserialize + // all elements of the array. + var type = json[0], + // Handle stored dictionary specially, since we need to keep a + // lookup table to retrieve referenced items from. + isDictionary = type === 'dictionary'; + // First see if this is perhaps a dictionary reference, and if so + // return its definition instead. + if (json.length == 1 && /^#/.test(type)) { + return _data.dictionary[type]; + } + type = Base.exports[type]; + res = []; + // Skip first type entry for arguments. + // Pass true for _isRoot in children if we have a dictionary, + // in which case we need to shift the root level one down. + for (var i = type ? 1 : 0, l = json.length; i < l; i++) { + res.push(Base.deserialize(json[i], create, _data, + isDictionary, hasDictionary)); + } + if (type) { + // Create serialized type and pass collected arguments to + // constructor(). + var args = res; + // If a create method is provided, handle our own creation. This + // is used in #importJSON() to pass on insert = false to all + // items except layers. + if (create) { + res = create(type, args, isFirst || _isRoot); + } else { + res = Base.create(type.prototype); + type.apply(res, args); + } + } + } else if (Base.isPlainObject(json)) { + res = {}; + // We need to set the dictionary object before further + // deserialization, because serialized symbols may contain + // references to serialized gradients + if (_setDictionary) + _data.dictionary = res; + for (var key in json) + res[key] = Base.deserialize(json[key], create, _data); + } + // Filter out deserialized dictionary: + return hasDictionary ? res[1] : res; + }, + + exportJSON: function(obj, options) { + var json = Base.serialize(obj, options); + return options && options.asString == false + ? json + : JSON.stringify(json); + }, + + importJSON: function(json, target) { + return Base.deserialize( + typeof json === 'string' ? JSON.parse(json) : json, + // Provide our own create function to handle target and + // insertion. + function(ctor, args, isRoot) { + // If a target is provided and its of the right type + // for the root item, import right into it. + var useTarget = isRoot && target + && target.constructor === ctor, + obj = useTarget ? target + : Base.create(ctor.prototype); + // NOTE: We don't set insert false for layers since we want + // these to be created on the fly in the active project into + // which we're importing (except for if it's a preexisting + // target layer). + if (args.length === 1 && obj instanceof Item + && (useTarget || !(obj instanceof Layer))) { + var arg = args[0]; + if (Base.isPlainObject(arg)) + arg.insert = false; + } + // When reusing an object, initialize it through #set() + // instead of the constructor function: + (useTarget ? obj.set : ctor).apply(obj, args); + // Clear target to only use it once. + if (useTarget) + target = null; + return obj; + }); + }, + + /** + * Utility function for adding and removing items from a list of which each + * entry keeps a reference to its index in the list in the private _index + * property. Used for PaperScope#projects and Item#children. + */ + splice: function(list, items, index, remove) { + var amount = items && items.length, + append = index === undefined; + index = append ? list.length : index; + if (index > list.length) + index = list.length; + // Update _index on the items to be added first. + for (var i = 0; i < amount; i++) + items[i]._index = index + i; + if (append) { + // Append them all at the end by using push + list.push.apply(list, items); + // Nothing removed, and nothing to adjust above + return []; + } else { + // Insert somewhere else and/or remove + var args = [index, remove]; + if (items) + args.push.apply(args, items); + var removed = list.splice.apply(list, args); + // Erase the indices of the removed items + for (var i = 0, l = removed.length; i < l; i++) + removed[i]._index = undefined; + // Adjust the indices of the items above. + for (var i = index + amount, l = list.length; i < l; i++) + list[i]._index = i; + return removed; + } + }, + + /** + * Capitalizes the passed string: hello world -> Hello World + */ + capitalize: function(str) { + return str.replace(/\b[a-z]/g, function(match) { + return match.toUpperCase(); + }); + }, + + /** + * Camelizes the passed hyphenated string: caps-lock -> capsLock + */ + camelize: function(str) { + return str.replace(/-(.)/g, function(match, chr) { + return chr.toUpperCase(); + }); + }, + + /** + * Converts camelized strings to hyphenated ones: CapsLock -> caps-lock + */ + hyphenate: function(str) { + return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } -}); +}}); diff --git a/src/export.js b/src/export.js index 25e879f7..34893d3a 100644 --- a/src/export.js +++ b/src/export.js @@ -17,8 +17,6 @@ // global one in the whole scope. paper = new (PaperScope.inject(Base.exports, { - // Mark fields as enumerable so PaperScope.inject can pick them up - enumerable: true, Base: Base, Numerical: Numerical, Key: Key, diff --git a/src/item/HitResult.js b/src/item/HitResult.js index cebe3771..460ee4b0 100644 --- a/src/item/HitResult.js +++ b/src/item/HitResult.js @@ -26,11 +26,8 @@ var HitResult = Base.extend(/** @lends HitResult# */{ // Inject passed values, so we can be flexible about the HitResult // properties. // This allows the definition of getters too, e.g. for 'pixel'. - if (values) { - // Make enumerable so toString() works. - values.enumerable = true; + if (values) this.inject(values); - } }, /** diff --git a/src/item/Item.js b/src/item/Item.js index be96cac1..08882ff0 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -827,14 +827,14 @@ new function() { // Injection scope for various item event handlers opts.cacheItem = this; // If we're caching bounds, pass on this item as cacheItem, so // the children can setup _boundsCache structures for it. - var bounds = this._getCachedBounds(hasMatrix && matrix, opts); + var rect = this._getCachedBounds(hasMatrix && matrix, opts).rect; // If we're returning '#bounds', create a LinkedRectangle that uses // the setBounds() setter to update the Item whenever the bounds are // changed: return !arguments.length - ? new LinkedRectangle(bounds.x, bounds.y, bounds.width, - bounds.height, this, 'setBounds') - : bounds; + ? new LinkedRectangle(rect.x, rect.y, rect.width, rect.height, + this, 'setBounds') + : rect; }, setBounds: function(/* rect */) { @@ -889,6 +889,14 @@ new function() { // Injection scope for various item event handlers return Item._getBounds(children, matrix, options); }, + _getBoundsCacheKey: function(options, internal) { + return [ + options.stroke ? 1 : 0, + options.handle ? 1 : 0, + internal ? 1 : 0 + ].join(''); + }, + /** * Private method that deals with the calling of _getBounds, recursive * matrix concatenation and handles all the complicated caching mechanisms. @@ -904,29 +912,43 @@ new function() { // Injection scope for various item event handlers cacheItem = options.cacheItem, _matrix = internal ? null : this._matrix._orNullIfIdentity(), // Create a key for caching, reflecting all bounds options. - cacheKey = cacheItem && (!matrix || matrix.equals(_matrix)) && [ - options.stroke ? 1 : 0, - options.handle ? 1 : 0, - internal ? 1 : 0 - ].join(''); + cacheKey = cacheItem && (!matrix || matrix.equals(_matrix)) + && this._getBoundsCacheKey(options, internal), + bounds = this._bounds; // NOTE: This needs to happen before returning cached values, since even // then, _boundsCache needs to be kept up-to-date. Item._updateBoundsCache(this._parent || this._symbol, cacheItem); - if (cacheKey && this._bounds && cacheKey in this._bounds) - return this._bounds[cacheKey].rect.clone(); - var bounds = this._getBounds(matrix || _matrix, options); + if (cacheKey && bounds && cacheKey in bounds) { + var cached = bounds[cacheKey]; + return { + rect: cached.rect.clone(), + nonscaling: cached.nonscaling + }; + } + var res = this._getBounds(matrix || _matrix, options), + // Support two versions of _getBounds(): One that directly returns a + // Rectangle, and one that returns a bounds object with nonscaling. + rect = res.rect || res, + style = this._style, + nonscaling = res.nonscaling || style.hasStroke() + && !style.getStrokeScaling(); // If we can cache the result, update the _bounds cache structure // before returning if (cacheKey) { - if (!this._bounds) - this._bounds = {}; - var cached = this._bounds[cacheKey] = { - rect: bounds.clone(), + if (!bounds) { + this._bounds = bounds = {}; + } + var cached = bounds[cacheKey] = { + rect: rect.clone(), + nonscaling: nonscaling, // Mark as internal, so Item#transform() won't transform it internal: internal }; } - return bounds; + return { + rect: rect, + nonscaling: nonscaling + }; }, /** @@ -1005,7 +1027,10 @@ new function() { // Injection scope for various item event handlers var x1 = Infinity, x2 = -x1, y1 = x1, - y2 = x2; + y2 = x2, + nonscaling = false; + // NOTE: As soon as one child-item has non-scaling strokes, the full + // bounds need to be considered non-scaling for caching purposes. options = options || {}; for (var i = 0, l = items.length; i < l; i++) { var item = items[i]; @@ -1013,17 +1038,23 @@ new function() { // Injection scope for various item event handlers // Pass true for noInternal, since even when getting // internal bounds for this item, we need to apply the // matrices to its children. - var rect = item._getCachedBounds( - matrix && matrix.appended(item._matrix), options, true); + var bounds = item._getCachedBounds( + matrix && matrix.appended(item._matrix), options, true), + rect = bounds.rect; x1 = Math.min(rect.x, x1); y1 = Math.min(rect.y, y1); x2 = Math.max(rect.x + rect.width, x2); y2 = Math.max(rect.y + rect.height, y2); + if (bounds.nonscaling) + nonscaling = true; } } - return isFinite(x1) + return { + rect: isFinite(x1) ? new Rectangle(x1, y1, x2 - x1, y2 - y1) - : new Rectangle(); + : new Rectangle(), + nonscaling: nonscaling + }; } } @@ -1122,8 +1153,22 @@ new function() { // Injection scope for various item event handlers scaling = Point.read(arguments, 0, { clone: true, readNull: true }); if (current && scaling && !current.equals(scaling)) { // See #setRotation() for preservation of _decomposed. - var decomposed = this._decomposed; - this.scale(scaling.x / current.x, scaling.y / current.y); + var rotation = this.getRotation(), + decomposed = this._decomposed, + matrix = new Matrix(), + center = this.getPosition(true); + // Create a matrix in which the scaling is applied in the non- + // rotated state, so it is always applied before the rotation. + // TODO: What about skewing? Do we need separately stored values for + // these properties, and apply them separately from the matrix? + matrix.translate(center); + if (rotation) + matrix.rotate(rotation); + matrix.scale(scaling.x / current.x, scaling.y / current.y); + if (rotation) + matrix.rotate(-rotation); + matrix.translate(center.negate()); + this.transform(matrix); if (decomposed) { decomposed.scaling = scaling; this._decomposed = decomposed; @@ -3376,11 +3421,9 @@ new function() { // Injection scope for hit-test functions shared with project // 'lines'. Default: ['objects', 'children'] transform: function(matrix, _applyMatrix, _applyRecursively, _setApplyMatrix) { - // If no matrix is provided, or the matrix is the identity, we might - // still have some work to do in case _applyMatrix is true - if (matrix && matrix.isIdentity()) - matrix = null; var _matrix = this._matrix, + // If no matrix is provided, or the matrix is the identity, we might + // still have some work to do in case _applyMatrix is true transform = matrix && !matrix.isIdentity(), applyMatrix = (_applyMatrix || this._applyMatrix) // Don't apply _matrix if the result of concatenating with @@ -3398,30 +3441,8 @@ new function() { // Injection scope for hit-test functions shared with project // non-invertible. This is then used again in setBounds to restore. if (!matrix.isInvertible() && _matrix.isInvertible()) _matrix._backup = _matrix.getValues(); - _matrix.prepend(matrix); - } - // Call #_transformContent() now, if we need to directly apply the - // internal _matrix transformations to the item's content. - // Application is not possible on Raster, PointText, SymbolItem, since - // the matrix is where the actual transformation state is stored. - if (applyMatrix) { - if (this._transformContent(_matrix, _applyRecursively, - _setApplyMatrix)) { - var pivot = this._pivot; - if (pivot) - _matrix._transformPoint(pivot, pivot, true); - // Reset the internal matrix to the identity transformation if - // it was possible to apply it. - _matrix.reset(true); - // Set the internal _applyMatrix flag to true if we're told to - // do so - if (_setApplyMatrix && this._canApplyMatrix) - this._applyMatrix = true; - } else { - applyMatrix = transform = false; - } - } - if (transform) { + // Pass `true` for _dontNotify, as we're handling this after. + _matrix.prepend(matrix, true); // When a new matrix was applied, we also need to transform gradient // color points. These always need transforming, regardless of // #applyMatrix, as they are defined in the parent's coordinate @@ -3438,39 +3459,65 @@ new function() { // Injection scope for hit-test functions shared with project if (strokeColor) strokeColor.transform(matrix); } + // Call #_transformContent() now, if we need to directly apply the + // internal _matrix transformations to the item's content. + // Application is not possible on Raster, PointText, SymbolItem, since + // the matrix is where the actual transformation state is stored. + if (applyMatrix && (applyMatrix = this._transformContent(_matrix, + _applyRecursively, _setApplyMatrix))) { + // Pivot is provided in the parent's coordinate system, so transform + // it along too. + var pivot = this._pivot; + if (pivot) + _matrix._transformPoint(pivot, pivot, true); + // Reset the internal matrix to the identity transformation if + // it was possible to apply it, but do not notify owner of change. + _matrix.reset(true); + // Set the internal _applyMatrix flag to true if we're told to + // do so + if (_setApplyMatrix && this._canApplyMatrix) + this._applyMatrix = true; + } // Calling _changed will clear _bounds and _position, but depending // on matrix we can calculate and set them again, so preserve them. var bounds = this._bounds, position = this._position; - // We always need to call _changed since we're caching bounds on all - // items, including Group. - this._changed(/*#=*/Change.GEOMETRY); + if (transform || applyMatrix) { + this._changed(/*#=*/Change.GEOMETRY); + } // Detect matrices that contain only translations and scaling // and transform the cached _bounds and _position without having to // fully recalculate each time. - var decomp = bounds && matrix && matrix.decompose(); - if (decomp && !decomp.shearing && decomp.rotation % 90 === 0) { - // Transform the old bound by looping through all the cached bounds - // in _bounds and transform each. + var decomp = transform && bounds && matrix.decompose(); + if (decomp && decomp.skewing.isZero() && decomp.rotation % 90 === 0) { + // Transform the old bound by looping through all the cached + // bounds in _bounds and transform each. for (var key in bounds) { var cache = bounds[key]; - // If these are internal bounds, only transform them if this - // item applied its matrix. - if (applyMatrix || !cache.internal) { + // If any item involved in the determination of these bounds has + // non-scaling strokes, delete the cache now as it can't be + // preserved through the transformation. + if (cache.nonscaling) { + delete bounds[key]; + } else if (applyMatrix || !cache.internal) { + // If these are internal bounds, only transform them if this + // item applied its matrix. var rect = cache.rect; matrix._transformBounds(rect, rect); } } - // If we have cached bounds, update _position again as its - // center. We need to take into account _boundsGetter here too, in - // case another getter is assigned to it, e.g. 'getStrokeBounds'. - var getter = this._boundsGetter, - rect = bounds[getter && getter.getBounds || getter || 'getBounds']; - if (rect) - this._position = rect.getCenter(true); this._bounds = bounds; - } else if (matrix && position) { - // Transform position as well. + // If we have cached bounds, try to determine _position as its + // center. Use _boundsOptions do get the cached default bounds. + var cached = bounds[this._getBoundsCacheKey( + this._boundsOptions || {})]; + if (cached) { + this._position = cached.rect.getCenter(true); + } + } else if (transform && position && this._pivot) { + // If the item has a pivot defined, it means that the default + // position defined as the center of the bounds won't shift with + // arbitrary transformations and we can therefore update _position: this._position = matrix._transformPoint(position, position); } // Allow chaining here, since transform() is related to Matrix functions diff --git a/src/options.js b/src/options.js index e7c380b7..19eaa278 100644 --- a/src/options.js +++ b/src/options.js @@ -17,7 +17,7 @@ // The paper.js version. // NOTE: Adjust value here before calling `gulp publish`, which then updates and // publishes the various JSON package files automatically. -var version = '0.11.2'; +var version = '0.11.3'; // If this file is loaded in the browser, we're in load.js mode. var load = typeof window === 'object'; diff --git a/src/path/Curve.js b/src/path/Curve.js index bd0df8cf..33a887ef 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -1955,7 +1955,7 @@ new function() { // Scope for bezier intersection using fat-line clipping // Calculate the curve values of the rotated curve. rv = [], roots = []; - for(var i = 0; i < 8; i += 2) { + for (var i = 0; i < 8; i += 2) { var x = v[i] - px, y = v[i + 1] - py; rv.push( diff --git a/src/svg/SvgExport.js b/src/svg/SvgExport.js index 31ee20fb..382a49e5 100644 --- a/src/svg/SvgExport.js +++ b/src/svg/SvgExport.js @@ -51,9 +51,9 @@ new function() { if (!Numerical.isZero(scale.x - 1) || !Numerical.isZero(scale.y - 1)) parts.push('scale(' + formatter.point(scale) +')'); - if (skew && skew.x) + if (skew.x) parts.push('skewX(' + formatter.number(skew.x) + ')'); - if (skew && skew.y) + if (skew.y) parts.push('skewY(' + formatter.number(skew.y) + ')'); attrs.transform = parts.join(' '); } else { @@ -115,8 +115,9 @@ new function() { if (length > 2) { type = item._closed ? 'polygon' : 'polyline'; var parts = []; - for(var i = 0; i < length; i++) + for (var i = 0; i < length; i++) { parts.push(formatter.point(segments[i]._point)); + } attrs.points = parts.join(' '); } else { type = 'line'; @@ -416,6 +417,7 @@ new function() { ? new Rectangle([0, 0], view.getViewSize()) : bounds === 'content' ? Item._getBounds(children, matrix, { stroke: true }) + .rect : Rectangle.read([bounds], 0, { readNull: true }), attrs = { version: '1.1', diff --git a/src/text/PointText.js b/src/text/PointText.js index 08102640..0886862e 100644 --- a/src/text/PointText.js +++ b/src/text/PointText.js @@ -115,9 +115,9 @@ var PointText = TextItem.extend(/** @lends PointText# */{ x -= width / (justification === 'center' ? 2: 1); // Until we don't have baseline measuring, assume 1 / 4 leading as a // rough guess: - var bounds = new Rectangle(x, + var rect = new Rectangle(x, numLines ? - 0.75 * leading : 0, width, numLines * leading); - return matrix ? matrix._transformBounds(bounds, bounds) : bounds; + return matrix ? matrix._transformBounds(rect, rect) : rect; } }); diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 7c5305ab..b2b3adfd 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -125,7 +125,7 @@ var Numerical = new function() { /** * The machine epsilon for a double precision (Javascript Number) is * 2.220446049250313e-16. (try this in the js console: - * (function(){for(var e=1;1<1+e/2;)e/=2;return e}()) + * (function(){ for (var e = 1; 1 < 1+e/2;) e/=2; return e }()) * * The constant MACHINE_EPSILON here refers to the constants δ and ε * such that, the error introduced by addition, multiplication on a diff --git a/src/view/View.js b/src/view/View.js index 20a5c430..39e896da 100644 --- a/src/view/View.js +++ b/src/view/View.js @@ -1271,12 +1271,11 @@ new function() { // Injection scope for event handling on the browser point, prevPoint) // Next handle the hit-item, if it's different from the drag-item // and not a descendant of it (in which case it would already have - // received an event in the call above). Use fallbacks to translate - // mousedrag to mousemove, since drag is handled above. + // received an event in the call above). || hitItem && hitItem !== dragItem && !hitItem.isDescendant(dragItem) - && emitMouseEvent(hitItem, null, fallbacks[type] || type, event, - point, prevPoint, dragItem) + && emitMouseEvent(hitItem, null, type, event, point, prevPoint, + dragItem) // Lastly handle the mouse events on the view, if we're still here. || emitMouseEvent(view, dragItem || hitItem || view, type, event, point, prevPoint)); diff --git a/test/tests/Item.js b/test/tests/Item.js index 34912b13..d19626b4 100644 --- a/test/tests/Item.js +++ b/test/tests/Item.js @@ -876,3 +876,56 @@ test('Item#pivot', function() { equals(path2.pivot, pivot.add(difference), 'Changing position of an item with applyMatrix = true should change pivot'); }); + +test('Item#position with irregular shape, #pivot and rotation', function() { + var path1 = new Path([ [0, 0], [200, 100], [0, 100] ]); + var path2 = path1.clone(); + path2.pivot = path2.position; + equals(path1.position, new Point(100, 50), + 'path1.position, before rotation'); + path1.rotate(45); + equals(path1.position, new Point(64.64466, 50), + 'path1.position, after rotation'); + equals(path2.position, new Point(100, 50), + 'path2.position with pivot, before rotation'); + path2.rotate(45); + equals(path2.position, new Point(100, 50), + 'path2.position with pivot, after rotation'); +}); + +test('Item#scaling, #rotation', function() { + var expected = new Rectangle(100, 50, 100, 200); + + var rect1 = new Path.Rectangle({ + from: [100, 100], + to: [200, 200], + applyMatrix: false + }); + var rect2 = rect1.clone(); + + rect1.scaling = [2, 1]; + rect1.rotation = 90; + equals(rect1.bounds, expected, + 'rect1.bounds, setting rect1.scaling before rect1.rotation'); + + rect2.rotation = 90; + rect2.scaling = [2, 1]; + equals(rect2.bounds, expected, + 'rect2.bounds, setting rect2.scaling before rect2.rotation'); + + var shape1 = new Shape.Rectangle({ + from: [100, 100], + to: [200, 200] + }); + var shape2 = shape1.clone(); + + shape1.scaling = [2, 1]; + shape1.rotation = 90; + equals(shape1.bounds, expected, + 'shape1.bounds, setting shape1.scaling before shape1.rotation'); + + shape2.rotation = 90; + shape2.scaling = [2, 1]; + equals(shape2.bounds, expected, + 'shape2.bounds, setting shape2.scaling before shape2.rotation'); +}); diff --git a/test/tests/Item_Bounds.js b/test/tests/Item_Bounds.js index d6bf114a..b4a3ff59 100644 --- a/test/tests/Item_Bounds.js +++ b/test/tests/Item_Bounds.js @@ -694,12 +694,15 @@ test('path.strokeBounds with applyMatrix disabled', function() { strokeColor: 'red', strokeWidth: 10 }); - equals(path.strokeBounds, new Rectangle(5, 5, 30, 30), 'path.strokeBounds, applyMatrix enabled'); + equals(path.strokeBounds, new Rectangle(5, 5, 30, 30), + 'path.strokeBounds, applyMatrix enabled'); path.applyMatrix = false; - equals(path.strokeBounds, new Rectangle(5, 5, 30, 30), 'path.strokeBounds, applyMatrix disabled'); + equals(path.strokeBounds, new Rectangle(5, 5, 30, 30), + 'path.strokeBounds, applyMatrix disabled'); path.scale([4, 2], [0, 0]); var expected = new Rectangle(20, 10, 120, 60); - equals(path.strokeBounds, expected, 'path.strokeBounds after scaling, applyMatrix disabled'); + equals(path.strokeBounds, expected, + 'path.strokeBounds after scaling, applyMatrix disabled'); function testHitResult() { // Hit-testing needs to handle applyMatrix disabled with stroke scaling, // even when hit-testing on "distorted" stroke joins: @@ -714,10 +717,29 @@ test('path.strokeBounds with applyMatrix disabled', function() { testHitResult(); path.applyMatrix = true; expected = new Rectangle(35, 15, 90, 50); - equals(path.strokeBounds, expected, 'path.strokeBounds after scaling, applyMatrix enabled'); + equals(path.strokeBounds, expected, + 'path.strokeBounds after scaling, applyMatrix enabled'); testHitResult(); }); +test('TEST', function() { + var path = new Path.Rectangle({ + applyMatrix: false, + point: [10, 10], + size: [20, 20], + strokeScaling: true, + strokeColor: 'red', + strokeWidth: 10 + }); + path.scale([4, 2], [0, 0]); + equals(path.strokeBounds, new Rectangle(20, 10, 120, 60), + 'path.strokeBounds after scaling, applyMatrix disabled'); + path.applyMatrix = true; + equals(path.strokeBounds, new Rectangle(35, 15, 90, 50), + 'path.strokeBounds after scaling, applyMatrix enabled'); + +}); + test('symbolItem.bounds with strokeScaling disabled', function() { var path = new Path.Rectangle({ size: [20, 20], diff --git a/test/tests/JSON.js b/test/tests/JSON.js index 2124c638..1c2fe5af 100644 --- a/test/tests/JSON.js +++ b/test/tests/JSON.js @@ -154,7 +154,7 @@ test('transform test 1', function() { var clones = 30; var angle = 360 / clones; - for(var i = 0; i < clones; i++) { + for (var i = 0; i < clones; i++) { var clonedPath = circlePath.clone(); clonedPath.rotate(angle * i, circlePath.bounds.topLeft); }