Flat, round, designer-friendly pseudo-3D engine for canvas & SVG
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 230 lines 6.6 kB view raw
1/** 2 * Shape 3 */ 4 5( function( root, factory ) { 6 // module definition 7 if ( typeof module == 'object' && module.exports ) { 8 // CommonJS 9 module.exports = factory( require('./boilerplate'), require('./vector'), 10 require('./path-command'), require('./anchor') ); 11 } else { 12 // browser global 13 var Zdog = root.Zdog; 14 Zdog.Shape = factory( Zdog, Zdog.Vector, Zdog.PathCommand, Zdog.Anchor ); 15 } 16}( this, function factory( utils, Vector, PathCommand, Anchor ) { 17 18var Shape = Anchor.subclass({ 19 stroke: 1, 20 fill: false, 21 color: '#333', 22 closed: true, 23 visible: true, 24 path: [ {} ], 25 front: { z: 1 }, 26 backface: true, 27}); 28 29Shape.type = 'Shape'; 30 31Shape.prototype.create = function( options ) { 32 Anchor.prototype.create.call( this, options ); 33 this.updatePath(); 34 // front 35 this.front = new Vector( options.front || this.front ); 36 this.renderFront = new Vector( this.front ); 37 this.renderNormal = new Vector(); 38}; 39 40var actionNames = [ 41 'move', 42 'line', 43 'bezier', 44 'arc', 45]; 46 47Shape.prototype.updatePath = function() { 48 this.setPath(); 49 this.updatePathCommands(); 50}; 51 52// place holder for Ellipse, Rect, etc. 53Shape.prototype.setPath = function() {}; 54 55// parse path into PathCommands 56Shape.prototype.updatePathCommands = function() { 57 var previousPoint; 58 this.pathCommands = this.path.map( function( pathPart, i ) { 59 // pathPart can be just vector coordinates -> { x, y, z } 60 // or path instruction -> { arc: [ {x0,y0,z0}, {x1,y1,z1} ] } 61 var keys = Object.keys( pathPart ); 62 var method = keys[0]; 63 var points = pathPart[ method ]; 64 // default to line if no instruction 65 var isInstruction = keys.length == 1 && actionNames.indexOf( method ) != -1; 66 if ( !isInstruction ) { 67 method = 'line'; 68 points = pathPart; 69 } 70 // munge single-point methods like line & move without arrays 71 var isLineOrMove = method == 'line' || method == 'move'; 72 var isPointsArray = Array.isArray( points ); 73 if ( isLineOrMove && !isPointsArray ) { 74 points = [ points ]; 75 } 76 77 // first action is always move 78 method = i === 0 ? 'move' : method; 79 // arcs require previous last point 80 var command = new PathCommand( method, points, previousPoint ); 81 // update previousLastPoint 82 previousPoint = command.endRenderPoint; 83 return command; 84 } ); 85}; 86 87// ----- update ----- // 88 89Shape.prototype.reset = function() { 90 this.renderOrigin.set( this.origin ); 91 this.renderFront.set( this.front ); 92 // reset command render points 93 this.pathCommands.forEach( function( command ) { 94 command.reset(); 95 } ); 96 97 if (this.backface && this.backface.reset) { 98 this.backface.reset(); 99 } 100 if (this.color && this.color.reset) { 101 this.color.reset(); 102 } 103}; 104 105Shape.prototype.transform = function( translation, rotation, scale ) { 106 // calculate render points backface visibility & cone/hemisphere shapes 107 this.renderOrigin.transform( translation, rotation, scale ); 108 this.renderFront.transform( translation, rotation, scale ); 109 this.renderNormal.set( this.renderOrigin ).subtract( this.renderFront ); 110 // transform points 111 this.pathCommands.forEach( function( command ) { 112 command.transform( translation, rotation, scale ); 113 } ); 114 // transform children 115 this.children.forEach( function( child ) { 116 child.transform( translation, rotation, scale ); 117 } ); 118 if (this.backface && this.backface.transform) { 119 this.backface.transform( translation, rotation, scale ); 120 } 121 if (this.color && this.color.transform) { 122 this.color.transform( translation, rotation, scale ); 123 } 124}; 125 126Shape.prototype.updateSortValue = function() { 127 // sort by average z of all points 128 // def not geometrically correct, but works for me 129 var pointCount = this.pathCommands.length; 130 var firstPoint = this.pathCommands[0].endRenderPoint; 131 var lastPoint = this.pathCommands[ pointCount - 1 ].endRenderPoint; 132 // ignore the final point if self closing shape 133 var isSelfClosing = pointCount > 2 && firstPoint.isSame( lastPoint ); 134 if ( isSelfClosing ) { 135 pointCount -= 1; 136 } 137 138 var sortValueTotal = 0; 139 for ( var i = 0; i < pointCount; i++ ) { 140 sortValueTotal += this.pathCommands[i].endRenderPoint.z; 141 } 142 this.sortValue = sortValueTotal/pointCount; 143}; 144 145// ----- render ----- // 146 147Shape.prototype.render = function( ctx, renderer ) { 148 var length = this.pathCommands.length; 149 if ( !this.visible || !length ) { 150 return; 151 } 152 // do not render if hiding backface 153 this.isFacingBack = this.renderNormal.z > 0; 154 if ( !this.backface && this.isFacingBack ) { 155 return; 156 } 157 if ( !renderer ) { 158 throw new Error( 'Zdog renderer required. Set to ' + renderer ); 159 } 160 // render dot or path 161 var isDot = length == 1; 162 if ( renderer.isCanvas && isDot ) { 163 this.renderCanvasDot( ctx, renderer ); 164 } else { 165 this.renderPath( ctx, renderer ); 166 } 167}; 168 169var TAU = utils.TAU; 170// Safari does not render lines with no size, have to render circle instead 171Shape.prototype.renderCanvasDot = function( ctx , renderer) { 172 var lineWidth = this.getLineWidth(); 173 if ( !lineWidth ) { 174 return; 175 } 176 var point = this.pathCommands[0].endRenderPoint; 177 ctx.beginPath(); 178 var radius = lineWidth/2; 179 ctx.arc( point.x, point.y, radius, 0, TAU ); 180 renderer.fill(ctx, null, true, this.getRenderColor() ); 181}; 182 183Shape.prototype.getLineWidth = function() { 184 if ( !this.stroke ) { 185 return 0; 186 } 187 if ( this.stroke == true ) { 188 return 1; 189 } 190 return this.stroke; 191}; 192 193Shape.prototype.getRenderColor = function() { 194 // use backface color if applicable 195 var isBackfaceColor = utils.isColor(this.backface) && this.isFacingBack; 196 var color = isBackfaceColor ? this.backface : this.color; 197 return color; 198}; 199 200Shape.prototype.renderPath = function( ctx, renderer ) { 201 var elem = this.getRenderElement( ctx, renderer ); 202 var isTwoPoints = this.pathCommands.length == 2 && 203 this.pathCommands[1].method == 'line'; 204 var isClosed = !isTwoPoints && this.closed; 205 var color = this.getRenderColor(); 206 207 renderer.renderPath( ctx, elem, this.pathCommands, isClosed ); 208 renderer.stroke( ctx, elem, this.stroke, color, this.getLineWidth() ); 209 renderer.fill( ctx, elem, this.fill, color ); 210 renderer.end( ctx, elem ); 211}; 212 213var svgURI = 'http://www.w3.org/2000/svg'; 214 215Shape.prototype.getRenderElement = function( ctx, renderer ) { 216 if ( !renderer.isSvg ) { 217 return; 218 } 219 if ( !this.svgElement ) { 220 // create svgElement 221 this.svgElement = document.createElementNS( svgURI, 'path' ); 222 this.svgElement.setAttribute( 'stroke-linecap', 'round' ); 223 this.svgElement.setAttribute( 'stroke-linejoin', 'round' ); 224 } 225 return this.svgElement; 226}; 227 228return Shape; 229 230} ) );