diff --git a/.github/contributing.md b/.github/contributing.md
index 252686d..e77fb8d 100644
--- a/.github/contributing.md
+++ b/.github/contributing.md
@@ -2,11 +2,9 @@
**Add 👍 reaction** to issues for features you would like to see added to Zdog. Do not add +1 comments — [they will be deleted](https://metafizzy.co/blog/use-github-reactions-delete-plus-1-comments/).
-## Submitting issues
+## Reduced test cases required
-### Reduced test case required
-
-All bug reports and problem issues require a [**reduced test case**](https://css-tricks.com/reduced-test-cases/).
+All bug reports and problem issues require a [**reduced test case**](https://css-tricks.com/reduced-test-cases/). Create one by [forking this CodePen](https://codepen.io/desandro/pen/xNLWwG).
+ A reduced test case clearly demonstrates the bug or issue.
+ It contains the bare minimum HTML, CSS, and JavaScript required to demonstrate the bug.
diff --git a/.github/issue_template.md b/.github/issue_template.md
index d4302da..7735e4b 100644
--- a/.github/issue_template.md
+++ b/.github/issue_template.md
@@ -1,3 +1,3 @@
-**Test case:** https://codepen.io/desandro/pen/azqbop
+**Test case:** https://codepen.io/desandro/pen/xNLWwG
diff --git a/Makefile b/Makefile
index 30226c5..882df72 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,9 @@
bundle:
cat js/boilerplate.js js/canvas-renderer.js js/svg-renderer.js js/vector.js \
- js/anchor.js js/path-command.js js/shape.js js/group.js js/rect.js \
- js/rounded-rect.js js/ellipse.js js/polygon.js js/hemisphere.js \
- js/cylinder.js js/cone.js js/box.js > dist/zdog.dist.js
+ js/anchor.js js/dragger.js js/illustration.js js/path-command.js \
+ js/shape.js js/group.js js/rect.js js/rounded-rect.js js/ellipse.js \
+ js/polygon.js js/hemisphere.js js/cylinder.js js/cone.js js/box.js \
+ js/index.js > dist/zdog.dist.js
uglify:
npx uglifyjs dist/zdog.dist.js -o dist/zdog.dist.min.js --mangle --comments /^!/
@@ -10,4 +11,4 @@ uglify:
lint:
npx jshint js/*.js demos/**/*.js
-dist: hint bundle uglify
+dist: lint bundle uglify
diff --git a/README.md b/README.md
index 7103174..7d977e9 100644
--- a/README.md
+++ b/README.md
@@ -8,15 +8,15 @@ View complete documentation and live demos at [zzz.dog](https://zzz.dog).
### Download
-+ [zdog.dist.min.js](https://unpkg.com/zdog@0/dist/zdog.dist.min.js) minified, or
-+ [zdog.dist.js](https://unpkg.com/zdog@0/dist/zdog.dist.js) un-minified
++ [zdog.dist.min.js](https://unpkg.com/zdog@1/dist/zdog.dist.min.js) minified, or
++ [zdog.dist.js](https://unpkg.com/zdog@1/dist/zdog.dist.js) un-minified
### CDN
Link directly to Zdog JS on [unpkg](https://unpkg.com).
``` html
-
+
```
### Package managers
@@ -86,7 +86,7 @@ Zdog v1 is a beta-release, of sorts. This is my first time creating a 3D engine,
### Other Zdog repos
-+ [zdog-demos](https://github.com/metafizzy/zdog-demos) - Lots more bigger, wilder Zdog demos
++ [zdog-demos](https://github.com/metafizzy/zdog-demos) - More, bigger, wilder Zdog demos
+ [zdog-docs](https://github.com/metafizzy/zdog-docs) - Documentation site source code for [zzz.dog](https://zzz.dog)
---
diff --git a/dist/zdog.dist.js b/dist/zdog.dist.js
new file mode 100644
index 0000000..9704168
--- /dev/null
+++ b/dist/zdog.dist.js
@@ -0,0 +1,2096 @@
+/*!
+ * Zdog v1.0.0
+ * Round, flat, designer-friendly pseudo-3D engine
+ * Licensed MIT
+ * https://zzz.dog
+ * Copyright 2019 Metafizzy
+ */
+
+/**
+ * Boilerplate & utils
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module */ // CommonJS
+ module.exports = factory();
+ } else {
+ // browser global
+ root.Zdog = factory();
+ }
+}( this, function factory() {
+
+var Zdog = {};
+
+Zdog.TAU = Math.PI * 2;
+
+Zdog.extend = function( a, b ) {
+ for ( var prop in b ) {
+ a[ prop ] = b[ prop ];
+ }
+ return a;
+};
+
+Zdog.lerp = function( a, b, t ) {
+ return ( b - a ) * t + a;
+};
+
+Zdog.modulo = function( num, div ) {
+ return ( ( num % div ) + div ) % div;
+};
+
+var powerMultipliers = {
+ 2: function( a ) {
+ return a * a;
+ },
+ 3: function( a ) {
+ return a * a * a;
+ },
+ 4: function( a ) {
+ return a * a * a * a;
+ },
+ 5: function( a ) {
+ return a * a * a * a * a;
+ }
+};
+
+Zdog.easeInOut = function( alpha, power ) {
+ if ( power == 1 ) {
+ return alpha;
+ }
+ alpha = Math.max( 0, Math.min( 1, alpha ) );
+ var isFirstHalf = alpha < 0.5;
+ var slope = isFirstHalf ? alpha : 1 - alpha;
+ slope = slope / 0.5;
+ // make easing steeper with more multiples
+ var powerMultiplier = powerMultipliers[ power ] || powerMultipliers[2];
+ var curve = powerMultiplier( slope );
+ curve = curve / 2;
+ return isFirstHalf ? curve : 1 - curve;
+};
+
+return Zdog;
+
+}));
+/**
+ * CanvasRenderer
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module */ // CommonJS
+ module.exports = factory();
+ } else {
+ // browser global
+ root.Zdog.CanvasRenderer = factory();
+ }
+}( this, function factory() {
+
+var CanvasRenderer = { isCanvas: true };
+
+CanvasRenderer.begin = function( ctx ) {
+ ctx.beginPath();
+};
+
+CanvasRenderer.move = function( ctx, elem, point ) {
+ ctx.moveTo( point.x, point.y );
+};
+
+CanvasRenderer.line = function( ctx, elem, point ) {
+ ctx.lineTo( point.x, point.y );
+};
+
+CanvasRenderer.bezier = function( ctx, elem, cp0, cp1, end ) {
+ ctx.bezierCurveTo( cp0.x, cp0.y, cp1.x, cp1.y, end.x, end.y );
+};
+
+CanvasRenderer.closePath = function( ctx ) {
+ ctx.closePath();
+};
+
+CanvasRenderer.setPath = function() {};
+
+CanvasRenderer.renderPath = function( ctx, elem, pathCommands, isClosed ) {
+ this.begin( ctx, elem );
+ pathCommands.forEach( function( command ) {
+ command.render( ctx, elem, CanvasRenderer );
+ });
+ if ( isClosed ) {
+ this.closePath( ctx, elem );
+ }
+};
+
+CanvasRenderer.stroke = function( ctx, elem, isStroke, color, lineWidth ) {
+ if ( !isStroke ) {
+ return;
+ }
+ ctx.strokeStyle = color;
+ ctx.lineWidth = lineWidth;
+ ctx.stroke();
+};
+
+CanvasRenderer.fill = function( ctx, elem, isFill, color ) {
+ if ( !isFill ) {
+ return;
+ }
+ ctx.fillStyle = color;
+ ctx.fill();
+};
+
+CanvasRenderer.end = function() {};
+
+return CanvasRenderer;
+
+}));
+/**
+ * SvgRenderer
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module */ // CommonJS
+ module.exports = factory();
+ } else {
+ // browser global
+ root.Zdog.SvgRenderer = factory();
+ }
+}( this, function factory() {
+
+var SvgRenderer = { isSvg: true };
+
+// round path coordinates to 3 decimals
+var round = SvgRenderer.round = function( num ) {
+ return Math.round( num * 1000 ) / 1000;
+};
+
+function getPointString( point ) {
+ return round( point.x ) + ',' + round( point.y ) + ' ';
+}
+
+SvgRenderer.begin = function() {};
+
+SvgRenderer.move = function( svg, elem, point ) {
+ return 'M' + getPointString( point );
+};
+
+SvgRenderer.line = function( svg, elem, point ) {
+ return 'L' + getPointString( point );
+};
+
+SvgRenderer.bezier = function( svg, elem, cp0, cp1, end ) {
+ return 'C' + getPointString( cp0 ) + getPointString( cp1 ) +
+ getPointString( end );
+};
+
+SvgRenderer.closePath = function(/* elem */) {
+ return 'Z';
+};
+
+SvgRenderer.setPath = function( svg, elem, pathValue ) {
+ elem.setAttribute( 'd', pathValue );
+};
+
+SvgRenderer.renderPath = function( svg, elem, pathCommands, isClosed ) {
+ var pathValue = '';
+ pathCommands.forEach( function( command ) {
+ pathValue += command.render( svg, elem, SvgRenderer );
+ });
+ if ( isClosed ) {
+ pathValue += this.closePath( svg, elem );
+ }
+ this.setPath( svg, elem, pathValue );
+};
+
+SvgRenderer.stroke = function( svg, elem, isStroke, color, lineWidth ) {
+ if ( !isStroke ) {
+ return;
+ }
+ elem.setAttribute( 'stroke', color );
+ elem.setAttribute( 'stroke-width', lineWidth );
+};
+
+SvgRenderer.fill = function( svg, elem, isFill, color ) {
+ var fillColor = isFill ? color : 'none';
+ elem.setAttribute( 'fill', fillColor );
+};
+
+SvgRenderer.end = function( svg, elem ) {
+ svg.appendChild( elem );
+};
+
+return SvgRenderer;
+
+}));
+/**
+ * Vector
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./boilerplate') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Vector = factory( Zdog );
+ }
+
+}( this, function factory( utils ) {
+
+function Vector( position ) {
+ this.set( position );
+}
+
+var TAU = utils.TAU;
+
+// 'pos' = 'position'
+Vector.prototype.set = function( pos ) {
+ this.x = pos && pos.x || 0;
+ this.y = pos && pos.y || 0;
+ this.z = pos && pos.z || 0;
+ return this;
+};
+
+// set coordinates without sanitizing
+// vec.write({ y: 2 }) only sets y coord
+Vector.prototype.write = function( pos ) {
+ if ( !pos ) {
+ return this;
+ }
+ this.x = pos.x != undefined ? pos.x : this.x;
+ this.y = pos.y != undefined ? pos.y : this.y;
+ this.z = pos.z != undefined ? pos.z : this.z;
+ return this;
+};
+
+Vector.prototype.rotate = function( rotation ) {
+ if ( !rotation ) {
+ return;
+ }
+ this.rotateZ( rotation.z );
+ this.rotateY( rotation.y );
+ this.rotateX( rotation.x );
+ return this;
+};
+
+Vector.prototype.rotateZ = function( angle ) {
+ rotateProperty( this, angle, 'x', 'y' );
+};
+
+Vector.prototype.rotateX = function( angle ) {
+ rotateProperty( this, angle, 'y', 'z' );
+};
+
+Vector.prototype.rotateY = function( angle ) {
+ rotateProperty( this, angle, 'x', 'z' );
+};
+
+function rotateProperty( vec, angle, propA, propB ) {
+ if ( !angle || angle % TAU === 0 ) {
+ return;
+ }
+ var cos = Math.cos( angle );
+ var sin = Math.sin( angle );
+ var a = vec[ propA ];
+ var b = vec[ propB ];
+ vec[ propA ] = a*cos - b*sin;
+ vec[ propB ] = b*cos + a*sin;
+}
+
+Vector.prototype.add = function( pos ) {
+ if ( !pos ) {
+ return this;
+ }
+ this.x += pos.x || 0;
+ this.y += pos.y || 0;
+ this.z += pos.z || 0;
+ return this;
+};
+
+Vector.prototype.subtract = function( pos ) {
+ if ( !pos ) {
+ return this;
+ }
+ this.x -= pos.x || 0;
+ this.y -= pos.y || 0;
+ this.z -= pos.z || 0;
+ return this;
+};
+
+Vector.prototype.multiply = function( pos ) {
+ if ( pos == undefined ) {
+ return this;
+ }
+ // multiple all values by same number
+ if ( typeof pos == 'number' ) {
+ this.x *= pos;
+ this.y *= pos;
+ this.z *= pos;
+ } else {
+ // multiply object
+ this.x *= pos.x != undefined ? pos.x : 1;
+ this.y *= pos.y != undefined ? pos.y : 1;
+ this.z *= pos.z != undefined ? pos.z : 1;
+ }
+ return this;
+};
+
+Vector.prototype.transform = function( translation, rotation, scale ) {
+ this.multiply( scale );
+ this.rotate( rotation );
+ this.add( translation );
+ return this;
+};
+
+Vector.prototype.lerp = function( pos, t ) {
+ this.x = utils.lerp( this.x, pos.x || 0, t );
+ this.y = utils.lerp( this.y, pos.y || 0, t );
+ this.z = utils.lerp( this.z, pos.z || 0, t );
+ return this;
+};
+
+Vector.prototype.magnitude = function() {
+ var sum = this.x*this.x + this.y*this.y + this.z*this.z;
+ return getMagnitudeSqrt( sum );
+};
+
+function getMagnitudeSqrt( sum ) {
+ // PERF: check if sum ~= 1 and skip sqrt
+ if ( Math.abs( sum - 1 ) < 0.00000001 ) {
+ return 1;
+ }
+ return Math.sqrt( sum );
+}
+
+Vector.prototype.magnitude2d = function() {
+ var sum = this.x*this.x + this.y*this.y;
+ return getMagnitudeSqrt( sum );
+};
+
+Vector.prototype.copy = function() {
+ return new Vector( this );
+};
+
+return Vector;
+
+}));
+/**
+ * Anchor
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./boilerplate'), require('./vector') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Anchor = factory( Zdog, Zdog.Vector );
+ }
+}( this, function factory( utils, Vector ) {
+
+var TAU = utils.TAU;
+var onePoint = { x: 1, y: 1, z: 1 };
+
+function Anchor( options ) {
+ this.create( options || {} );
+}
+
+Anchor.prototype.create = function( options ) {
+ // set defaults & options
+ utils.extend( this, this.constructor.defaults );
+ this.setOptions( options );
+
+ // transform
+ this.translate = new Vector( options.translate );
+ this.rotate = new Vector( options.rotate );
+ this.scale = new Vector( onePoint ).multiply( this.scale );
+ // origin
+ this.origin = new Vector();
+ this.renderOrigin = new Vector();
+ // children
+ this.children = [];
+ if ( this.addTo ) {
+ this.addTo.addChild( this );
+ }
+};
+
+Anchor.defaults = {};
+
+Anchor.optionKeys = Object.keys( Anchor.defaults ).concat([
+ 'rotate',
+ 'translate',
+ 'scale',
+ 'addTo',
+]);
+
+Anchor.prototype.setOptions = function( options ) {
+ var optionKeys = this.constructor.optionKeys;
+
+ for ( var key in options ) {
+ if ( optionKeys.includes( key ) ) {
+ this[ key ] = options[ key ];
+ }
+ }
+};
+
+Anchor.prototype.addChild = function( shape ) {
+ var index = this.children.indexOf( shape );
+ if ( index != -1 ) {
+ return;
+ }
+ shape.remove(); // remove previous parent
+ shape.addTo = this; // keep parent reference
+ this.children.push( shape );
+};
+
+Anchor.prototype.removeChild = function( shape ) {
+ var index = this.children.indexOf( shape );
+ if ( index != -1 ) {
+ this.children.splice( index, 1 );
+ }
+};
+
+Anchor.prototype.remove = function() {
+ if ( this.addTo ) {
+ this.addTo.removeChild( this );
+ }
+};
+
+// ----- update ----- //
+
+Anchor.prototype.update = function() {
+ // update self
+ this.reset();
+ // update children
+ this.children.forEach( function( child ) {
+ child.update();
+ });
+ this.transform( this.translate, this.rotate, this.scale );
+};
+
+Anchor.prototype.reset = function() {
+ this.renderOrigin.set( this.origin );
+};
+
+Anchor.prototype.transform = function( translation, rotation, scale ) {
+ this.renderOrigin.transform( translation, rotation, scale );
+ // transform children
+ this.children.forEach( function( child ) {
+ child.transform( translation, rotation, scale );
+ });
+};
+
+Anchor.prototype.updateGraph = function() {
+ this.update();
+ this.checkFlatGraph();
+ this.flatGraph.forEach( function( item ) {
+ item.updateSortValue();
+ });
+ // z-sort
+ this.flatGraph.sort( Anchor.shapeSorter );
+};
+
+Anchor.shapeSorter = function( a, b ) {
+ return a.sortValue - b.sortValue;
+};
+
+Anchor.prototype.checkFlatGraph = function() {
+ if ( !this.flatGraph ) {
+ this.updateFlatGraph();
+ }
+};
+
+Anchor.prototype.updateFlatGraph = function() {
+ this.flatGraph = this.getFlatGraph();
+};
+
+// return Array of self & all child graph items
+Anchor.prototype.getFlatGraph = function() {
+ var flatGraph = [ this ];
+ this.children.forEach( function( child ) {
+ var childFlatGraph = child.getFlatGraph();
+ flatGraph = flatGraph.concat( childFlatGraph );
+ });
+ return flatGraph;
+};
+
+Anchor.prototype.updateSortValue = function() {
+ this.sortValue = this.renderOrigin.z;
+};
+
+// ----- render ----- //
+
+Anchor.prototype.render = function() {};
+
+Anchor.prototype.renderGraphCanvas = function( ctx ) {
+ if ( !ctx ) {
+ throw new Error( 'ctx is ' + ctx + '. ' +
+ 'Canvas context required for render. Check .renderGraphCanvas( ctx ).' );
+ }
+ this.checkFlatGraph();
+ this.flatGraph.forEach( function( item ) {
+ item.render( ctx, Zdog.CanvasRenderer );
+ });
+};
+
+Anchor.prototype.renderGraphSvg = function( svg ) {
+ if ( !svg ) {
+ throw new Error( 'svg is ' + svg + '. ' +
+ 'SVG required for render. Check .renderGraphSvg( svg ).' );
+ }
+ this.checkFlatGraph();
+ this.flatGraph.forEach( function( item ) {
+ item.render( svg, Zdog.SvgRenderer );
+ });
+};
+
+// ----- misc ----- //
+
+Anchor.prototype.copy = function( options ) {
+ // copy options
+ var itemOptions = {};
+ var optionKeys = this.constructor.optionKeys;
+ optionKeys.forEach( function( key ) {
+ itemOptions[ key ] = this[ key ];
+ }, this );
+ // add set options
+ utils.extend( itemOptions, options );
+ var ItemClass = this.constructor;
+ return new ItemClass( itemOptions );
+};
+
+Anchor.prototype.copyGraph = function( options ) {
+ var clone = this.copy( options );
+ this.children.forEach( function( child ) {
+ child.copyGraph({
+ addTo: clone,
+ });
+ });
+ return clone;
+};
+
+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 );
+};
+
+// ----- subclass ----- //
+
+function getSubclass( Super ) {
+ return function( defaults ) {
+ // create constructor
+ function Item( options ) {
+ this.create( options || {} );
+ }
+
+ Item.prototype = Object.create( Super.prototype );
+ Item.prototype.constructor = Item;
+
+ Item.defaults = utils.extend( {}, Super.defaults );
+ utils.extend( Item.defaults, defaults );
+ // create optionKeys
+ Item.optionKeys = Super.optionKeys.slice(0);
+ // add defaults keys to optionKeys, dedupe
+ Object.keys( Item.defaults ).forEach( function( key ) {
+ if ( !Item.optionKeys.includes( key ) ) {
+ Item.optionKeys.push( key );
+ }
+ });
+
+ Item.subclass = getSubclass( Item );
+
+ return Item;
+ };
+}
+
+Anchor.subclass = getSubclass( Anchor );
+
+return Anchor;
+
+}));
+/**
+ * Dragger
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module */ // CommonJS
+ module.exports = factory();
+ } else {
+ // browser global
+ root.Zdog.Dragger = factory();
+ }
+}( this, function factory() {
+
+// quick & dirty drag event stuff
+// messes up if multiple pointers/touches
+
+// event support, default to mouse events
+var downEvent = 'mousedown';
+var moveEvent = 'mousemove';
+var upEvent = 'mouseup';
+if ( window.PointerEvent ) {
+ // PointerEvent, Chrome
+ downEvent = 'pointerdown';
+ moveEvent = 'pointermove';
+ upEvent = 'pointerup';
+} else if ( 'ontouchstart' in window ) {
+ // Touch Events, iOS Safari
+ downEvent = 'touchstart';
+ moveEvent = 'touchmove';
+ upEvent = 'touchend';
+}
+
+function noop() {}
+
+function Dragger( options ) {
+ this.create( options || {} );
+}
+
+Dragger.prototype.create = function( options ) {
+ this.onDragStart = options.onDragStart || noop;
+ this.onDragMove = options.onDragMove || noop;
+ this.onDragEnd = options.onDragEnd || noop;
+
+ this.bindDrag( options.startElement );
+};
+
+Dragger.prototype.bindDrag = function( element ) {
+ element = this.getQueryElement( element );
+ if ( element ) {
+ element.addEventListener( downEvent , this );
+ }
+};
+
+Dragger.prototype.getQueryElement = function( element ) {
+ if ( typeof element == 'string' ) {
+ // with string, query selector
+ element = document.querySelector( element );
+ }
+ return element;
+};
+
+Dragger.prototype.handleEvent = function( event ) {
+ var method = this[ 'on' + event.type ];
+ if ( method ) {
+ method.call( this, event );
+ }
+};
+
+Dragger.prototype.onmousedown =
+Dragger.prototype.onpointerdown = function( event ) {
+ this.dragStart( event, event );
+};
+
+Dragger.prototype.ontouchstart = function( event ) {
+ this.dragStart( event, event.changedTouches[0] );
+};
+
+Dragger.prototype.dragStart = function( event, pointer ) {
+ event.preventDefault();
+ this.dragStartX = pointer.pageX;
+ this.dragStartY = pointer.pageY;
+ window.addEventListener( moveEvent, this );
+ window.addEventListener( upEvent, this );
+ this.onDragStart( pointer );
+};
+
+Dragger.prototype.ontouchmove = function( event ) {
+ // HACK, moved touch may not be first
+ this.dragMove( event, event.changedTouches[0] );
+};
+
+Dragger.prototype.onmousemove =
+Dragger.prototype.onpointermove = function( event ) {
+ this.dragMove( event, event );
+};
+
+Dragger.prototype.dragMove = function( event, pointer ) {
+ event.preventDefault();
+ var moveX = pointer.pageX - this.dragStartX;
+ var moveY = pointer.pageY - this.dragStartY;
+ this.onDragMove( pointer, moveX, moveY );
+};
+
+Dragger.prototype.onmouseup =
+Dragger.prototype.onpointerup =
+Dragger.prototype.ontouchend =
+Dragger.prototype.dragEnd = function(/* event */) {
+ window.removeEventListener( moveEvent, this );
+ window.removeEventListener( upEvent, this );
+ this.onDragEnd();
+};
+
+return Dragger;
+
+}));
+/**
+ * Illustration
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./boilerplate'), require('./anchor'),
+ require('./dragger') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Illustration = factory( Zdog, Zdog.Anchor, Zdog.Dragger );
+ }
+}( this, function factory( utils, Anchor, Dragger ) {
+
+function noop() {}
+var TAU = utils.TAU;
+
+var Illustration = Anchor.subclass({
+ element: undefined,
+ centered: true,
+ zoom: 1,
+ dragRotate: false,
+ resize: false,
+ onPrerender: noop,
+ onDragStart: noop,
+ onDragMove: noop,
+ onDragEnd: noop,
+ onResize: noop,
+});
+
+utils.extend( Illustration.prototype, Dragger.prototype );
+
+Illustration.prototype.create = function( options ) {
+ Anchor.prototype.create.call( this, options );
+ Dragger.prototype.create.call( this, options );
+ this.setElement( this.element );
+ this.setDragRotate( this.dragRotate );
+ this.setResize( this.resize );
+};
+
+Illustration.prototype.setElement = function( element ) {
+ element = this.getQueryElement( element );
+ if ( !element ) {
+ throw new Error( 'Zdog.Illustration element required. Set to ' + element );
+ }
+
+ var nodeName = element.nodeName.toLowerCase();
+ if ( nodeName == 'canvas' ) {
+ this.setCanvas( element );
+ } else if ( nodeName == 'svg' ) {
+ this.setSvg( element );
+ }
+};
+
+Illustration.prototype.setSize = function( width, height ) {
+ width = Math.round( width );
+ height = Math.round( height );
+ if ( this.isCanvas ) {
+ this.setSizeCanvas( width, height );
+ } else if ( this.isSvg ) {
+ this.setSizeSvg( width, height );
+ }
+};
+
+Illustration.prototype.setResize = function( resize ) {
+ this.resize = resize;
+ // create resize event listener
+ if ( !this.resizeListener ) {
+ this.resizeListener = this.onWindowResize.bind( this );
+ }
+ // add/remove event listener
+ if ( resize ) {
+ window.addEventListener( 'resize', this.resizeListener );
+ this.onWindowResize();
+ } else {
+ window.removeEventListener( 'resize', this.resizeListener );
+ }
+};
+
+// TODO debounce this?
+Illustration.prototype.onWindowResize = function() {
+ this.setMeasuredSize();
+ this.onResize( this.width, this.height );
+};
+
+Illustration.prototype.setMeasuredSize = function() {
+ var width, height;
+ var isFullscreen = this.resize == 'fullscreen';
+ if ( isFullscreen ) {
+ width = window.innerWidth;
+ height = window.innerHeight;
+ } else {
+ var rect = this.element.getBoundingClientRect();
+ width = rect.width;
+ height = rect.height;
+ }
+ this.setSize( width, height );
+};
+
+// ----- render ----- //
+
+Illustration.prototype.renderGraph = function( item ) {
+ if ( this.isCanvas ) {
+ this.renderGraphCanvas( item );
+ } else if ( this.isSvg ) {
+ this.renderGraphSvg( item );
+ }
+};
+
+// combo method
+Illustration.prototype.updateRenderGraph = function( item ) {
+ this.updateGraph();
+ this.renderGraph( item );
+};
+
+// ----- canvas ----- //
+
+Illustration.prototype.setCanvas = function( element ) {
+ this.element = element;
+ this.isCanvas = true;
+ // update related properties
+ this.ctx = this.element.getContext('2d');
+ // set initial size
+ this.setSizeCanvas( element.width, element.height );
+};
+
+Illustration.prototype.setSizeCanvas = function( width, height ) {
+ this.width = width;
+ this.height = height;
+ // up-rez for hi-DPI devices
+ var pixelRatio = this.pixelRatio = window.devicePixelRatio || 1;
+ this.element.width = this.canvasWidth = width * pixelRatio;
+ this.element.height = this.canvasHeight = height * pixelRatio;
+ if ( pixelRatio > 1 ) {
+ this.element.style.width = width + 'px';
+ this.element.style.height = height + 'px';
+ }
+};
+
+Illustration.prototype.renderGraphCanvas = function( item ) {
+ item = item || this;
+ this.prerenderCanvas();
+ Anchor.prototype.renderGraphCanvas.call( item, this.ctx );
+ this.postrenderCanvas();
+};
+
+Illustration.prototype.prerenderCanvas = function() {
+ var ctx = this.ctx;
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+ ctx.clearRect( 0, 0, this.canvasWidth, this.canvasHeight );
+ ctx.save();
+ if ( this.centered ) {
+ ctx.translate( this.width/2, this.height/2 );
+ }
+ var scale = this.pixelRatio * this.zoom;
+ ctx.scale( scale, scale );
+ this.onPrerender( ctx );
+};
+
+Illustration.prototype.postrenderCanvas = function () {
+ this.ctx.restore();
+};
+
+// ----- svg ----- //
+
+Illustration.prototype.setSvg = function( element ) {
+ this.element = element;
+ this.isSvg = true;
+ this.pixelRatio = 1;
+ // set initial size from width & height attributes
+ var width = element.getAttribute('width');
+ var height = element.getAttribute('height');
+ this.setSizeSvg( width, height );
+};
+
+Illustration.prototype.setSizeSvg = function( width, height ) {
+ this.width = width;
+ this.height = height;
+ var viewWidth = width / this.zoom;
+ var viewHeight = height / this.zoom;
+ var viewX = this.centered ? -viewWidth/2 : 0;
+ var viewY = this.centered ? -viewHeight/2 : 0;
+ this.element.setAttribute( 'viewBox', viewX + ' ' + viewY + ' ' +
+ viewWidth + ' ' + viewHeight );
+ if ( this.resize ) {
+ // remove size attributes, let size be determined by viewbox
+ this.element.removeAttribute('width');
+ this.element.removeAttribute('height');
+ } else {
+ this.element.setAttribute( 'width', width );
+ this.element.setAttribute( 'height', height );
+ }
+};
+
+Illustration.prototype.renderGraphSvg = function( item ) {
+ item = item || this;
+ empty( this.element );
+ this.onPrerender( this.element );
+ Anchor.prototype.renderGraphSvg.call( item, this.element );
+};
+
+function empty( element ) {
+ while ( element.firstChild ) {
+ element.removeChild( element.firstChild );
+ }
+}
+
+// ----- drag ----- //
+
+Illustration.prototype.setDragRotate = function( item ) {
+ if ( !item ) {
+ return;
+ } else if ( item === true ) {
+ item = this;
+ }
+ this.dragRotate = item;
+
+ this.bindDrag( this.element );
+};
+
+Illustration.prototype.dragStart = function(/* event, pointer */) {
+ this.dragStartRX = this.dragRotate.rotate.x;
+ this.dragStartRY = this.dragRotate.rotate.y;
+ Dragger.prototype.dragStart.apply( this, arguments );
+};
+
+Illustration.prototype.dragMove = function( event, pointer ) {
+ var moveX = pointer.pageX - this.dragStartX;
+ var moveY = pointer.pageY - this.dragStartY;
+ var displaySize = Math.min( this.width, this.height );
+ var moveRY = moveX / displaySize * TAU;
+ var moveRX = moveY / displaySize * TAU;
+ this.dragRotate.rotate.x = this.dragStartRX - moveRX;
+ this.dragRotate.rotate.y = this.dragStartRY - moveRY;
+ Dragger.prototype.dragMove.apply( this, arguments );
+};
+
+return Illustration;
+
+}));
+/**
+ * PathCommand
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./vector') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.PathCommand = factory( Zdog.Vector );
+ }
+}( this, function factory( Vector ) {
+
+function PathCommand( method, points, previousPoint ) {
+ this.method = method;
+ this.points = points.map( mapVectorPoint );
+ this.renderPoints = points.map( mapNewVector );
+ this.previousPoint = previousPoint;
+ this.endRenderPoint = this.renderPoints[ this.renderPoints.length - 1 ];
+ // arc actions come with previous point & corner point
+ // but require bezier control points
+ if ( method == 'arc' ) {
+ this.controlPoints = [ new Vector(), new Vector() ];
+ }
+}
+
+function mapVectorPoint( point ) {
+ if ( point instanceof Vector ) {
+ return point;
+ } else {
+ return new Vector( point );
+ }
+}
+
+function mapNewVector( point ) {
+ return new Vector( point );
+}
+
+PathCommand.prototype.reset = function() {
+ // reset renderPoints back to orignal points position
+ var points = this.points;
+ this.renderPoints.forEach( function( renderPoint, i ) {
+ var point = points[i];
+ renderPoint.set( point );
+ });
+};
+
+PathCommand.prototype.transform = function( translation, rotation, scale ) {
+ this.renderPoints.forEach( function( renderPoint ) {
+ renderPoint.transform( translation, rotation, scale );
+ });
+};
+
+PathCommand.prototype.render = function( ctx, elem, renderer ) {
+ return this[ this.method ]( ctx, elem, renderer );
+};
+
+PathCommand.prototype.move = function( ctx, elem, renderer ) {
+ return renderer.move( ctx, elem, this.renderPoints[0] );
+};
+
+PathCommand.prototype.line = function( ctx, elem, renderer ) {
+ return renderer.line( ctx, elem, this.renderPoints[0] );
+};
+
+PathCommand.prototype.bezier = function( ctx, elem, renderer ) {
+ var cp0 = this.renderPoints[0];
+ var cp1 = this.renderPoints[1];
+ var end = this.renderPoints[2];
+ return renderer.bezier( ctx, elem, cp0, cp1, end );
+};
+
+PathCommand.prototype.arc = function( ctx, elem, renderer ) {
+ var prev = this.previousPoint;
+ var corner = this.renderPoints[0];
+ var end = this.renderPoints[1];
+ var cp0 = this.controlPoints[0];
+ var cp1 = this.controlPoints[1];
+ cp0.set( prev ).lerp( corner, 9/16 );
+ cp1.set( end ).lerp( corner, 9/16 );
+ return renderer.bezier( ctx, elem, cp0, cp1, end );
+};
+
+return PathCommand;
+
+}));
+/**
+ * Shape
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./boilerplate'), require('./vector'),
+ require('./path-command'), require('./anchor') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Shape = factory( Zdog, Zdog.Vector, Zdog.PathCommand, Zdog.Anchor );
+ }
+}( this, function factory( utils, Vector, PathCommand, Anchor ) {
+
+var Shape = Anchor.subclass({
+ stroke: 1,
+ fill: false,
+ color: '#333',
+ closed: true,
+ visible: true,
+ path: [ {} ],
+ front: { z: 1 },
+ backface: true,
+});
+
+Shape.prototype.create = function( options ) {
+ Anchor.prototype.create.call( this, options );
+ this.updatePath();
+ // front
+ this.front = new Vector( options.front || this.front );
+ this.renderFront = new Vector( this.front );
+ this.renderNormal = new Vector();
+};
+
+var actionNames = [
+ 'move',
+ 'line',
+ 'bezier',
+ 'arc',
+];
+
+Shape.prototype.updatePath = function() {
+ this.setPath();
+ this.updatePathCommands();
+};
+
+// place holder for Ellipse, Rect, etc.
+Shape.prototype.setPath = function() {};
+
+// parse path into PathCommands
+Shape.prototype.updatePathCommands = function() {
+ var previousPoint;
+ this.pathCommands = this.path.map( function( pathPart, i ) {
+ // pathPart can be just vector coordinates -> { x, y, z }
+ // or path instruction -> { arc: [ {x0,y0,z0}, {x1,y1,z1} ] }
+ var keys = Object.keys( pathPart );
+ var method = keys[0];
+ var points = pathPart[ method ];
+ // default to line if no instruction
+ var isInstruction = keys.length == 1 && actionNames.includes( method );
+ if ( !isInstruction ) {
+ method = 'line';
+ points = pathPart;
+ }
+ // munge single-point methods like line & move without arrays
+ var isLineOrMove = method == 'line' || method == 'move';
+ var isPointsArray = Array.isArray( points );
+ if ( isLineOrMove && !isPointsArray ) {
+ points = [ points ];
+ }
+
+ // first action is always move
+ method = i === 0 ? 'move' : method;
+ // arcs require previous last point
+ var command = new PathCommand( method, points, previousPoint );
+ // update previousLastPoint
+ previousPoint = command.endRenderPoint;
+ return command;
+ });
+};
+
+// ----- update ----- //
+
+Shape.prototype.reset = function() {
+ this.renderOrigin.set( this.origin );
+ this.renderFront.set( this.front );
+ // reset command render points
+ this.pathCommands.forEach( function( command ) {
+ command.reset();
+ });
+};
+
+Shape.prototype.transform = function( translation, rotation, scale ) {
+ // calculate render points backface visibility & cone/hemisphere shapes
+ this.renderOrigin.transform( translation, rotation, scale );
+ this.renderFront.transform( translation, rotation, scale );
+ this.renderNormal.set( this.renderOrigin ).subtract( this.renderFront );
+ // transform points
+ this.pathCommands.forEach( function( command ) {
+ command.transform( translation, rotation, scale );
+ });
+ // transform children
+ this.children.forEach( function( child ) {
+ child.transform( translation, rotation, scale );
+ });
+};
+
+
+Shape.prototype.updateSortValue = function() {
+ var sortValueTotal = 0;
+ this.pathCommands.forEach( function( command ) {
+ sortValueTotal += command.endRenderPoint.z;
+ });
+ // average sort value of all points
+ // def not geometrically correct, but works for me
+ this.sortValue = sortValueTotal / this.pathCommands.length;
+};
+
+// ----- render ----- //
+
+Shape.prototype.render = function( ctx, renderer ) {
+ var length = this.pathCommands.length;
+ if ( !this.visible || !length ) {
+ return;
+ }
+ // do not render if hiding backface
+ this.isFacingBack = this.renderNormal.z > 0;
+ if ( !this.backface && this.isFacingBack ) {
+ return;
+ }
+ if ( !renderer ) {
+ throw new Error( 'Zdog renderer required. Set to ' + renderer );
+ }
+ // render dot or path
+ var isDot = length == 1;
+ if ( renderer.isCanvas && isDot ) {
+ this.renderCanvasDot( ctx, renderer );
+ } else {
+ this.renderPath( ctx, renderer );
+ }
+};
+
+var TAU = utils.TAU;
+// Safari does not render lines with no size, have to render circle instead
+Shape.prototype.renderCanvasDot = function( ctx ) {
+ var lineWidth = this.getLineWidth();
+ if ( !lineWidth ) {
+ return;
+ }
+ ctx.fillStyle = this.getRenderColor();
+ var point = this.pathCommands[0].endRenderPoint;
+ ctx.beginPath();
+ var radius = lineWidth/2;
+ ctx.arc( point.x, point.y, radius, 0, TAU );
+ ctx.fill();
+};
+
+Shape.prototype.getLineWidth = function() {
+ if ( !this.stroke ) {
+ return 0;
+ }
+ if ( this.stroke == true ) {
+ return 1;
+ }
+ return this.stroke;
+};
+
+Shape.prototype.getRenderColor = function() {
+ // use backface color if applicable
+ var isBackfaceColor = typeof this.backface == 'string' && this.isFacingBack;
+ var color = isBackfaceColor ? this.backface : this.color;
+ return color;
+};
+
+Shape.prototype.renderPath = function( ctx, renderer ) {
+ var elem = this.getRenderElement( ctx, renderer );
+ var isTwoPoints = this.pathCommands.length == 2 &&
+ this.pathCommands[1].method == 'line';
+ var isClosed = !isTwoPoints && this.closed;
+ var color = this.getRenderColor();
+
+ renderer.renderPath( ctx, elem, this.pathCommands, isClosed );
+ renderer.stroke( ctx, elem, this.stroke, color, this.getLineWidth() );
+ renderer.fill( ctx, elem, this.fill, color );
+ renderer.end( ctx, elem );
+};
+
+var svgURI = 'http://www.w3.org/2000/svg';
+
+Shape.prototype.getRenderElement = function( ctx, renderer ) {
+ if ( !renderer.isSvg ) {
+ return;
+ }
+ if ( !this.svgElement ) {
+ // create svgElement
+ this.svgElement = document.createElementNS( svgURI, 'path');
+ this.svgElement.setAttribute( 'stroke-linecap', 'round' );
+ this.svgElement.setAttribute( 'stroke-linejoin', 'round' );
+ }
+ return this.svgElement;
+};
+
+return Shape;
+
+}));
+/**
+ * Group
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./anchor') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Group = factory( Zdog.Anchor );
+ }
+}( this, function factory( Anchor ) {
+
+var Group = Anchor.subclass({
+ updateSort: false,
+ visible: true,
+});
+
+// ----- update ----- //
+
+Group.prototype.updateSortValue = function() {
+ var sortValueTotal = 0;
+ this.checkFlatGraph();
+ this.flatGraph.forEach( function( item ) {
+ item.updateSortValue();
+ sortValueTotal += item.sortValue;
+ });
+ // average sort value of all points
+ // def not geometrically correct, but works for me
+ this.sortValue = sortValueTotal / this.flatGraph.length;
+
+ if ( this.updateSort ) {
+ this.flatGraph.sort( Anchor.shapeSorter );
+ }
+};
+
+// ----- render ----- //
+
+Group.prototype.render = function( ctx, renderer ) {
+ if ( !this.visible ) {
+ return;
+ }
+
+ this.checkFlatGraph();
+ this.flatGraph.forEach( function( item ) {
+ item.render( ctx, renderer );
+ });
+};
+
+// do not include children, group handles rendering & sorting internally
+Group.prototype.getFlatGraph = function() {
+ return [ this ];
+};
+
+// get flat graph only used for group
+// do not include in parent flatGraphs
+Group.prototype.updateFlatGraph = function() {
+ // do not include self
+ var flatGraph = [];
+ this.children.forEach( function( child ) {
+ var childFlatGraph = child.getFlatGraph();
+ flatGraph = flatGraph.concat( childFlatGraph );
+ });
+ this.flatGraph = flatGraph;
+};
+
+return Group;
+
+}));
+/**
+ * Rect
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */// CommonJS
+ module.exports = factory( require('./shape') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Rect = factory( Zdog.Shape );
+ }
+}( this, function factory( Shape ) {
+
+var Rect = Shape.subclass({
+ width: 1,
+ height: 1,
+});
+
+Rect.prototype.setPath = function() {
+ var x = this.width / 2;
+ var y = this.height / 2;
+ this.path = [
+ { x: -x, y: -y },
+ { x: x, y: -y },
+ { x: x, y: y },
+ { x: -x, y: y },
+ ];
+};
+
+return Rect;
+
+}));
+/**
+ * RoundedRect
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./shape') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.RoundedRect = factory( Zdog.Shape );
+ }
+}( this, function factory( Shape ) {
+
+var RoundedRect = Shape.subclass({
+ width: 1,
+ height: 1,
+ cornerRadius: 0.25,
+ closed: false,
+});
+
+RoundedRect.prototype.setPath = function() {
+ var xA = this.width / 2;
+ var yA = this.height / 2;
+ var shortSide = Math.min( xA, yA );
+ var cornerRadius = Math.min( this.cornerRadius, shortSide );
+ var xB = xA - cornerRadius;
+ var yB = yA - cornerRadius;
+ var path = [
+ // top right corner
+ { x: xB, y: -yA },
+ { arc: [
+ { x: xA, y: -yA },
+ { x: xA, y: -yB },
+ ]},
+ ];
+ // bottom right corner
+ if ( yB ) {
+ path.push({ x: xA, y: yB });
+ }
+ path.push({ arc: [
+ { x: xA, y: yA },
+ { x: xB, y: yA },
+ ]});
+ // bottom left corner
+ if ( xB ) {
+ path.push({ x: -xB, y: yA });
+ }
+ path.push({ arc: [
+ { x: -xA, y: yA },
+ { x: -xA, y: yB },
+ ]});
+ // top left corner
+ if ( yB ) {
+ path.push({ x: -xA, y: -yB });
+ }
+ path.push({ arc: [
+ { x: -xA, y: -yA },
+ { x: -xB, y: -yA },
+ ]});
+
+ // back to top right corner
+ if ( xB ) {
+ path.push({ x: xB, y: -yA });
+ }
+
+ this.path = path;
+};
+
+RoundedRect.prototype.updateSortValue = function() {
+ Shape.prototype.updateSortValue.apply( this, arguments );
+ // ellipse is self closing, do not count last point twice
+ var length = this.pathCommands.length;
+ var lastPoint = this.pathCommands[ length - 1 ].endRenderPoint;
+ this.sortValue -= lastPoint.z / length;
+};
+
+return RoundedRect;
+
+}));
+/**
+ * Ellipse
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./shape') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Ellipse = factory( Zdog.Shape );
+ }
+
+}( this, function factory( Shape ) {
+
+var Ellipse = Shape.subclass({
+ diameter: 1,
+ width: undefined,
+ height: undefined,
+ quarters: 4,
+ closed: false,
+});
+
+Ellipse.prototype.setPath = function() {
+ var width = this.width != undefined ? this.width : this.diameter;
+ var height = this.height != undefined ? this.height : this.diameter;
+ var x = width / 2;
+ var y = height / 2;
+ this.path = [
+ { x: 0, y: -y },
+ { arc: [ // top right
+ { x: x, y: -y },
+ { x: x, y: 0 },
+ ]}
+ ];
+ // bottom right
+ if ( this.quarters > 1 ) {
+ this.path.push({ arc: [
+ { x: x, y: y },
+ { x: 0, y: y },
+ ]});
+ }
+ // bottom left
+ if ( this.quarters > 2 ) {
+ this.path.push({ arc: [
+ { x: -x, y: y },
+ { x: -x, y: 0 },
+ ]});
+ }
+ // top left
+ if ( this.quarters > 3 ) {
+ this.path.push({ arc: [
+ { x: -x, y: -y },
+ { x: 0, y: -y },
+ ]});
+ }
+};
+
+Ellipse.prototype.updateSortValue = function() {
+ Shape.prototype.updateSortValue.apply( this, arguments );
+ if ( this.quarters != 4 ) {
+ return;
+ }
+ // ellipse is self closing, do not count last point twice
+ var length = this.pathCommands.length;
+ var lastPoint = this.pathCommands[ length - 1 ].endRenderPoint;
+ this.sortValue -= lastPoint.z / length;
+};
+
+return Ellipse;
+
+}));
+/**
+ * Shape
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./boilerplate'), require('./shape') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Polygon = factory( Zdog, Zdog.Shape );
+ }
+}( this, function factory( utils, Shape ) {
+
+var Polygon = Shape.subclass({
+ sides: 3,
+ radius: 0.5,
+});
+
+var TAU = utils.TAU;
+
+Polygon.prototype.setPath = function() {
+ this.path = [];
+ for ( var i=0; i < this.sides; i++ ) {
+ var theta = i/this.sides * TAU - TAU/4;
+ var x = Math.cos( theta ) * this.radius;
+ var y = Math.sin( theta ) * this.radius;
+ this.path.push({ x: x, y: y });
+ }
+};
+
+return Polygon;
+
+}));
+/**
+ * Hemisphere composite shape
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./boilerplate'), require('./ellipse') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Hemisphere = factory( Zdog, Zdog.Ellipse );
+ }
+}( this, function factory( utils, Ellipse ) {
+
+var Hemisphere = Ellipse.subclass({
+ fill: true,
+});
+
+var TAU = utils.TAU;
+
+Hemisphere.prototype.render = function( ctx, renderer ) {
+ this.renderDome( ctx, renderer );
+ // call super
+ Ellipse.prototype.render.apply( this, arguments );
+};
+
+Hemisphere.prototype.renderDome = function( ctx, renderer ) {
+ if ( !this.visible ) {
+ return;
+ }
+ var elem = this.getDomeRenderElement( ctx, renderer );
+ var contourAngle = Math.atan2( this.renderNormal.y, this.renderNormal.x );
+ var domeRadius = this.diameter/2 * this.renderNormal.magnitude();
+ var x = this.renderOrigin.x;
+ var y = this.renderOrigin.y;
+
+ if ( renderer.isCanvas ) {
+ // canvas
+ var startAngle = contourAngle + TAU/4;
+ var endAngle = contourAngle - TAU/4;
+ ctx.beginPath();
+ ctx.arc( x, y, domeRadius, startAngle, endAngle );
+ } else if ( renderer.isSvg ) {
+ // svg
+ contourAngle = (contourAngle - TAU/4) / TAU * 360;
+ this.domeSvgElement.setAttribute( 'd', 'M ' + (-domeRadius) + ',0 A ' +
+ domeRadius + ',' + domeRadius + ' 0 0 1 ' + domeRadius + ',0' );
+ this.domeSvgElement.setAttribute( 'transform',
+ 'translate(' + x + ',' + y + ' ) rotate(' + contourAngle + ')' );
+ }
+
+ renderer.stroke( ctx, elem, this.stroke, this.color, this.getLineWidth() );
+ renderer.fill( ctx, elem, this.fill, this.color );
+ renderer.end( ctx, elem );
+};
+
+var svgURI = 'http://www.w3.org/2000/svg';
+
+Hemisphere.prototype.getDomeRenderElement = function( ctx, renderer ) {
+ if ( !renderer.isSvg ) {
+ return;
+ }
+ if ( !this.domeSvgElement ) {
+ // create svgElement
+ this.domeSvgElement = document.createElementNS( svgURI, 'path');
+ this.domeSvgElement.setAttribute( 'stroke-linecap', 'round' );
+ this.domeSvgElement.setAttribute( 'stroke-linejoin', 'round' );
+ }
+ return this.domeSvgElement;
+};
+
+return Hemisphere;
+
+}));
+/**
+ * Cylinder composite shape
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./boilerplate'),
+ require('./path-command'), require('./shape'), require('./group'),
+ require('./ellipse') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Cylinder = factory( Zdog, Zdog.PathCommand, Zdog.Shape,
+ Zdog.Group, Zdog.Ellipse );
+ }
+}( this, function factory( utils, PathCommand, Shape, Group, Ellipse ) {
+
+function noop() {}
+
+// ----- CylinderGroup ----- //
+
+var CylinderGroup = Group.subclass({
+ color: '#333',
+ updateSort: true,
+});
+
+CylinderGroup.prototype.create = function() {
+ Group.prototype.create.apply( this, arguments );
+ this.pathCommands = [
+ new PathCommand( 'move', [ {} ] ),
+ new PathCommand( 'line', [ {} ] ),
+ ];
+};
+
+CylinderGroup.prototype.render = function( ctx, renderer ) {
+ this.renderCylinderSurface( ctx, renderer );
+ Group.prototype.render.apply( this, arguments );
+};
+
+CylinderGroup.prototype.renderCylinderSurface = function( ctx, renderer ) {
+ if ( !this.visible ) {
+ return;
+ }
+ // render cylinder surface
+ var elem = this.getRenderElement( ctx, renderer );
+ var frontBase = this.frontBase;
+ var rearBase = this.rearBase;
+ var scale = frontBase.renderNormal.magnitude();
+ var strokeWidth = frontBase.diameter * scale + frontBase.getLineWidth();
+ // set path command render points
+ this.pathCommands[0].renderPoints[0].set( frontBase.renderOrigin );
+ this.pathCommands[1].renderPoints[0].set( rearBase.renderOrigin );
+
+ if ( renderer.isCanvas ) {
+ ctx.lineCap = 'butt'; // nice
+ }
+ renderer.renderPath( ctx, elem, this.pathCommands );
+ renderer.stroke( ctx, elem, true, this.color, strokeWidth );
+ renderer.end( ctx, elem );
+
+ if ( renderer.isCanvas ) {
+ ctx.lineCap = 'round'; // reset
+ }
+};
+
+var svgURI = 'http://www.w3.org/2000/svg';
+
+CylinderGroup.prototype.getRenderElement = function( ctx, renderer ) {
+ if ( !renderer.isSvg ) {
+ return;
+ }
+ if ( !this.svgElement ) {
+ // create svgElement
+ this.svgElement = document.createElementNS( svgURI, 'path');
+ }
+ return this.svgElement;
+};
+
+// prevent double-creation in parent.copyGraph()
+// only create in Cylinder.create()
+CylinderGroup.prototype.copyGraph = noop;
+
+// ----- CylinderEllipse ----- //
+
+var CylinderEllipse = Ellipse.subclass();
+
+CylinderEllipse.prototype.copyGraph = noop;
+
+// ----- Cylinder ----- //
+
+var Cylinder = Shape.subclass({
+ diameter: 1,
+ length: 1,
+ frontBaseColor: undefined,
+ rearBaseColor: undefined,
+ fill: true,
+});
+
+var TAU = utils.TAU;
+
+Cylinder.prototype.create = function(/* options */) {
+ // call super
+ Shape.prototype.create.apply( this, arguments );
+ // composite shape, create child shapes
+ // CylinderGroup to render cylinder surface then bases
+ this.group = new CylinderGroup({
+ addTo: this,
+ color: this.color,
+ visible: this.visible,
+ });
+ var baseZ = this.length/2;
+ var baseColor = this.backface || true;
+ // front outside base
+ this.frontBase = this.group.frontBase = new Ellipse({
+ addTo: this.group,
+ diameter: this.diameter,
+ translate: { z: baseZ },
+ rotate: { y: TAU/2 },
+ color: this.color,
+ stroke: this.stroke,
+ fill: this.fill,
+ backface: this.frontBaseColor || baseColor,
+ visible: this.visible,
+ });
+ // back outside base
+ this.rearBase = this.group.rearBase = this.frontBase.copy({
+ translate: { z: -baseZ },
+ rotate: { y: 0 },
+ backface: this.rearBaseColor || baseColor,
+ });
+};
+
+// Cylinder shape does not render anything
+Cylinder.prototype.render = function() {};
+
+// ----- set child properties ----- //
+
+var childProperties = [ 'stroke', 'fill', 'color', 'visible' ];
+childProperties.forEach( function( property ) {
+ // use proxy property for custom getter & setter
+ var _prop = '_' + property;
+ Object.defineProperty( Cylinder.prototype, property, {
+ get: function() {
+ return this[ _prop ];
+ },
+ set: function( value ) {
+ this[ _prop ] = value;
+ // set property on children
+ if ( this.frontBase ) {
+ this.frontBase[ property ] = value;
+ this.rearBase[ property ] = value;
+ this.group[ property ] = value;
+ }
+ },
+ });
+});
+
+// TODO child property setter for backface, frontBaseColor, & rearBaseColor
+
+return Cylinder;
+
+}));
+/**
+ * Cone composite shape
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./boilerplate'), require('./vector'),
+ require('./path-command'), require('./anchor'), require('./ellipse') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Cone = factory( Zdog, Zdog.Vector, Zdog.PathCommand,
+ Zdog.Anchor, Zdog.Ellipse );
+ }
+}( this, function factory( utils, Vector, PathCommand, Anchor, Ellipse ) {
+
+var Cone = Ellipse.subclass({
+ length: 1,
+ fill: true,
+});
+
+var TAU = utils.TAU;
+
+Cone.prototype.create = function(/* options */) {
+ // call super
+ Ellipse.prototype.create.apply( this, arguments );
+ // composite shape, create child shapes
+ this.apex = new Anchor({
+ addTo: this,
+ translate: { z: this.length },
+ });
+
+ // vectors used for calculation
+ this.renderApex = new Vector();
+ this.tangentA = new Vector();
+ this.tangentB = new Vector();
+
+ this.surfacePathCommands = [
+ new PathCommand( 'move', [ {} ] ), // points set in renderConeSurface
+ new PathCommand( 'line', [ {} ] ),
+ new PathCommand( 'line', [ {} ] ),
+ ];
+};
+
+Cone.prototype.render = function( ctx, renderer ) {
+ this.renderConeSurface( ctx, renderer );
+ Ellipse.prototype.render.apply( this, arguments );
+};
+
+Cone.prototype.renderConeSurface = function( ctx, renderer ) {
+ if ( !this.visible ) {
+ return;
+ }
+ this.renderApex.set( this.apex.renderOrigin )
+ .subtract( this.renderOrigin );
+
+ var scale = this.renderNormal.magnitude();
+ var apexDistance = this.renderApex.magnitude2d();
+ var normalDistance = this.renderNormal.magnitude2d();
+ // eccentricity
+ var eccenAngle = Math.acos( normalDistance / scale );
+ var eccen = Math.sin( eccenAngle );
+ var radius = this.diameter/2 * scale;
+ // does apex extend beyond eclipse of face
+ var isApexVisible = radius * eccen < apexDistance;
+ if ( !isApexVisible ) {
+ return;
+ }
+ // update tangents
+ var apexAngle = Math.atan2( this.renderNormal.y, this.renderNormal.x ) + TAU/2;
+ var projectLength = apexDistance / eccen;
+ var projectAngle = Math.acos( radius / projectLength );
+ // set tangent points
+ var tangentA = this.tangentA;
+ var tangentB = this.tangentB;
+
+ tangentA.x = Math.cos( projectAngle ) * radius * eccen;
+ tangentA.y = Math.sin( projectAngle ) * radius;
+
+ tangentB.set( this.tangentA );
+ tangentB.y *= -1;
+
+ tangentA.rotateZ( apexAngle );
+ tangentB.rotateZ( apexAngle );
+ tangentA.add( this.renderOrigin );
+ tangentB.add( this.renderOrigin );
+
+ this.setSurfaceRenderPoint( 0, tangentA );
+ this.setSurfaceRenderPoint( 1, this.apex.renderOrigin );
+ this.setSurfaceRenderPoint( 2, tangentB );
+
+ // render
+ var elem = this.getSurfaceRenderElement( ctx, renderer );
+ renderer.renderPath( ctx, elem, this.surfacePathCommands );
+ renderer.stroke( ctx, elem, this.stroke, this.color, this.getLineWidth() );
+ renderer.fill( ctx, elem, this.fill, this.color );
+ renderer.end( ctx, elem );
+};
+
+var svgURI = 'http://www.w3.org/2000/svg';
+
+Cone.prototype.getSurfaceRenderElement = function( ctx, renderer ) {
+ if ( !renderer.isSvg ) {
+ return;
+ }
+ if ( !this.surfaceSvgElement ) {
+ // create svgElement
+ this.surfaceSvgElement = document.createElementNS( svgURI, 'path');
+ this.surfaceSvgElement.setAttribute( 'stroke-linecap', 'round' );
+ this.surfaceSvgElement.setAttribute( 'stroke-linejoin', 'round' );
+ }
+ return this.surfaceSvgElement;
+};
+
+Cone.prototype.setSurfaceRenderPoint = function( index, point ) {
+ var renderPoint = this.surfacePathCommands[ index ].renderPoints[0];
+ renderPoint.set( point );
+};
+
+return Cone;
+
+}));
+/**
+ * Box composite shape
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory( require('./boilerplate'), require('./anchor'),
+ require('./shape'), require('./rect') );
+ } else {
+ // browser global
+ var Zdog = root.Zdog;
+ Zdog.Box = factory( Zdog, Zdog.Anchor, Zdog.Shape, Zdog.Rect );
+ }
+}( this, function factory( utils, Anchor, Shape, Rect ) {
+
+// ----- BoxRect ----- //
+
+var BoxRect = Rect.subclass();
+// prevent double-creation in parent.copyGraph()
+// only create in Box.create()
+BoxRect.prototype.copyGraph = function() {};
+
+// ----- Box ----- //
+
+var boxDefaults = utils.extend( {
+ width: 1,
+ height: 1,
+ depth: 1,
+ frontFace: true,
+ rearFace: true,
+ leftFace: true,
+ rightFace: true,
+ topFace: true,
+ bottomFace: true,
+}, Shape.defaults );
+// default fill
+boxDefaults.fill = true;
+delete boxDefaults.path;
+
+var Box = Anchor.subclass( boxDefaults );
+
+var TAU = utils.TAU;
+
+Box.prototype.create = function( options ) {
+ Anchor.prototype.create.call( this, options );
+ this.updatePath();
+};
+
+Box.prototype.updatePath = function() {
+ this.setFace( 'frontFace', {
+ width: this.width,
+ height: this.height,
+ translate: { z: this.depth/2 },
+ });
+ this.setFace( 'rearFace', {
+ width: this.width,
+ height: this.height,
+ translate: { z: -this.depth/2 },
+ rotate: { y: TAU/2 },
+ });
+ this.setFace( 'leftFace', {
+ width: this.depth,
+ height: this.height,
+ translate: { x: -this.width/2 },
+ rotate: { y: -TAU/4 },
+ });
+ this.setFace( 'rightFace', {
+ width: this.depth,
+ height: this.height,
+ translate: { x: this.width/2 },
+ rotate: { y: TAU/4 },
+ });
+ this.setFace( 'topFace', {
+ width: this.width,
+ height: this.depth,
+ translate: { y: -this.height/2 },
+ rotate: { x: -TAU/4 },
+ });
+ this.setFace( 'bottomFace', {
+ width: this.width,
+ height: this.depth,
+ translate: { y: this.height/2 },
+ rotate: { x: -TAU/4 },
+ });
+};
+
+Box.prototype.setFace = function( faceName, options ) {
+ var property = this[ faceName ];
+ var rectProperty = faceName + 'Rect';
+ var rect = this[ rectProperty ];
+ // remove if false
+ if ( !property ) {
+ this.removeChild( rect );
+ return;
+ }
+ // update & add face
+ utils.extend( options, {
+ // set color from option, i.e. `front: '#19F'`
+ color: typeof property == 'string' ? property : this.color,
+ stroke: this.stroke,
+ fill: this.fill,
+ backface: this.backface,
+ front: this.front,
+ visible: this.visible,
+ });
+ if ( rect ) {
+ // update previous
+ rect.setOptions( options );
+ } else {
+ // create new
+ rect = this[ rectProperty ] = new BoxRect( options );
+ }
+ rect.updatePath();
+ this.addChild( rect );
+};
+
+return Box;
+
+}));
+/**
+ * Index
+ */
+
+( function( root, factory ) {
+ // module definition
+ if ( typeof module == 'object' && module.exports ) {
+ /* globals module, require */ // CommonJS
+ module.exports = factory(
+ require('./boilerplate'),
+ require('./canvas-renderer'),
+ require('./svg-renderer'),
+ require('./vector'),
+ require('./anchor'),
+ require('./dragger'),
+ require('./illustration'),
+ require('./path-command'),
+ require('./shape'),
+ require('./group'),
+ require('./rect'),
+ require('./rounded-rect'),
+ require('./ellipse'),
+ require('./polygon'),
+ require('./hemisphere'),
+ require('./cylinder'),
+ require('./cone'),
+ require('./box')
+ );
+ } else if ( typeof define == 'function' && define.amd ) {
+ /* globals define */ // AMD
+ define( 'zdog', [], root.Zdog );
+ }
+})( this, function factory( Zdog ) {
+
+ return Zdog;
+
+});
diff --git a/dist/zdog.dist.min.js b/dist/zdog.dist.min.js
new file mode 100644
index 0000000..6513bca
--- /dev/null
+++ b/dist/zdog.dist.min.js
@@ -0,0 +1,8 @@
+/*!
+ * Zdog v1.0.0
+ * Round, flat, designer-friendly pseudo-3D engine
+ * Licensed MIT
+ * https://zzz.dog
+ * Copyright 2019 Metafizzy
+ */
+(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e()}else{t.Zdog=e()}})(this,function t(){var e={};e.TAU=Math.PI*2;e.extend=function(t,e){for(var r in e){t[r]=e[r]}return t};e.lerp=function(t,e,r){return(e-t)*r+t};e.modulo=function(t,e){return(t%e+e)%e};var s={2:function(t){return t*t},3:function(t){return t*t*t},4:function(t){return t*t*t*t},5:function(t){return t*t*t*t*t}};e.easeInOut=function(t,e){if(e==1){return t}t=Math.max(0,Math.min(1,t));var r=t<.5;var i=r?t:1-t;i=i/.5;var o=s[e]||s[2];var n=o(i);n=n/2;return r?n:1-n};return e});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e()}else{t.Zdog.CanvasRenderer=e()}})(this,function t(){var o={isCanvas:true};o.begin=function(t){t.beginPath()};o.move=function(t,e,r){t.moveTo(r.x,r.y)};o.line=function(t,e,r){t.lineTo(r.x,r.y)};o.bezier=function(t,e,r,i,o){t.bezierCurveTo(r.x,r.y,i.x,i.y,o.x,o.y)};o.closePath=function(t){t.closePath()};o.setPath=function(){};o.renderPath=function(e,r,t,i){this.begin(e,r);t.forEach(function(t){t.render(e,r,o)});if(i){this.closePath(e,r)}};o.stroke=function(t,e,r,i,o){if(!r){return}t.strokeStyle=i;t.lineWidth=o;t.stroke()};o.fill=function(t,e,r,i){if(!r){return}t.fillStyle=i;t.fill()};o.end=function(){};return o});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e()}else{t.Zdog.SvgRenderer=e()}})(this,function t(){var n={isSvg:true};var e=n.round=function(t){return Math.round(t*1e3)/1e3};function s(t){return e(t.x)+","+e(t.y)+" "}n.begin=function(){};n.move=function(t,e,r){return"M"+s(r)};n.line=function(t,e,r){return"L"+s(r)};n.bezier=function(t,e,r,i,o){return"C"+s(r)+s(i)+s(o)};n.closePath=function(){return"Z"};n.setPath=function(t,e,r){e.setAttribute("d",r)};n.renderPath=function(e,r,t,i){var o="";t.forEach(function(t){o+=t.render(e,r,n)});if(i){o+=this.closePath(e,r)}this.setPath(e,r,o)};n.stroke=function(t,e,r,i,o){if(!r){return}e.setAttribute("stroke",i);e.setAttribute("stroke-width",o)};n.fill=function(t,e,r,i){var o=r?i:"none";e.setAttribute("fill",o)};n.end=function(t,e){t.appendChild(e)};return n});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e(require("./boilerplate"))}else{var r=t.Zdog;r.Vector=e(r)}})(this,function t(r){function e(t){this.set(t)}var h=r.TAU;e.prototype.set=function(t){this.x=t&&t.x||0;this.y=t&&t.y||0;this.z=t&&t.z||0;return this};e.prototype.write=function(t){if(!t){return this}this.x=t.x!=undefined?t.x:this.x;this.y=t.y!=undefined?t.y:this.y;this.z=t.z!=undefined?t.z:this.z;return this};e.prototype.rotate=function(t){if(!t){return}this.rotateZ(t.z);this.rotateY(t.y);this.rotateX(t.x);return this};e.prototype.rotateZ=function(t){i(this,t,"x","y")};e.prototype.rotateX=function(t){i(this,t,"y","z")};e.prototype.rotateY=function(t){i(this,t,"x","z")};function i(t,e,r,i){if(!e||e%h===0){return}var o=Math.cos(e);var n=Math.sin(e);var s=t[r];var a=t[i];t[r]=s*o-a*n;t[i]=a*o+s*n}e.prototype.add=function(t){if(!t){return this}this.x+=t.x||0;this.y+=t.y||0;this.z+=t.z||0;return this};e.prototype.subtract=function(t){if(!t){return this}this.x-=t.x||0;this.y-=t.y||0;this.z-=t.z||0;return this};e.prototype.multiply=function(t){if(t==undefined){return this}if(typeof t=="number"){this.x*=t;this.y*=t;this.z*=t}else{this.x*=t.x!=undefined?t.x:1;this.y*=t.y!=undefined?t.y:1;this.z*=t.z!=undefined?t.z:1}return this};e.prototype.transform=function(t,e,r){this.multiply(r);this.rotate(e);this.add(t);return this};e.prototype.lerp=function(t,e){this.x=r.lerp(this.x,t.x||0,e);this.y=r.lerp(this.y,t.y||0,e);this.z=r.lerp(this.z,t.z||0,e);return this};e.prototype.magnitude=function(){var t=this.x*this.x+this.y*this.y+this.z*this.z;return o(t)};function o(t){if(Math.abs(t-1)<1e-8){return 1}return Math.sqrt(t)}e.prototype.magnitude2d=function(){var t=this.x*this.x+this.y*this.y;return o(t)};e.prototype.copy=function(){return new e(this)};return e});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e(require("./boilerplate"),require("./vector"))}else{var r=t.Zdog;r.Anchor=e(r,r.Vector)}})(this,function t(o,e){var r=o.TAU;var i={x:1,y:1,z:1};function n(t){this.create(t||{})}n.prototype.create=function(t){o.extend(this,this.constructor.defaults);this.setOptions(t);this.translate=new e(t.translate);this.rotate=new e(t.rotate);this.scale=new e(i).multiply(this.scale);this.origin=new e;this.renderOrigin=new e;this.children=[];if(this.addTo){this.addTo.addChild(this)}};n.defaults={};n.optionKeys=Object.keys(n.defaults).concat(["rotate","translate","scale","addTo"]);n.prototype.setOptions=function(t){var e=this.constructor.optionKeys;for(var r in t){if(e.includes(r)){this[r]=t[r]}}};n.prototype.addChild=function(t){var e=this.children.indexOf(t);if(e!=-1){return}t.remove();t.addTo=this;this.children.push(t)};n.prototype.removeChild=function(t){var e=this.children.indexOf(t);if(e!=-1){this.children.splice(e,1)}};n.prototype.remove=function(){if(this.addTo){this.addTo.removeChild(this)}};n.prototype.update=function(){this.reset();this.children.forEach(function(t){t.update()});this.transform(this.translate,this.rotate,this.scale)};n.prototype.reset=function(){this.renderOrigin.set(this.origin)};n.prototype.transform=function(e,r,i){this.renderOrigin.transform(e,r,i);this.children.forEach(function(t){t.transform(e,r,i)})};n.prototype.updateGraph=function(){this.update();this.checkFlatGraph();this.flatGraph.forEach(function(t){t.updateSortValue()});this.flatGraph.sort(n.shapeSorter)};n.shapeSorter=function(t,e){return t.sortValue-e.sortValue};n.prototype.checkFlatGraph=function(){if(!this.flatGraph){this.updateFlatGraph()}};n.prototype.updateFlatGraph=function(){this.flatGraph=this.getFlatGraph()};n.prototype.getFlatGraph=function(){var r=[this];this.children.forEach(function(t){var e=t.getFlatGraph();r=r.concat(e)});return r};n.prototype.updateSortValue=function(){this.sortValue=this.renderOrigin.z};n.prototype.render=function(){};n.prototype.renderGraphCanvas=function(e){if(!e){throw new Error("ctx is "+e+". "+"Canvas context required for render. Check .renderGraphCanvas( ctx ).")}this.checkFlatGraph();this.flatGraph.forEach(function(t){t.render(e,Zdog.CanvasRenderer)})};n.prototype.renderGraphSvg=function(e){if(!e){throw new Error("svg is "+e+". "+"SVG required for render. Check .renderGraphSvg( svg ).")}this.checkFlatGraph();this.flatGraph.forEach(function(t){t.render(e,Zdog.SvgRenderer)})};n.prototype.copy=function(t){var e={};var r=this.constructor.optionKeys;r.forEach(function(t){e[t]=this[t]},this);o.extend(e,t);var i=this.constructor;return new i(e)};n.prototype.copyGraph=function(t){var e=this.copy(t);this.children.forEach(function(t){t.copyGraph({addTo:e})});return e};n.prototype.normalizeRotate=function(){this.rotate.x=o.modulo(this.rotate.x,r);this.rotate.y=o.modulo(this.rotate.y,r);this.rotate.z=o.modulo(this.rotate.z,r)};function s(r){return function(t){function e(t){this.create(t||{})}e.prototype=Object.create(r.prototype);e.prototype.constructor=e;e.defaults=o.extend({},r.defaults);o.extend(e.defaults,t);e.optionKeys=r.optionKeys.slice(0);Object.keys(e.defaults).forEach(function(t){if(!e.optionKeys.includes(t)){e.optionKeys.push(t)}});e.subclass=s(e);return e}}n.subclass=s(n);return n});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e()}else{t.Zdog.Dragger=e()}})(this,function t(){var e="mousedown";var r="mousemove";var i="mouseup";if(window.PointerEvent){e="pointerdown";r="pointermove";i="pointerup"}else if("ontouchstart"in window){e="touchstart";r="touchmove";i="touchend"}function o(){}function n(t){this.create(t||{})}n.prototype.create=function(t){this.onDragStart=t.onDragStart||o;this.onDragMove=t.onDragMove||o;this.onDragEnd=t.onDragEnd||o;this.bindDrag(t.startElement)};n.prototype.bindDrag=function(t){t=this.getQueryElement(t);if(t){t.addEventListener(e,this)}};n.prototype.getQueryElement=function(t){if(typeof t=="string"){t=document.querySelector(t)}return t};n.prototype.handleEvent=function(t){var e=this["on"+t.type];if(e){e.call(this,t)}};n.prototype.onmousedown=n.prototype.onpointerdown=function(t){this.dragStart(t,t)};n.prototype.ontouchstart=function(t){this.dragStart(t,t.changedTouches[0])};n.prototype.dragStart=function(t,e){t.preventDefault();this.dragStartX=e.pageX;this.dragStartY=e.pageY;window.addEventListener(r,this);window.addEventListener(i,this);this.onDragStart(e)};n.prototype.ontouchmove=function(t){this.dragMove(t,t.changedTouches[0])};n.prototype.onmousemove=n.prototype.onpointermove=function(t){this.dragMove(t,t)};n.prototype.dragMove=function(t,e){t.preventDefault();var r=e.pageX-this.dragStartX;var i=e.pageY-this.dragStartY;this.onDragMove(e,r,i)};n.prototype.onmouseup=n.prototype.onpointerup=n.prototype.ontouchend=n.prototype.dragEnd=function(){window.removeEventListener(r,this);window.removeEventListener(i,this);this.onDragEnd()};return n});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e(require("./boilerplate"),require("./anchor"),require("./dragger"))}else{var r=t.Zdog;r.Illustration=e(r,r.Anchor,r.Dragger)}})(this,function t(e,r,a){function i(){}var h=e.TAU;var o=r.subclass({element:undefined,centered:true,zoom:1,dragRotate:false,resize:false,onPrerender:i,onDragStart:i,onDragMove:i,onDragEnd:i,onResize:i});e.extend(o.prototype,a.prototype);o.prototype.create=function(t){r.prototype.create.call(this,t);a.prototype.create.call(this,t);this.setElement(this.element);this.setDragRotate(this.dragRotate);this.setResize(this.resize)};o.prototype.setElement=function(t){t=this.getQueryElement(t);if(!t){throw new Error("Zdog.Illustration element required. Set to "+t)}var e=t.nodeName.toLowerCase();if(e=="canvas"){this.setCanvas(t)}else if(e=="svg"){this.setSvg(t)}};o.prototype.setSize=function(t,e){t=Math.round(t);e=Math.round(e);if(this.isCanvas){this.setSizeCanvas(t,e)}else if(this.isSvg){this.setSizeSvg(t,e)}};o.prototype.setResize=function(t){this.resize=t;if(!this.resizeListener){this.resizeListener=this.onWindowResize.bind(this)}if(t){window.addEventListener("resize",this.resizeListener);this.onWindowResize()}else{window.removeEventListener("resize",this.resizeListener)}};o.prototype.onWindowResize=function(){this.setMeasuredSize();this.onResize(this.width,this.height)};o.prototype.setMeasuredSize=function(){var t,e;var r=this.resize=="fullscreen";if(r){t=window.innerWidth;e=window.innerHeight}else{var i=this.element.getBoundingClientRect();t=i.width;e=i.height}this.setSize(t,e)};o.prototype.renderGraph=function(t){if(this.isCanvas){this.renderGraphCanvas(t)}else if(this.isSvg){this.renderGraphSvg(t)}};o.prototype.updateRenderGraph=function(t){this.updateGraph();this.renderGraph(t)};o.prototype.setCanvas=function(t){this.element=t;this.isCanvas=true;this.ctx=this.element.getContext("2d");this.setSizeCanvas(t.width,t.height)};o.prototype.setSizeCanvas=function(t,e){this.width=t;this.height=e;var r=this.pixelRatio=window.devicePixelRatio||1;this.element.width=this.canvasWidth=t*r;this.element.height=this.canvasHeight=e*r;if(r>1){this.element.style.width=t+"px";this.element.style.height=e+"px"}};o.prototype.renderGraphCanvas=function(t){t=t||this;this.prerenderCanvas();r.prototype.renderGraphCanvas.call(t,this.ctx);this.postrenderCanvas()};o.prototype.prerenderCanvas=function(){var t=this.ctx;t.lineCap="round";t.lineJoin="round";t.clearRect(0,0,this.canvasWidth,this.canvasHeight);t.save();if(this.centered){t.translate(this.width/2,this.height/2)}var e=this.pixelRatio*this.zoom;t.scale(e,e);this.onPrerender(t)};o.prototype.postrenderCanvas=function(){this.ctx.restore()};o.prototype.setSvg=function(t){this.element=t;this.isSvg=true;this.pixelRatio=1;var e=t.getAttribute("width");var r=t.getAttribute("height");this.setSizeSvg(e,r)};o.prototype.setSizeSvg=function(t,e){this.width=t;this.height=e;var r=t/this.zoom;var i=e/this.zoom;var o=this.centered?-r/2:0;var n=this.centered?-i/2:0;this.element.setAttribute("viewBox",o+" "+n+" "+r+" "+i);if(this.resize){this.element.removeAttribute("width");this.element.removeAttribute("height")}else{this.element.setAttribute("width",t);this.element.setAttribute("height",e)}};o.prototype.renderGraphSvg=function(t){t=t||this;n(this.element);this.onPrerender(this.element);r.prototype.renderGraphSvg.call(t,this.element)};function n(t){while(t.firstChild){t.removeChild(t.firstChild)}}o.prototype.setDragRotate=function(t){if(!t){return}else if(t===true){t=this}this.dragRotate=t;this.bindDrag(this.element)};o.prototype.dragStart=function(){this.dragStartRX=this.dragRotate.rotate.x;this.dragStartRY=this.dragRotate.rotate.y;a.prototype.dragStart.apply(this,arguments)};o.prototype.dragMove=function(t,e){var r=e.pageX-this.dragStartX;var i=e.pageY-this.dragStartY;var o=Math.min(this.width,this.height);var n=r/o*h;var s=i/o*h;this.dragRotate.rotate.x=this.dragStartRX-s;this.dragRotate.rotate.y=this.dragStartRY-n;a.prototype.dragMove.apply(this,arguments)};return o});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e(require("./vector"))}else{var r=t.Zdog;r.PathCommand=e(r.Vector)}})(this,function t(i){function e(t,e,r){this.method=t;this.points=e.map(o);this.renderPoints=e.map(n);this.previousPoint=r;this.endRenderPoint=this.renderPoints[this.renderPoints.length-1];if(t=="arc"){this.controlPoints=[new i,new i]}}function o(t){if(t instanceof i){return t}else{return new i(t)}}function n(t){return new i(t)}e.prototype.reset=function(){var i=this.points;this.renderPoints.forEach(function(t,e){var r=i[e];t.set(r)})};e.prototype.transform=function(e,r,i){this.renderPoints.forEach(function(t){t.transform(e,r,i)})};e.prototype.render=function(t,e,r){return this[this.method](t,e,r)};e.prototype.move=function(t,e,r){return r.move(t,e,this.renderPoints[0])};e.prototype.line=function(t,e,r){return r.line(t,e,this.renderPoints[0])};e.prototype.bezier=function(t,e,r){var i=this.renderPoints[0];var o=this.renderPoints[1];var n=this.renderPoints[2];return r.bezier(t,e,i,o,n)};e.prototype.arc=function(t,e,r){var i=this.previousPoint;var o=this.renderPoints[0];var n=this.renderPoints[1];var s=this.controlPoints[0];var a=this.controlPoints[1];s.set(i).lerp(o,9/16);a.set(n).lerp(o,9/16);return r.bezier(t,e,s,a,n)};return e});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e(require("./boilerplate"),require("./vector"),require("./path-command"),require("./anchor"))}else{var r=t.Zdog;r.Shape=e(r,r.Vector,r.PathCommand,r.Anchor)}})(this,function t(e,r,p,i){var o=i.subclass({stroke:1,fill:false,color:"#333",closed:true,visible:true,path:[{}],front:{z:1},backface:true});o.prototype.create=function(t){i.prototype.create.call(this,t);this.updatePath();this.front=new r(t.front||this.front);this.renderFront=new r(this.front);this.renderNormal=new r};var d=["move","line","bezier","arc"];o.prototype.updatePath=function(){this.setPath();this.updatePathCommands()};o.prototype.setPath=function(){};o.prototype.updatePathCommands=function(){var u;this.pathCommands=this.path.map(function(t,e){var r=Object.keys(t);var i=r[0];var o=t[i];var n=r.length==1&&d.includes(i);if(!n){i="line";o=t}var s=i=="line"||i=="move";var a=Array.isArray(o);if(s&&!a){o=[o]}i=e===0?"move":i;var h=new p(i,o,u);u=h.endRenderPoint;return h})};o.prototype.reset=function(){this.renderOrigin.set(this.origin);this.renderFront.set(this.front);this.pathCommands.forEach(function(t){t.reset()})};o.prototype.transform=function(e,r,i){this.renderOrigin.transform(e,r,i);this.renderFront.transform(e,r,i);this.renderNormal.set(this.renderOrigin).subtract(this.renderFront);this.pathCommands.forEach(function(t){t.transform(e,r,i)});this.children.forEach(function(t){t.transform(e,r,i)})};o.prototype.updateSortValue=function(){var e=0;this.pathCommands.forEach(function(t){e+=t.endRenderPoint.z});this.sortValue=e/this.pathCommands.length};o.prototype.render=function(t,e){var r=this.pathCommands.length;if(!this.visible||!r){return}this.isFacingBack=this.renderNormal.z>0;if(!this.backface&&this.isFacingBack){return}if(!e){throw new Error("Zdog renderer required. Set to "+e)}var i=r==1;if(e.isCanvas&&i){this.renderCanvasDot(t,e)}else{this.renderPath(t,e)}};var n=e.TAU;o.prototype.renderCanvasDot=function(t){var e=this.getLineWidth();if(!e){return}t.fillStyle=this.getRenderColor();var r=this.pathCommands[0].endRenderPoint;t.beginPath();var i=e/2;t.arc(r.x,r.y,i,0,n);t.fill()};o.prototype.getLineWidth=function(){if(!this.stroke){return 0}if(this.stroke==true){return 1}return this.stroke};o.prototype.getRenderColor=function(){var t=typeof this.backface=="string"&&this.isFacingBack;var e=t?this.backface:this.color;return e};o.prototype.renderPath=function(t,e){var r=this.getRenderElement(t,e);var i=this.pathCommands.length==2&&this.pathCommands[1].method=="line";var o=!i&&this.closed;var n=this.getRenderColor();e.renderPath(t,r,this.pathCommands,o);e.stroke(t,r,this.stroke,n,this.getLineWidth());e.fill(t,r,this.fill,n);e.end(t,r)};var s="http://www.w3.org/2000/svg";o.prototype.getRenderElement=function(t,e){if(!e.isSvg){return}if(!this.svgElement){this.svgElement=document.createElementNS(s,"path");this.svgElement.setAttribute("stroke-linecap","round");this.svgElement.setAttribute("stroke-linejoin","round")}return this.svgElement};return o});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e(require("./anchor"))}else{var r=t.Zdog;r.Group=e(r.Anchor)}})(this,function t(r){var e=r.subclass({updateSort:false,visible:true});e.prototype.updateSortValue=function(){var e=0;this.checkFlatGraph();this.flatGraph.forEach(function(t){t.updateSortValue();e+=t.sortValue});this.sortValue=e/this.flatGraph.length;if(this.updateSort){this.flatGraph.sort(r.shapeSorter)}};e.prototype.render=function(e,r){if(!this.visible){return}this.checkFlatGraph();this.flatGraph.forEach(function(t){t.render(e,r)})};e.prototype.getFlatGraph=function(){return[this]};e.prototype.updateFlatGraph=function(){var r=[];this.children.forEach(function(t){var e=t.getFlatGraph();r=r.concat(e)});this.flatGraph=r};return e});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e(require("./shape"))}else{var r=t.Zdog;r.Rect=e(r.Shape)}})(this,function t(e){var r=e.subclass({width:1,height:1});r.prototype.setPath=function(){var t=this.width/2;var e=this.height/2;this.path=[{x:-t,y:-e},{x:t,y:-e},{x:t,y:e},{x:-t,y:e}]};return r});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e(require("./shape"))}else{var r=t.Zdog;r.RoundedRect=e(r.Shape)}})(this,function t(r){var e=r.subclass({width:1,height:1,cornerRadius:.25,closed:false});e.prototype.setPath=function(){var t=this.width/2;var e=this.height/2;var r=Math.min(t,e);var i=Math.min(this.cornerRadius,r);var o=t-i;var n=e-i;var s=[{x:o,y:-e},{arc:[{x:t,y:-e},{x:t,y:-n}]}];if(n){s.push({x:t,y:n})}s.push({arc:[{x:t,y:e},{x:o,y:e}]});if(o){s.push({x:-o,y:e})}s.push({arc:[{x:-t,y:e},{x:-t,y:n}]});if(n){s.push({x:-t,y:-n})}s.push({arc:[{x:-t,y:-e},{x:-o,y:-e}]});if(o){s.push({x:o,y:-e})}this.path=s};e.prototype.updateSortValue=function(){r.prototype.updateSortValue.apply(this,arguments);var t=this.pathCommands.length;var e=this.pathCommands[t-1].endRenderPoint;this.sortValue-=e.z/t};return e});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e(require("./shape"))}else{var r=t.Zdog;r.Ellipse=e(r.Shape)}})(this,function t(r){var e=r.subclass({diameter:1,width:undefined,height:undefined,quarters:4,closed:false});e.prototype.setPath=function(){var t=this.width!=undefined?this.width:this.diameter;var e=this.height!=undefined?this.height:this.diameter;var r=t/2;var i=e/2;this.path=[{x:0,y:-i},{arc:[{x:r,y:-i},{x:r,y:0}]}];if(this.quarters>1){this.path.push({arc:[{x:r,y:i},{x:0,y:i}]})}if(this.quarters>2){this.path.push({arc:[{x:-r,y:i},{x:-r,y:0}]})}if(this.quarters>3){this.path.push({arc:[{x:-r,y:-i},{x:0,y:-i}]})}};e.prototype.updateSortValue=function(){r.prototype.updateSortValue.apply(this,arguments);if(this.quarters!=4){return}var t=this.pathCommands.length;var e=this.pathCommands[t-1].endRenderPoint;this.sortValue-=e.z/t};return e});(function(t,e){if(typeof module=="object"&&module.exports){module.exports=e(require("./boilerplate"),require("./shape"))}else{var r=t.Zdog;r.Polygon=e(r,r.Shape)}})(this,function t(e,r){var i=r.subclass({sides:3,radius:.5});var o=e.TAU;i.prototype.setPath=function(){this.path=[];for(var t=0;t