diff --git a/demos/import-graph/import-graph.js b/demos/import-graph/import-graph.js new file mode 100644 index 0000000..6dcf643 --- /dev/null +++ b/demos/import-graph/import-graph.js @@ -0,0 +1,31 @@ +fetch('zdog.json') + .then(function( res ) { + return res.json(); + }) + .then(function(model) { + // ----- variables ----- // + var sceneSize = 100; + var TAU = Zdog.TAU; + var initRotate = { x: 20/360 * TAU, y: -50/360 * TAU }; + + // ----- model ----- // + var illo = new Zdog.Illustration({ + element: '.illo', + rotate: initRotate, + dragRotate: true, + resize: 'fullscreen', + importGraph: model, + onResize: function( width, height ) { + this.zoom = Math.floor( Math.min( width, height ) * 2 / sceneSize ) / 2; + }, + }); + + // ----- animate ----- // + + function animate() { + illo.updateRenderGraph(); + requestAnimationFrame(animate); + } + + animate(); +}); diff --git a/demos/import-graph/index.html b/demos/import-graph/index.html new file mode 100644 index 0000000..6a1f241 --- /dev/null +++ b/demos/import-graph/index.html @@ -0,0 +1,47 @@ + + + + + + + Import graph + + + + + + +
+ +
+ + + + + + + + + + + + + + + + diff --git a/demos/import-graph/zdog.json b/demos/import-graph/zdog.json new file mode 100644 index 0000000..5533086 --- /dev/null +++ b/demos/import-graph/zdog.json @@ -0,0 +1,797 @@ +{ + "type": "Illustration", + "zoom": 8, + "children": [ + { + "type": "Group", + "children": [ + { + "type": "Group", + "updateSort": true, + "children": [ + { + "type": "Rect", + "rotate": { + "x": 1.5707963267948966 + }, + "translate": { + "y": -20 + }, + "stroke": 8, + "fill": true, + "color": "#E62", + "path": [ + { + "x": -20, + "y": -10 + }, + { + "x": 20, + "y": -10 + }, + { + "x": 20, + "y": 10 + }, + { + "x": -20, + "y": 10 + } + ], + "front": { + "z": 1 + }, + "width": 40, + "height": 20 + }, + { + "type": "Rect", + "rotate": { + "x": -1.5707963267948966 + }, + "translate": { + "y": 20 + }, + "stroke": 8, + "fill": true, + "color": "#E62", + "path": [ + { + "x": -20, + "y": -10 + }, + { + "x": 20, + "y": -10 + }, + { + "x": 20, + "y": 10 + }, + { + "x": -20, + "y": 10 + } + ], + "front": { + "z": 1 + }, + "width": 40, + "height": 20 + }, + { + "type": "Rect", + "rotate": { + "y": 1.5707963267948966 + }, + "translate": { + "x": -20, + "y": -16 + }, + "stroke": 8, + "fill": true, + "color": "#E62", + "path": [ + { + "x": -10, + "y": -4 + }, + { + "x": 10, + "y": -4 + }, + { + "x": 10, + "y": 4 + }, + { + "x": -10, + "y": 4 + } + ], + "front": { + "z": 1 + }, + "backface": false, + "width": 20, + "height": 8 + }, + { + "type": "Rect", + "rotate": { + "y": -1.5707963267948966 + }, + "translate": { + "x": 20, + "y": 16 + }, + "stroke": 8, + "fill": true, + "color": "#E62", + "path": [ + { + "x": -10, + "y": -4 + }, + { + "x": 10, + "y": -4 + }, + { + "x": 10, + "y": 4 + }, + { + "x": -10, + "y": 4 + } + ], + "front": { + "z": 1 + }, + "backface": false, + "width": 20, + "height": 8 + }, + { + "type": "Rect", + "rotate": { + "y": 1.5707963267948966 + }, + "translate": { + "x": -20, + "y": 15 + }, + "stroke": 8, + "fill": true, + "color": "#E62", + "path": [ + { + "x": -10, + "y": -5 + }, + { + "x": 10, + "y": -5 + }, + { + "x": 10, + "y": 5 + }, + { + "x": -10, + "y": 5 + } + ], + "front": { + "z": 1 + }, + "backface": false, + "width": 20, + "height": 10 + }, + { + "type": "Rect", + "rotate": { + "y": -1.5707963267948966 + }, + "translate": { + "x": 20, + "y": -15 + }, + "stroke": 8, + "fill": true, + "color": "#E62", + "path": [ + { + "x": -10, + "y": -5 + }, + { + "x": 10, + "y": -5 + }, + { + "x": 10, + "y": 5 + }, + { + "x": -10, + "y": 5 + } + ], + "front": { + "z": 1 + }, + "backface": false, + "width": 20, + "height": 10 + }, + { + "type": "Rect", + "rotate": { + "x": -1.5707963267948966 + }, + "translate": { + "x": -5, + "y": -12 + }, + "stroke": 8, + "fill": true, + "color": "#E62", + "path": [ + { + "x": -15, + "y": -10 + }, + { + "x": 15, + "y": -10 + }, + { + "x": 15, + "y": 10 + }, + { + "x": -15, + "y": 10 + } + ], + "front": { + "z": 1 + }, + "width": 30, + "height": 20 + }, + { + "type": "Rect", + "rotate": { + "x": 1.5707963267948966 + }, + "translate": { + "x": 5, + "y": 12 + }, + "stroke": 8, + "fill": true, + "color": "#E62", + "path": [ + { + "x": -15, + "y": -10 + }, + { + "x": 15, + "y": -10 + }, + { + "x": 15, + "y": 10 + }, + { + "x": -15, + "y": 10 + } + ], + "front": { + "z": 1 + }, + "width": 30, + "height": 20 + }, + { + "type": "Rect", + "rotate": { + "x": 1.5707963267948966, + "y": 0.6327488350021832 + }, + "translate": { + "x": -5, + "y": -1 + }, + "stroke": 8, + "fill": true, + "color": "#E62", + "path": [ + { + "x": -18.601075237738275, + "y": -10 + }, + { + "x": 18.601075237738275, + "y": -10 + }, + { + "x": 18.601075237738275, + "y": 10 + }, + { + "x": -18.601075237738275, + "y": 10 + } + ], + "front": { + "z": 1 + }, + "backface": false, + "width": 37.20215047547655, + "height": 20 + }, + { + "type": "Rect", + "rotate": { + "x": -1.5707963267948966, + "y": -0.6327488350021832 + }, + "translate": { + "x": 5, + "y": 1 + }, + "stroke": 8, + "fill": true, + "color": "#E62", + "path": [ + { + "x": -18.601075237738275, + "y": -10 + }, + { + "x": 18.601075237738275, + "y": -10 + }, + { + "x": 18.601075237738275, + "y": 10 + }, + { + "x": -18.601075237738275, + "y": 10 + } + ], + "front": { + "z": 1 + }, + "backface": false, + "width": 37.20215047547655, + "height": 20 + }, + { + "type": "Ellipse", + "rotate": { + "z": 1.5707963267948966 + }, + "translate": { + "x": 22, + "y": -4 + }, + "stroke": 8, + "color": "#E62", + "path": [ + { + "x": 0, + "y": -16 + }, + { + "arc": [ + { + "x": 16, + "y": -16 + }, + { + "x": 16, + "y": 0 + } + ] + } + ], + "front": { + "z": 1 + }, + "diameter": 32, + "quarters": 1 + }, + { + "type": "Anchor", + "rotate": { + "y": 1.5707963267948966 + }, + "translate": { + "x": -6, + "y": -7 + }, + "children": [ + { + "type": "Shape", + "rotate": { + "x": 0.9420000403794636 + }, + "stroke": 4, + "fill": true, + "color": "#636", + "path": [ + { + "x": -5, + "y": 0 + }, + { + "x": 5, + "y": 0 + }, + { + "x": 5, + "y": 12 + }, + { + "arc": [ + { + "x": 5, + "y": 17 + }, + { + "x": 0, + "y": 17 + } + ] + }, + { + "arc": [ + { + "x": -5, + "y": 17 + }, + { + "x": -5, + "y": 12 + } + ] + } + ], + "front": { + "z": 1 + } + } + ] + }, + { + "type": "Ellipse", + "rotate": { + "y": 1.5707963267948966, + "z": 1.5707963267948966 + }, + "translate": { + "x": -26, + "y": -20 + }, + "scale": 8, + "stroke": 5, + "fill": true, + "color": "#636", + "closed": true, + "path": [ + { + "x": 0, + "y": -0.5 + }, + { + "arc": [ + { + "x": 0.5, + "y": -0.5 + }, + { + "x": 0.5, + "y": 0 + } + ] + }, + { + "arc": [ + { + "x": 0.5, + "y": 0.5 + }, + { + "x": 0, + "y": 0.5 + } + ] + } + ], + "front": { + "z": 1 + }, + "quarters": 2 + } + ] + }, + { + "type": "Group", + "updateSort": true, + "children": [ + { + "type": "Shape", + "translate": { + "z": 10 + }, + "stroke": 8, + "fill": true, + "color": "#EA0", + "path": [ + { + "x": -20, + "y": -20 + }, + { + "x": 20, + "y": -20 + }, + { + "x": 20, + "y": -10 + }, + { + "x": -10, + "y": 12 + }, + { + "x": 20, + "y": 12 + }, + { + "x": 20, + "y": 20 + }, + { + "x": -20, + "y": 20 + }, + { + "x": -20, + "y": 10 + }, + { + "x": 10, + "y": -12 + }, + { + "x": -20, + "y": -12 + } + ], + "front": { + "z": 1 + }, + "backface": false + }, + { + "type": "Shape", + "rotate": { + "y": 3.141592653589793 + }, + "translate": { + "z": -10 + }, + "scale": { + "x": -1, + "y": 1, + "z": 1 + }, + "stroke": 8, + "fill": true, + "color": "#EA0", + "path": [ + { + "x": -20, + "y": -20 + }, + { + "x": 20, + "y": -20 + }, + { + "x": 20, + "y": -10 + }, + { + "x": -10, + "y": 12 + }, + { + "x": 20, + "y": 12 + }, + { + "x": 20, + "y": 20 + }, + { + "x": -20, + "y": 20 + }, + { + "x": -20, + "y": 10 + }, + { + "x": 10, + "y": -12 + }, + { + "x": -20, + "y": -12 + } + ], + "front": { + "z": 1 + }, + "backface": false + } + ] + } + ] + }, + { + "type": "Group", + "children": [ + { + "type": "Ellipse", + "rotate": { + "x": 0.39269908169872414, + "z": -0.39269908169872414 + }, + "translate": { + "x": 10, + "y": -14, + "z": 20 + }, + "scale": 24, + "stroke": 5, + "fill": true, + "color": "#636", + "closed": true, + "path": [ + { + "x": 0, + "y": -0.5 + }, + { + "arc": [ + { + "x": 0.5, + "y": -0.5 + }, + { + "x": 0.5, + "y": 0 + } + ] + }, + { + "arc": [ + { + "x": 0.5, + "y": 0.5 + }, + { + "x": 0, + "y": 0.5 + } + ] + } + ], + "front": { + "z": 1 + }, + "quarters": 2, + "children": [ + { + "type": "Shape", + "translate": { + "x": -0.5, + "z": 0.5 + }, + "visible": false, + "front": { + "z": 1 + } + } + ] + } + ] + }, + { + "type": "Group", + "scale": { + "x": 1, + "y": 1, + "z": -1 + }, + "children": [ + { + "type": "Ellipse", + "rotate": { + "x": 0.39269908169872414, + "z": -0.39269908169872414 + }, + "translate": { + "x": 10, + "y": -14, + "z": 20 + }, + "scale": 24, + "stroke": 5, + "fill": true, + "color": "#636", + "closed": true, + "path": [ + { + "x": 0, + "y": -0.5 + }, + { + "arc": [ + { + "x": 0.5, + "y": -0.5 + }, + { + "x": 0.5, + "y": 0 + } + ] + }, + { + "arc": [ + { + "x": 0.5, + "y": 0.5 + }, + { + "x": 0, + "y": 0.5 + } + ] + } + ], + "front": { + "z": 1 + }, + "quarters": 2, + "children": [ + { + "type": "Shape", + "translate": { + "x": -0.5, + "z": 0.5 + }, + "visible": false, + "front": { + "z": 1 + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/js/anchor.js b/js/anchor.js index 0744810..4926693 100644 --- a/js/anchor.js +++ b/js/anchor.js @@ -22,6 +22,7 @@ var onePoint = { x: 1, y: 1, z: 1 }; function Anchor( options ) { this.create( options || {} ); } +Anchor.type = 'Anchor'; Anchor.prototype.create = function( options ) { // set defaults & options @@ -40,6 +41,9 @@ Anchor.prototype.create = function( options ) { if ( this.addTo ) { this.addTo.addChild( this ); } + if ( options.importGraph ) { + this.importGraph( options.importGraph ); + } }; Anchor.defaults = {}; @@ -51,11 +55,15 @@ Anchor.optionKeys = Object.keys( Anchor.defaults ).concat([ 'addTo', ]); +Anchor.ignoreKeysJSON = [ + 'addTo', +]; + Anchor.prototype.setOptions = function( options ) { var optionKeys = this.constructor.optionKeys; for ( var key in options ) { - if ( optionKeys.includes( key ) ) { + if ( optionKeys.indexOf( key ) > -1 ) { this[ key ] = options[ key ]; } } @@ -207,12 +215,73 @@ Anchor.prototype.copyGraph = function( options ) { return clone; }; +Anchor.prototype.importGraph = function( model ) { + this.addChild( revive( model ) ); + + function revive( graph ) { + graph = utils.extend( {}, graph ); + // quick hack to avoid nested Illustration items + var type = graph.type === 'Illustration' ? 'Anchor' : graph.type; + var children = (graph.children || []).slice( 0 ); + delete graph.children; + + var Item = utils[ type ]; + var rootGraph; + if ( Item ) { + rootGraph = new Item( graph ); + children.forEach( function( child ) { + revive( utils.extend( child, { addTo: rootGraph } ) ); + } ); + } + return rootGraph; + } +}; + Anchor.prototype.normalizeRotate = function() { this.rotate.x = utils.modulo( this.rotate.x, TAU ); this.rotate.y = utils.modulo( this.rotate.y, TAU ); this.rotate.z = utils.modulo( this.rotate.z, TAU ); }; +Anchor.prototype.toJSON = function() { + var type = this.constructor.type; + var result = { type: type }; + var defaults = this.constructor.defaults; + var optionKeys = this.constructor.optionKeys.slice(0).concat('children'); + var ignoreKeys = Anchor.ignoreKeysJSON + .slice(0) + .concat(this.constructor.ignoreKeysJSON || []); + + optionKeys.forEach(function( key ) { + if (ignoreKeys.indexOf(key) > -1) { + return; + } + var value = this[key]; + + if ( + ![ 'undefined', 'function' ].indexOf(typeof value) > -1 && + value !== defaults[key] + ) { + if (Array.isArray(value) && value.length === 0) { + return; + } + if (value.toJSON) { + var serialized = value.toJSON(); + if (typeof serialized !== 'undefined') { + if (key === 'scale' && serialized === 1) { + return; + } + result[key] = serialized; + } + } else { + result[key] = value; + } + } + }, this); + + return result; +}; + // ----- subclass ----- // function getSubclass( Super ) { @@ -231,10 +300,12 @@ function getSubclass( Super ) { Item.optionKeys = Super.optionKeys.slice(0); // add defaults keys to optionKeys, dedupe Object.keys( Item.defaults ).forEach( function( key ) { - if ( !Item.optionKeys.includes( key ) ) { + if ( !Item.optionKeys.indexOf( key ) > -1 ) { Item.optionKeys.push( key ); } }); + // create ignoreKeysJSON + Item.ignoreKeysJSON = Super.ignoreKeysJSON.slice(0); Item.subclass = getSubclass( Item ); diff --git a/js/boilerplate.js b/js/boilerplate.js index a22ddc9..413c0fd 100644 --- a/js/boilerplate.js +++ b/js/boilerplate.js @@ -70,6 +70,10 @@ Zdog.easeInOut = function( alpha, power ) { return isFirstHalf ? curve : 1 - curve; }; +Zdog.exportGraph = function(model) { + return JSON.parse( JSON.stringify( model ) ); +}; + return Zdog; })); diff --git a/js/box.js b/js/box.js index 4f8735f..e12ce3e 100644 --- a/js/box.js +++ b/js/box.js @@ -18,9 +18,10 @@ // ----- BoxRect ----- // var BoxRect = Rect.subclass(); + // prevent double-creation in parent.copyGraph() // only create in Box.create() -BoxRect.prototype.copyGraph = function() {}; +BoxRect.prototype.copyGraph = function() { }; // ----- Box ----- // @@ -38,8 +39,9 @@ var boxDefaults = utils.extend( { // default fill boxDefaults.fill = true; delete boxDefaults.path; - var Box = Anchor.subclass( boxDefaults ); +Box.ignoreKeysJSON = [ 'children' ]; +Box.type = 'Box'; var TAU = utils.TAU; diff --git a/js/cone.js b/js/cone.js index 4535e5a..73a63a8 100644 --- a/js/cone.js +++ b/js/cone.js @@ -20,6 +20,7 @@ var Cone = Ellipse.subclass({ length: 1, fill: true, }); +Cone.type = 'Cone'; var TAU = utils.TAU; diff --git a/js/cylinder.js b/js/cylinder.js index cc2a4f0..d1b9979 100644 --- a/js/cylinder.js +++ b/js/cylinder.js @@ -26,6 +26,8 @@ var CylinderGroup = Group.subclass({ updateSort: true, }); +CylinderGroup.type = 'CylinderGroup'; + CylinderGroup.prototype.create = function() { Group.prototype.create.apply( this, arguments ); this.pathCommands = [ @@ -97,6 +99,9 @@ var Cylinder = Shape.subclass({ fill: true, }); +Cylinder.type = 'Cylinder'; + + var TAU = utils.TAU; Cylinder.prototype.create = function(/* options */) { diff --git a/js/ellipse.js b/js/ellipse.js index fafbe98..def1a05 100644 --- a/js/ellipse.js +++ b/js/ellipse.js @@ -23,6 +23,8 @@ var Ellipse = Shape.subclass({ closed: false, }); +Ellipse.type = 'Ellipse'; + Ellipse.prototype.setPath = function() { var width = this.width != undefined ? this.width : this.diameter; var height = this.height != undefined ? this.height : this.diameter; diff --git a/js/group.js b/js/group.js index ffafb84..746e2a6 100644 --- a/js/group.js +++ b/js/group.js @@ -19,6 +19,9 @@ var Group = Anchor.subclass({ visible: true, }); +Group.type = 'Group'; + + // ----- update ----- // Group.prototype.updateSortValue = function() { diff --git a/js/hemisphere.js b/js/hemisphere.js index fa92c4e..abaeb00 100644 --- a/js/hemisphere.js +++ b/js/hemisphere.js @@ -18,6 +18,8 @@ var Hemisphere = Ellipse.subclass({ fill: true, }); +Hemisphere.type = 'Hemisphere'; + var TAU = utils.TAU; Hemisphere.prototype.render = function( ctx, renderer ) { diff --git a/js/illustration.js b/js/illustration.js index a8751bb..f433bf3 100644 --- a/js/illustration.js +++ b/js/illustration.js @@ -30,6 +30,8 @@ var Illustration = Anchor.subclass({ onDragEnd: noop, onResize: noop, }); +Illustration.ignoreKeysJSON = [ 'dragRotate', 'element', 'resize' ]; +Illustration.type = 'Illustration'; utils.extend( Illustration.prototype, Dragger.prototype ); diff --git a/js/polygon.js b/js/polygon.js index a3ea770..e7da3f1 100644 --- a/js/polygon.js +++ b/js/polygon.js @@ -19,6 +19,8 @@ var Polygon = Shape.subclass({ radius: 0.5, }); +Polygon.type = 'Polygon'; + var TAU = utils.TAU; Polygon.prototype.setPath = function() { diff --git a/js/rect.js b/js/rect.js index 9e69542..ffedaa9 100644 --- a/js/rect.js +++ b/js/rect.js @@ -19,6 +19,8 @@ var Rect = Shape.subclass({ height: 1, }); +Rect.type = 'Rect'; + Rect.prototype.setPath = function() { var x = this.width / 2; var y = this.height / 2; diff --git a/js/rounded-rect.js b/js/rounded-rect.js index 30c8a3f..28316e3 100644 --- a/js/rounded-rect.js +++ b/js/rounded-rect.js @@ -21,6 +21,8 @@ var RoundedRect = Shape.subclass({ closed: false, }); +RoundedRect.type = 'RoundedRect'; + RoundedRect.prototype.setPath = function() { /* eslint id-length: [ "error", { "min": 2, "exceptions": [ "x", "y" ] }], diff --git a/js/shape.js b/js/shape.js index 40c927c..673289b 100644 --- a/js/shape.js +++ b/js/shape.js @@ -26,6 +26,8 @@ var Shape = Anchor.subclass({ backface: true, }); +Shape.type = 'Shape'; + Shape.prototype.create = function( options ) { Anchor.prototype.create.call( this, options ); this.updatePath(); @@ -60,7 +62,7 @@ Shape.prototype.updatePathCommands = function() { var method = keys[0]; var points = pathPart[ method ]; // default to line if no instruction - var isInstruction = keys.length == 1 && actionNames.includes( method ); + var isInstruction = keys.length == 1 && actionNames.indexOf( method ) > -1; if ( !isInstruction ) { method = 'line'; points = pathPart; diff --git a/js/vector.js b/js/vector.js index b896526..2d1f6d5 100644 --- a/js/vector.js +++ b/js/vector.js @@ -149,6 +149,32 @@ Vector.prototype.copy = function() { return new Vector( this ); }; +function round( num ) { + return Math.round( num * 1000 ) / 1000; +} + +Vector.prototype.toJSON = function() { + var x = this.x; + var y = this.y; + var z = this.z; + + if ( x === y && y === z ) { + return x !== 0 ? round( x ) : undefined; + } + + var obj = { x: x, y: y, z: z }; + var result = {}; + + Object.keys( obj ).forEach( function( key ) { + var value = obj[ key ]; + if ( value !== 0 ) { + result[ key ] = round( value ); + } + }); + + return Object.keys( result ).length ? result : undefined; +}; + return Vector; }));