mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-23 15:59:45 -05:00
f2ae7840cf
- @values lists - Improve event documentation - Compound path - etc.
564 lines
24 KiB
JavaScript
564 lines
24 KiB
JavaScript
/*
|
|
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
|
|
* http://paperjs.org/
|
|
*
|
|
* Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey
|
|
* http://scratchdisk.com/ & http://jonathanpuckey.com/
|
|
*
|
|
* Distributed under the MIT license. See LICENSE file for details.
|
|
*
|
|
* All rights reserved.
|
|
*/
|
|
|
|
/**
|
|
* @name Base
|
|
* @class
|
|
* @private
|
|
*/
|
|
// Extend Base with utility functions used across the library.
|
|
Base.inject(/** @lends Base# */{
|
|
/**
|
|
* Renders base objects to strings in object literal notation.
|
|
*/
|
|
toString: function() {
|
|
return this._id != null
|
|
? (this._class || 'Object') + (this._name
|
|
? " '" + this._name + "'"
|
|
: ' @' + this._id)
|
|
: '{ ' + Base.each(this, function(value, key) {
|
|
// Hide internal properties even if they are enumerable
|
|
if (!/^_/.test(key)) {
|
|
var type = typeof value;
|
|
this.push(key + ': ' + (type === 'number'
|
|
? Formatter.instance.number(value)
|
|
: type === 'string' ? "'" + value + "'" : value));
|
|
}
|
|
}, []).join(', ') + ' }';
|
|
},
|
|
|
|
/**
|
|
* The class name of the object as a string, if the prototype defines a
|
|
* `_class` value.
|
|
*
|
|
* @bean
|
|
*/
|
|
getClassName: function() {
|
|
return this._class || '';
|
|
},
|
|
|
|
/**
|
|
* Exports (serializes) this object to a JSON data object or string.
|
|
*
|
|
* @option [options.asString=true] {Boolean} whether the JSON is returned as
|
|
* a `Object` or a `String`
|
|
* @option [options.precision=5] {Number} the amount of fractional digits in
|
|
* numbers used in JSON data
|
|
*
|
|
* @param {Object} [options] the serialization options
|
|
* @return {String} the exported JSON data
|
|
*/
|
|
exportJSON: function(options) {
|
|
return Base.exportJSON(this, options);
|
|
},
|
|
|
|
// To support JSON.stringify:
|
|
toJSON: function() {
|
|
return Base.serialize(this);
|
|
},
|
|
|
|
/**
|
|
* #_set() is part of the mechanism for constructors which take one object
|
|
* literal describing all the properties to be set on the created instance.
|
|
*
|
|
* @param {Object} props an object describing the properties to set
|
|
* @param {Object} [exclude] a lookup table listing properties to exclude
|
|
* @param {Boolean} [dontCheck=false] whether to perform a
|
|
* Base.isPlainObject() check on props or not
|
|
* @return {Boolean} {@true if the object is a plain object}
|
|
*/
|
|
_set: function(props, exclude, dontCheck) {
|
|
if (props && (dontCheck || Base.isPlainObject(props))) {
|
|
// If props is a filtering object, we need to execute hasOwnProperty
|
|
// on the original object (it's parent / prototype). See _filtered
|
|
// inheritance trick in the argument reading code.
|
|
var keys = Object.keys(props._filtering || props);
|
|
for (var i = 0, l = keys.length; i < l; i++) {
|
|
var key = keys[i];
|
|
if (!(exclude && exclude[key])) {
|
|
// Due to the _filtered inheritance trick, undefined is used
|
|
// to mask already consumed named arguments.
|
|
var value = props[key];
|
|
if (value !== undefined)
|
|
this[key] = value;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
},
|
|
|
|
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)
|
|
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)
|
|
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 {Number} length the amount of elements that can be read
|
|
* @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
|
|
*/
|
|
read: function(list, start, options, length) {
|
|
// 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,
|
|
index = start || readIndex && list.__index || 0;
|
|
if (!length)
|
|
length = list.length - index;
|
|
var obj = list[index];
|
|
if (obj instanceof this
|
|
|| options && options.readNull && obj == null && length <= 1) {
|
|
if (readIndex)
|
|
list.__index = index + 1;
|
|
return obj && options && options.clone ? obj.clone() : obj;
|
|
}
|
|
obj = Base.create(this.prototype);
|
|
if (readIndex)
|
|
obj.__read = true;
|
|
obj = obj.initialize.apply(obj, index > 0 || length < list.length
|
|
? Array.prototype.slice.call(list, index, index + length)
|
|
: list) || obj;
|
|
if (readIndex) {
|
|
list.__index = index + 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
|
|
*/
|
|
readAll: function(list, start, options) {
|
|
var res = [],
|
|
entry;
|
|
for (var i = start || 0, l = list.length; i < l; 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 {Number} start the index at which to start reading in the list
|
|
* @param {String} name the property name to read from
|
|
*/
|
|
readNamed: function(list, name, start, options, length) {
|
|
var value = this.getNamed(list, name),
|
|
hasObject = value !== undefined;
|
|
if (hasObject) {
|
|
// Create a _filtered object that inherits from argument 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 _filtering to the original so Base#_set() can
|
|
// execute hasOwnProperty on it.
|
|
filtered._filtering = 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, length);
|
|
},
|
|
|
|
/**
|
|
* @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);
|
|
},
|
|
|
|
/**
|
|
* Returns true if obj is either a plain object or an array, as used by
|
|
* many argument reading methods.
|
|
*/
|
|
isPlainValue: function(obj, asString) {
|
|
return this.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 root = !dictionary,
|
|
res;
|
|
if (root) {
|
|
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;
|
|
if (name && !compact && !res._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);
|
|
// Mark array as compact, so obj._serialize handling above
|
|
// doesn't add the class name again.
|
|
if (compact)
|
|
res._compact = true;
|
|
} 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 root && 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, _isDictionary) {
|
|
var res = json,
|
|
isRoot = !_data;
|
|
// 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 = [];
|
|
// We need to set the dictionary object before further
|
|
// deserialization, because serialized symbols may contain
|
|
// references to serialized gradients
|
|
if (_isDictionary)
|
|
_data.dictionary = res;
|
|
// Skip first type entry for arguments
|
|
for (var i = type ? 1 : 0, l = json.length; i < l; i++)
|
|
res.push(Base.deserialize(json[i], create, _data,
|
|
isDictionary));
|
|
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);
|
|
} else {
|
|
res = Base.create(type.prototype);
|
|
type.apply(res, args);
|
|
}
|
|
}
|
|
} else if (Base.isPlainObject(json)) {
|
|
res = {};
|
|
// See above why we have to set this before Base.deserialize()
|
|
if (_isDictionary)
|
|
_data.dictionary = res;
|
|
for (var key in json)
|
|
res[key] = Base.deserialize(json[key], create, _data);
|
|
}
|
|
// Filter out deserialized dictionary.
|
|
return isRoot && json && json.length && json[0][0] === 'dictionary'
|
|
? 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(type, args) {
|
|
// If a target is provided and its of the right type,
|
|
// import right into it.
|
|
var obj = target && target.constructor === type
|
|
? target
|
|
: Base.create(type.prototype),
|
|
isTarget = obj === target;
|
|
// 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
|
|
&& (isTarget || !(obj instanceof Layer))) {
|
|
var arg = args[0];
|
|
if (Base.isPlainObject(arg))
|
|
arg.insert = false;
|
|
}
|
|
type.apply(obj, args);
|
|
// Clear target to only use it once.
|
|
if (isTarget)
|
|
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(all, chr) {
|
|
return chr.toUpperCase();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Converst camelized strings to hyphenated ones: CapsLock -> caps-lock
|
|
*/
|
|
hyphenate: function(str) {
|
|
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
}
|
|
}
|
|
});
|