paper.js/src/core/Base.js

455 lines
15 KiB
JavaScript

/*
* 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 bootstrap.js ommits that.
this.Base = Base.inject(/** @lends Base# */{
// Have generics versions of #clone() and #toString():
generics: true,
/**
* General purpose clone function that delegates cloning to the constructor
* that receives the object to be cloned as the first argument.
* Note: #clone() needs to be overridden in any class that requires other
* cloning behavior.
*/
clone: function() {
return new this.constructor(this);
},
/**
* Renders base objects to strings in object literal notation.
*/
toString: function() {
return this._id != null
? (this.constructor._name || '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'
? Format.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);
},
/**
* #_set() is part of the mechanism for constructors which take one object
* literal describing all the properties to be set on the created instance.
* @return {Boolean} {@true if the object is a plain object}
*/
_set: function(props) {
if (Base.isPlainObject(props)) {
for (var key in props)
if (props.hasOwnProperty(key) && key in this)
this[key] = props[key];
return true;
}
},
statics: /** @lends Base */{
_types: {},
/**
* A uniqued id number, which when consumed needs to be increased by one
*/
_uid: 0,
extend: function(src) {
// Override Base.extend() with a version that registers classes that
// define #_type inside the Base._types lookup, for deserialization.
var res = this.base.apply(this, arguments);
if (src._type)
Base._types[src._type] = 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) && typeof o2[i] === 'undefined')
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 {Boolean} clone controls wether passed objects should be
* cloned if they are already provided in the required type
*/
read: function(list, start, length, clone, readNull) {
// 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
// If the class defines _readNull, return null when nothing
// was provided
|| (proto._readNull || readNull) && obj == null && length <= 1) {
if (readIndex)
list._index = index + 1;
return obj && clone ? obj.clone() : obj;
}
obj = Base.create(this);
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;
// Have arguments._read point to the amount of args read in the
// last read() call
list._read = obj._read;
delete obj._read;
}
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 {Boolean} clone controls wether passed objects should be
* cloned if they are already provided in the required type
*/
readAll: function(list, start, clone) {
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, 0, clone) // 0 for length = max
: this.read(list, i, 1, clone));
}
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) {
var value = this.getNamed(list, name);
// value is undefined if there is no arguments object, and null
// if there is one, but no value is defined.
return this.read(value !== undefined ? [value] : list);
},
/**
* @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.
value = name ? arg[name] : arg;
// Convert undefined to null, to distinguish from undefined
// result, when there is no arguments object.
return value !== undefined ? value : null;
}
},
/**
* 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);
},
/**
* 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) {
// 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.
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++;
this.definitions[id] = create.call(item);
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 type,
// e.g. for types that do not support compact form.
if (obj._type && !compact && res[0] !== obj._type)
res.unshift(obj._type);
} 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 = {};
for (var i in obj)
if (obj.hasOwnProperty(i))
res[i] = Base.serialize(obj[i], options, compact,
dictionary);
} else if (typeof obj === 'number') {
res = Format.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._types, and the values following in
* the array are the arguments to their initialize function.
* Any other value is passed on unmodified.
* The passed data is recoursively traversed and converted, leaves first
*/
deserialize: function(obj, data) {
var res = obj;
// A data side-car to deserialize that can hold any kind of 'global'
// data across a deserialization. It's currently just used to hold
// dictionary definitions.
data = data || {};
if (Array.isArray(obj)) {
// 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 = obj[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 && obj.length == 1 && /^#/.test(type))
return data.dictionary[type];
type = Base._types[type];
}
res = [];
// Skip first type entry for arguments
for (var i = type ? 1 : 0, l = obj.length; i < l; i++)
res.push(Base.deserialize(obj[i], data));
if (isDictionary) {
data.dictionary = res[0];
} else if (type) {
// Create serialized type and pass collected arguments to
// #initialize().
var args = res;
res = Base.create(type);
res.initialize.apply(res, args);
}
} else if (Base.isPlainObject(obj)) {
res = {};
for (var key in obj)
res[key] = Base.deserialize(obj[key], data);
}
return res;
},
exportJson: function(obj, options) {
return JSON.stringify(Base.serialize(obj, options));
},
importJson: function(json) {
return Base.deserialize(JSON.parse(json));
},
/**
* 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;
// 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;
}
},
/**
* Merge all passed hash objects into a newly creted Base object.
*/
merge: function() {
return Base.each(arguments, function(hash) {
Base.each(hash, function(value, key) {
this[key] = value;
}, this);
}, new Base(), true); // Pass true for asArray, as arguments is none
},
/**
* 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();
}
}
});