/* * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. * http://paperjs.org/ * * Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey * http://lehni.org/ & 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. Also set // this.Base on the injection scope, since straps.js ommits that. 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(', ') + ' }'; }, /** * Serializes this object to a JSON string. * * @param {Object} [options={ precision: 5 }] */ 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=undefined] a lookup table listing properties to * exclude. * @return {Boolean} {@true if the object is a plain object} */ _set: function(props, exclude) { if (props && 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 orig = props._filtering || props; for (var key in orig) { if (key in this && orig.hasOwnProperty(key) && (!exclude || !exclude[key])) { var value = props[key]; // Due to the _filtered inheritance trick, undefined is used // to mask already consumed named arguments. if (value !== undefined) this[key] = value; } } return true; } }, 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; }, /** * 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) { function checkKeys(o1, o2) { for (var i in o1) if (o1.hasOwnProperty(i) && !o2.hasOwnProperty(i)) return false; return true; } 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); // Compare arrays if (Array.isArray(obj1) && Array.isArray(obj2)) { if (obj1.length !== obj2.length) return false; for (var i = 0, l = obj1.length; i < l; i++) { if (!Base.equals(obj1[i], obj2[i])) return false; } return true; } // Compare objects if (obj1 && typeof obj1 === 'object' && obj2 && typeof obj2 === 'object') { if (!checkKeys(obj1, obj2) || !checkKeys(obj2, obj1)) return false; for (var i in obj1) { if (obj1.hasOwnProperty(i) && !Base.equals(obj1[i], obj2[i])) 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 apssed 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 {@code options.readNull} controls whether null * is returned or converted. {@code options.clone} controls * whether passed objects should be cloned if they are already * provided in the required type */ read: function(list, start, length, options) { // 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++; list.__read = 1; 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; // If options were provided, pass them on to the constructed object if (options) obj.__options = options; 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; // Have arguments.__read point to the amount of args read in the // last read() call list.__read = obj.__read; delete obj.__read; if (options) delete obj.__options; } 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]; }, /** * Reads all readable arguments from the list, handling nested arrays * seperately. * @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 {@code options.readNull} controls whether null * is returned or converted. {@code 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]) // lenghh = 0 for length = max ? this.read(entry, 0, 0, options) : this.read(list, i, 1, options)); } 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, length, options) { 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, length, options); }, /** * @return the named value if the list provides an arguments object, * {@code null} if the named value is {@code null} or {@code undefined}, * and {@code 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 {@code null}, it returns {@code 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) { return this.isPlainObject(obj) || Array.isArray(obj); }, /** * 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 = {}; for (var i in obj) if (obj.hasOwnProperty(i)) res[i] = Base.serialize(obj[i], 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) { 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 is a lookup table to retrieve referenced items from. isDictionary = type === 'dictionary'; if (!isDictionary) { // First see if this is perhaps a dictionary reference, and // if so return its definition instead. if (_data.dictionary && json.length == 1 && /^#/.test(type)) return _data.dictionary[type]; type = Base.exports[type]; } 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)); if (isDictionary) { _data.dictionary = res[0]; } else 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, isRoot); } else { res = Base.create(type.prototype); type.apply(res, args); } } } else if (Base.isPlainObject(json)) { res = {}; for (var key in json) res[key] = Base.deserialize(json[key], create, _data); } return res; }, exportJSON: function(obj, options) { return JSON.stringify(Base.serialize(obj, options)); }, 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, isRoot) { // 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 (!isRoot && args.length === 1 && obj instanceof Item && (!(obj instanceof Layer) || isTarget)) { 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); // Delete the indices of the removed items for (var i = 0, l = removed.length; i < l; i++) delete removed[i]._index; // 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(); } } });