mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-08 13:52:06 -05:00
455 lines
15 KiB
JavaScript
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();
|
|
}
|
|
}
|
|
});
|