From f5ea60fffa1619e4c77c6ad4a297dd18267222a2 Mon Sep 17 00:00:00 2001 From: David DeSandro Date: Mon, 20 May 2019 20:52:38 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=B6=20build=20v1.0.0!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/contributing.md | 6 +- .github/issue_template.md | 2 +- Makefile | 9 +- README.md | 8 +- dist/zdog.dist.js | 2096 +++++++++++++++++++++++++++++++++++++ dist/zdog.dist.min.js | 8 + js/boilerplate.js | 2 +- js/index.js | 2 + package-lock.json | 2 +- package.json | 2 +- 10 files changed, 2121 insertions(+), 16 deletions(-) create mode 100644 dist/zdog.dist.js create mode 100644 dist/zdog.dist.min.js 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