/*! * Zdog v1.1.3 * Round, flat, designer-friendly pseudo-3D engine * Licensed MIT * https://zzz.dog * Copyright 2020 Metafizzy */ /** * Boilerplate & utils */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // 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, alpha ) { return ( b - a ) * alpha + 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 /= 0.5; // make easing steeper with more multiples var powerMultiplier = powerMultipliers[ power ] || powerMultipliers[2]; var curve = powerMultiplier( slope ); curve /= 2; return isFirstHalf ? curve : 1 - curve; }; return Zdog; } ) ); /** * CanvasRenderer */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // 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 ) { // 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 ) { // 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.isSame = function( pos ) { if ( !pos ) { return false; } return this.x === pos.x && this.y === pos.y && this.z === pos.z; }; 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, alpha ) { this.x = utils.lerp( this.x, pos.x || 0, alpha ); this.y = utils.lerp( this.y, pos.y || 0, alpha ); this.z = utils.lerp( this.z, pos.z || 0, alpha ); 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 ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./vector'), require('./canvas-renderer'), require('./svg-renderer') ); } else { // browser global var Zdog = root.Zdog; Zdog.Anchor = factory( Zdog, Zdog.Vector, Zdog.CanvasRenderer, Zdog.SvgRenderer ); } }( this, function factory( utils, Vector, CanvasRenderer, SvgRenderer ) { var TAU = utils.TAU; var onePoint = { x: 1, y: 1, z: 1 }; function Anchor( options ) { this.create( options || {} ); } Anchor.prototype.create = function( options ) { this.children = []; // 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(); 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.indexOf( key ) != -1 ) { this[ key ] = options[ key ]; } } }; Anchor.prototype.addChild = function( shape ) { if ( this.children.indexOf( shape ) != -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.updateFlatGraph(); this.flatGraph.forEach( function( item ) { item.updateSortValue(); } ); // z-sort this.flatGraph.sort( Anchor.shapeSorter ); }; Anchor.shapeSorter = function( a, b ) { return a.sortValue - b.sortValue; }; // custom getter to check for flatGraph before using it Object.defineProperty( Anchor.prototype, 'flatGraph', { get: function() { if ( !this._flatGraph ) { this.updateFlatGraph(); } return this._flatGraph; }, set: function( graph ) { this._flatGraph = graph; }, } ); Anchor.prototype.updateFlatGraph = function() { this.flatGraph = this.getFlatGraph(); }; // return Array of self & all child graph items Anchor.prototype.getFlatGraph = function() { var flatGraph = [ this ]; return this.addChildFlatGraph( flatGraph ); }; Anchor.prototype.addChildFlatGraph = function( flatGraph ) { this.children.forEach( function( child ) { var childFlatGraph = child.getFlatGraph(); Array.prototype.push.apply( flatGraph, childFlatGraph ); } ); return flatGraph; }; Anchor.prototype.updateSortValue = function() { this.sortValue = this.renderOrigin.z; }; // ----- render ----- // Anchor.prototype.render = function() {}; // TODO refactor out CanvasRenderer so its not a dependency within anchor.js Anchor.prototype.renderGraphCanvas = function( ctx ) { if ( !ctx ) { throw new Error( 'ctx is ' + ctx + '. ' + 'Canvas context required for render. Check .renderGraphCanvas( ctx ).' ); } this.flatGraph.forEach( function( item ) { item.render( ctx, CanvasRenderer ); } ); }; Anchor.prototype.renderGraphSvg = function( svg ) { if ( !svg ) { throw new Error( 'svg is ' + svg + '. ' + 'SVG required for render. Check .renderGraphSvg( svg ).' ); } this.flatGraph.forEach( function( item ) { item.render( svg, 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.indexOf( key ) != 1 ) { 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 ) { // 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 // check for browser window #85 var hasWindow = typeof window != 'undefined'; // event support, default to mouse events var downEvent = 'mousedown'; var moveEvent = 'mousemove'; var upEvent = 'mouseup'; if ( hasWindow ) { 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 ) { return; } // disable browser gestures #53 element.style.touchAction = 'none'; 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; if ( hasWindow ) { 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 ) { // 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; var needsHighPixelRatioSizing = pixelRatio > 1 && !this.resize; if ( needsHighPixelRatioSizing ) { 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 ) { var centerX = this.width / 2 * this.pixelRatio; var centerY = this.height / 2 * this.pixelRatio; ctx.translate( centerX, centerY ); } 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 ) { /* eslint consistent-this: "off" */ 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 ) { // 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 ); }; var arcHandleLength = 9/16; 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, arcHandleLength ); cp1.set( end ).lerp( corner, arcHandleLength ); return renderer.bezier( ctx, elem, cp0, cp1, end ); }; return PathCommand; } ) ); /** * Shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // 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.indexOf( method ) != -1; 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() { // sort by average z of all points // def not geometrically correct, but works for me var pointCount = this.pathCommands.length; var firstPoint = this.pathCommands[0].endRenderPoint; var lastPoint = this.pathCommands[ pointCount - 1 ].endRenderPoint; // ignore the final point if self closing shape var isSelfClosing = pointCount > 2 && firstPoint.isSame( lastPoint ); if ( isSelfClosing ) { pointCount -= 1; } var sortValueTotal = 0; for ( var i = 0; i < pointCount; i++ ) { sortValueTotal += this.pathCommands[i].endRenderPoint.z; } this.sortValue = sortValueTotal/pointCount; }; // ----- 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 ) { // 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.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.flatGraph.forEach( function( item ) { item.render( ctx, renderer ); } ); }; // actual group flatGraph only used inside group Group.prototype.updateFlatGraph = function() { // do not include self var flatGraph = []; this.flatGraph = this.addChildFlatGraph( flatGraph ); }; // do not include children, group handles rendering & sorting internally Group.prototype.getFlatGraph = function() { return [ this ]; }; return Group; } ) ); /** * Rect */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // 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; /* eslint key-spacing: "off" */ 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 ) { // 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() { /* eslint id-length: [ "error", { "min": 2, "exceptions": [ "x", "y" ] }], key-spacing: "off" */ 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; }; return RoundedRect; } ) ); /** * Ellipse */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // 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 }, ] }); } }; return Ellipse; } ) ); /** * Shape */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // 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 ) { // CommonJS module.exports = factory( require('./boilerplate'), require('./vector'), require('./anchor'), require('./ellipse') ); } else { // browser global var Zdog = root.Zdog; Zdog.Hemisphere = factory( Zdog, Zdog.Vector, Zdog.Anchor, Zdog.Ellipse ); } }( this, function factory( utils, Vector, Anchor, Ellipse ) { var Hemisphere = Ellipse.subclass({ fill: true, }); var TAU = utils.TAU; Hemisphere.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.diameter / 2 }, }); // vector used for calculation this.renderCentroid = new Vector(); }; Hemisphere.prototype.updateSortValue = function() { // centroid of hemisphere is 3/8 between origin and apex this.renderCentroid.set( this.renderOrigin ) .lerp( this.apex.renderOrigin, 3/8 ); this.sortValue = this.renderCentroid.z; }; 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 ) { // 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, frontFace: 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.frontFace || baseColor, visible: this.visible, }); // back outside base this.rearBase = this.group.rearBase = this.frontBase.copy({ translate: { z: -baseZ }, rotate: { y: 0 }, backface: 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 ) { // 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.renderCentroid = 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.updateSortValue = function() { // center of cone is one third of its length this.renderCentroid.set( this.renderOrigin ) .lerp( this.apex.renderOrigin, 1/3 ); this.sortValue = this.renderCentroid.z; }; 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 ) { // 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 TAU = utils.TAU; var faceNames = [ 'frontFace', 'rearFace', 'leftFace', 'rightFace', 'topFace', 'bottomFace', ]; var boxDefaults = utils.extend( {}, Shape.defaults ); delete boxDefaults.path; faceNames.forEach( function( faceName ) { boxDefaults[ faceName ] = true; } ); utils.extend( boxDefaults, { width: 1, height: 1, depth: 1, fill: true, } ); var Box = Anchor.subclass( boxDefaults ); /* eslint-disable no-self-assign */ Box.prototype.create = function( options ) { Anchor.prototype.create.call( this, options ); this.updatePath(); // HACK reset fill to trigger face setter this.fill = this.fill; }; Box.prototype.updatePath = function() { // reset all faces to trigger setters faceNames.forEach( function( faceName ) { this[ faceName ] = this[ faceName ]; }, this ); }; /* eslint-enable no-self-assign */ faceNames.forEach( function( faceName ) { var _faceName = '_' + faceName; Object.defineProperty( Box.prototype, faceName, { get: function() { return this[ _faceName ]; }, set: function( value ) { this[ _faceName ] = value; this.setFace( faceName, value ); }, } ); } ); Box.prototype.setFace = function( faceName, value ) { var rectProperty = faceName + 'Rect'; var rect = this[ rectProperty ]; // remove if false if ( !value ) { this.removeChild( rect ); return; } // update & add face var options = this.getFaceOptions( faceName ); options.color = typeof value == 'string' ? value : this.color; if ( rect ) { // update previous rect.setOptions( options ); } else { // create new rect = this[ rectProperty ] = new BoxRect( options ); } rect.updatePath(); this.addChild( rect ); }; Box.prototype.getFaceOptions = function( faceName ) { return { frontFace: { width: this.width, height: this.height, translate: { z: this.depth / 2 }, }, rearFace: { width: this.width, height: this.height, translate: { z: -this.depth / 2 }, rotate: { y: TAU/2 }, }, leftFace: { width: this.depth, height: this.height, translate: { x: -this.width / 2 }, rotate: { y: -TAU/4 }, }, rightFace: { width: this.depth, height: this.height, translate: { x: this.width / 2 }, rotate: { y: TAU/4 }, }, topFace: { width: this.width, height: this.depth, translate: { y: -this.height / 2 }, rotate: { x: -TAU/4 }, }, bottomFace: { width: this.width, height: this.depth, translate: { y: this.height / 2 }, rotate: { x: TAU/4 }, }, }[ faceName ]; }; // ----- set face properties ----- // var childProperties = [ 'color', 'stroke', 'fill', 'backface', 'front', 'visible' ]; childProperties.forEach( function( property ) { // use proxy property for custom getter & setter var _prop = '_' + property; Object.defineProperty( Box.prototype, property, { get: function() { return this[ _prop ]; }, set: function( value ) { this[ _prop ] = value; faceNames.forEach( function( faceName ) { var rect = this[ faceName + 'Rect' ]; var isFaceColor = typeof this[ faceName ] == 'string'; var isColorUnderwrite = property == 'color' && isFaceColor; if ( rect && !isColorUnderwrite ) { rect[ property ] = value; } }, this ); }, } ); } ); return Box; } ) ); /** * Index */ ( function( root, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // 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 ); } /* eslint-disable max-params */ } )( this, function factory( Zdog, CanvasRenderer, SvgRenderer, Vector, Anchor, Dragger, Illustration, PathCommand, Shape, Group, Rect, RoundedRect, Ellipse, Polygon, Hemisphere, Cylinder, Cone, Box ) { /* eslint-enable max-params */ Zdog.CanvasRenderer = CanvasRenderer; Zdog.SvgRenderer = SvgRenderer; Zdog.Vector = Vector; Zdog.Anchor = Anchor; Zdog.Dragger = Dragger; Zdog.Illustration = Illustration; Zdog.PathCommand = PathCommand; Zdog.Shape = Shape; Zdog.Group = Group; Zdog.Rect = Rect; Zdog.RoundedRect = RoundedRect; Zdog.Ellipse = Ellipse; Zdog.Polygon = Polygon; Zdog.Hemisphere = Hemisphere; Zdog.Cylinder = Cylinder; Zdog.Cone = Cone; Zdog.Box = Box; return Zdog; } );