diff --git a/src/basic/Matrix.js b/src/basic/Matrix.js index 50c0930a..d7a91c91 100644 --- a/src/basic/Matrix.js +++ b/src/basic/Matrix.js @@ -42,6 +42,8 @@ * matrix multiplication). */ var Matrix = this.Matrix = Base.extend(/** @lends Matrix# */{ + _type: 'matrix', + /** * Creates a 2D affine transform. * @@ -74,6 +76,10 @@ var Matrix = this.Matrix = Base.extend(/** @lends Matrix# */{ throw new Error('Unsupported matrix parameters'); }, + _serialize: function() { + return this.getValues(); + }, + /** * @return {Matrix} A copy of this transform. */ diff --git a/src/basic/Point.js b/src/basic/Point.js index de3b5b1c..315db5db 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -168,6 +168,10 @@ var Point = this.Point = Base.extend(/** @lends Point# */{ } }, + _serialize: function() { + return [this.x, this.y]; + }, + /** * The x coordinate of the point * diff --git a/src/color/Color.js b/src/color/Color.js index eed7b26c..52efd1a7 100644 --- a/src/color/Color.js +++ b/src/color/Color.js @@ -282,6 +282,17 @@ var Color = this.Color = Base.extend(new function() { return res; }, + _serialize: function() { + var res = []; + for (var i = 0, l = this._components.length; i < l; i++) { + var component = this._components[i], + value = this['_' + component]; + if (component !== 'alpha' || value != null && value < 1) + res.push(value); + } + return res; + }, + /** * @return {RgbColor|GrayColor|HsbColor} a copy of the color object */ diff --git a/src/core/Base.js b/src/core/Base.js index d09538cc..e5edfb1b 100644 --- a/src/core/Base.js +++ b/src/core/Base.js @@ -50,8 +50,23 @@ this.Base = Base.inject(/** @lends Base# */{ }, []).join(', ') + ' }'; }, + toJson: function() { + return Base.toJson(this); + }, + statics: /** @lends Base */{ + _types: {}, + + 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 @@ -164,6 +179,75 @@ this.Base = Base.inject(/** @lends Base# */{ return res; }, + /** + * Serializes the passed object into a format that can be passed to + * JSON.stringify() for JSON serialization. + */ + serialize: function(obj, compact) { + if (obj && obj._serialize) { + var res = obj._serialize(); + if (!compact && res[0] !== obj._type) + res.unshift(obj._type); + return res; + } + if (typeof obj !== 'object') + return obj; + var res = obj; + if (Array.isArray(obj)) { + res = []; + for (var i = 0, l = obj.length; i < l; i++) + res[i] = Base.serialize(obj[i], true); + } else if (Base.isObject(obj)) { + res = {}; + for (var i in obj) + if (obj.hasOwnProperty(i)) + res[i] = Base.serialize(obj[i], true); + } + return 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) { + var res = obj; + 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 = Base._types[obj[0]]; + 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])); + 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.isObject(obj)) { + res = {}; + for (var key in obj) + res[key] = Base.deserialize(obj[key]); + } + return res; + }, + + toJson: function(obj) { + return JSON.stringify(Base.serialize(obj)); + }, + + fromJson: 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 diff --git a/src/item/Item.js b/src/item/Item.js index 7854aba5..f546cc32 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -25,6 +25,13 @@ * that they inherit from Item. */ var Item = this.Item = Base.extend(Callback, /** @lends Item# */{ + // Provide information about fields to be serialized, with their defaults + // that can be ommited. + _serializeFields: { + name: null, + children: [], + matrix: new Matrix() + }, initialize: function(point) { // Define this Item's unique id. @@ -116,10 +123,8 @@ var Item = this.Item = Base.extend(Callback, /** @lends Item# */{ // one object literal describing all the properties to be set on the created // instance. _setProperties: function(props) { - if (Base.isObject(props)) { - this.set(props); - return true; - } + if (Base.isObject(props)) + return this.set(props); }, /** @@ -133,6 +138,27 @@ var Item = this.Item = Base.extend(Callback, /** @lends Item# */{ return this; }, + _serialize: function() { + var props = {}, + that = this; + + function serialize(fields) { + for (var key in fields) { + var value = that[key]; + if (!Base.equals(value, fields[key])) + props[key] = Base.serialize(value); + } + } + + // Serialize fields that this Item subclass defines first + serialize(this._serializeFields); + // Serialize style fields, but only if they differ from defaults + serialize(this._style._defaults); + // There is no compact form for Item serialization, we always keep the + // type. + return [ this._type, props ]; + }, + /** * Private notifier that is called whenever a change occurs in this item or * its sub-elements, such as Segments, Curves, PathStyles, etc. diff --git a/src/path/Path.js b/src/path/Path.js index 70ca682e..6345de2e 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -24,6 +24,10 @@ // DOCS: Explain that path matrix is always applied with each transformation. var Path = this.Path = PathItem.extend(/** @lends Path# */{ _type: 'path', + _serializeFields: { + segments: [], + closed: false + }, /** * Creates a new Path item and places it at the top of the active layer. diff --git a/src/path/Segment.js b/src/path/Segment.js index 929e5f8b..603d39f0 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -27,6 +27,8 @@ * objects that are connected by this segment. */ var Segment = this.Segment = Base.extend(/** @lends Segment# */{ + _type: 'segment', + /** * Creates a new Segment object. * @@ -86,6 +88,12 @@ var Segment = this.Segment = Base.extend(/** @lends Segment# */{ createPoint(this, '_handleOut', handleOut); }, + _serialize: function() { + return Base.serialize(this._handleIn.isZero() && this._handleOut.isZero() + ? this._point + : [this._point, this._handleIn, this._handleOut], true); + }, + _changed: function(point) { if (!this._path) return;