paper.js/src/core/Base.js
Jürg Lehni f2ae7840cf A lot of work on documentation.
- @values lists
- Improve event documentation
- Compound path
- etc.
2016-01-08 20:45:54 +01:00

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();
}
}
});