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.

Add horn support from bendablegears/zdog:master

+267 -1
+1
.gitignore
··· 1 1 .DS_Store 2 2 node_modules/ 3 3 bower_components/ 4 + *.bak
+263
js/horn.js
··· 1 + /** 2 + * Horn composite 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'), 10 + require('./path-command'), require('./shape'), require('./group'), 11 + require('./vector') ); 12 + } else { 13 + // browser global 14 + var Zdog = root.Zdog; 15 + Zdog.Horn = factory( Zdog, Zdog.PathCommand, Zdog.Shape, 16 + Zdog.Group, Zdog.Vector ); 17 + } 18 + }( this, function factory( utils, PathCommand, Shape, Group, Vector ) { 19 + 20 + function noop() {} 21 + 22 + // ----- HornGroup ----- // 23 + 24 + var HornGroup = Group.subclass({ 25 + color: '#333', 26 + updateSort: true, 27 + }); 28 + 29 + HornGroup.prototype.create = function() { 30 + Group.prototype.create.apply( this, arguments ); 31 + 32 + // vectors used for calculation 33 + this.renderApex = new Vector(); 34 + this.tangentFrontA = new Vector(); 35 + this.tangentFrontB = new Vector(); 36 + this.tangentRearA = new Vector(); 37 + this.tangentRearB = new Vector(); 38 + 39 + this.pathCommands = [ 40 + new PathCommand( 'move', [ {} ] ), 41 + new PathCommand( 'line', [ {} ] ), 42 + new PathCommand( 'line', [ {} ] ), 43 + new PathCommand( 'line', [ {} ] ), 44 + ]; 45 + }; 46 + 47 + HornGroup.prototype.render = function( ctx, renderer ) { 48 + this.renderHornSurface( ctx, renderer ); 49 + Group.prototype.render.apply( this, arguments ); 50 + }; 51 + 52 + HornGroup.prototype.renderHornSurface = function( ctx, renderer ) { 53 + if ( !this.visible ) { 54 + return; 55 + } 56 + // render horn surface 57 + var elem = this.getRenderElement( ctx, renderer ); 58 + var frontBase = this.frontBase; 59 + var frontDiameter = frontBase.stroke; 60 + var rearBase = this.rearBase; 61 + var rearDiameter = rearBase.stroke; 62 + var scale = frontBase.renderNormal.magnitude(); 63 + var frontRadius = frontDiameter/2 * scale; 64 + var rearRadius = rearDiameter/2 * scale; 65 + 66 + this.renderApex.set( rearBase.renderOrigin ) 67 + .subtract( frontBase.renderOrigin ); 68 + 69 + // calculate tangents. 70 + var scale = frontBase.renderNormal.magnitude(); 71 + var apexDistance = this.renderApex.magnitude2d(); 72 + var normalDistance = frontBase.renderNormal.magnitude2d(); 73 + // eccentricity 74 + var eccenAngle = Math.acos( normalDistance / scale ); 75 + var biggerRadius = (frontRadius > rearRadius) ? frontRadius : rearRadius; 76 + var eccenPercent; 77 + if (frontRadius == 0 || rearRadius == 0) { 78 + eccenPercent = 1.0; 79 + } else { 80 + eccenPercent = (Math.abs(frontRadius - rearRadius) / biggerRadius); 81 + } 82 + var eccen = Math.sin( eccenAngle ) * Math.sqrt(eccenPercent); 83 + // does apex extend beyond eclipse of face 84 + apexDistance = apexDistance + frontRadius/4 + rearRadius/4; 85 + var isApexVisible = frontRadius * eccen < apexDistance && 86 + rearRadius * eccen < apexDistance; 87 + if ( !isApexVisible ) { 88 + return; 89 + } 90 + // update tangents 91 + // TODO: try something more like horn_old.js updateSortValue() 92 + var apexAngle = Math.atan2( frontBase.renderNormal.y, frontBase.renderNormal.x ) + 93 + TAU/2; 94 + var projectFrontLength = (apexDistance + frontRadius) / eccen; 95 + var projectRearLength = (apexDistance + rearRadius) / eccen; 96 + var projectFrontAngle = Math.acos( frontRadius / projectFrontLength ); 97 + var projectRearAngle = Math.acos( rearRadius / -projectRearLength ); 98 + // set tangent points 99 + var tangentFrontA = this.tangentFrontA; 100 + var tangentFrontB = this.tangentFrontB; 101 + var tangentRearA = this.tangentRearA; 102 + var tangentRearB = this.tangentRearB; 103 + 104 + tangentFrontA.x = Math.cos( projectFrontAngle ) * frontRadius * eccen; 105 + tangentFrontA.y = Math.sin( projectFrontAngle ) * frontRadius; 106 + tangentRearA.x = Math.cos( projectRearAngle ) * rearRadius * eccen; 107 + tangentRearA.y = Math.sin( projectRearAngle ) * rearRadius; 108 + 109 + tangentFrontB.set( this.tangentFrontA ); 110 + tangentFrontB.y *= -1; 111 + tangentRearB.set( this.tangentRearA ); 112 + tangentRearB.y *= -1; 113 + 114 + tangentFrontA.rotateZ( apexAngle); 115 + tangentFrontB.rotateZ( apexAngle); 116 + tangentFrontA.add( frontBase.renderOrigin ); 117 + tangentFrontB.add( frontBase.renderOrigin ); 118 + tangentRearA.rotateZ( apexAngle + TAU/2); 119 + tangentRearB.rotateZ( apexAngle + TAU/2); 120 + tangentRearA.add( rearBase.renderOrigin ); 121 + tangentRearB.add( rearBase.renderOrigin ); 122 + 123 + 124 + // set path command render points 125 + this.pathCommands[0].renderPoints[0].set( tangentFrontA ); 126 + this.pathCommands[1].renderPoints[0].set( tangentRearB ); 127 + this.pathCommands[2].renderPoints[0].set( tangentRearA ); 128 + this.pathCommands[3].renderPoints[0].set( tangentFrontB ); 129 + 130 + if ( renderer.isCanvas ) { 131 + ctx.lineCap = 'butt'; // nice 132 + } 133 + renderer.renderPath( ctx, elem, this.pathCommands ); 134 + //renderer.stroke( ctx, elem, true, '#333', 0.1 ); // remove once testing is done. 135 + renderer.fill( ctx, elem, true, this.color ); 136 + renderer.end( ctx, elem ); 137 + 138 + if ( renderer.isCanvas ) { 139 + ctx.lineCap = 'round'; // reset 140 + } 141 + }; 142 + 143 + var svgURI = 'http://www.w3.org/2000/svg'; 144 + 145 + HornGroup.prototype.getRenderElement = function( ctx, renderer ) { 146 + if ( !renderer.isSvg ) { 147 + return; 148 + } 149 + if ( !this.svgElement ) { 150 + // create svgElement 151 + this.svgElement = document.createElementNS( svgURI, 'path'); 152 + } 153 + return this.svgElement; 154 + }; 155 + 156 + // prevent double-creation in parent.copyGraph() 157 + // only create in Horn.create() 158 + HornGroup.prototype.copyGraph = noop; 159 + 160 + // ----- HornCap ----- // 161 + 162 + var HornCap = Shape.subclass(); 163 + 164 + HornCap.prototype.copyGraph = noop; 165 + 166 + // ----- Horn ----- // 167 + 168 + var Horn = Shape.subclass({ 169 + frontDiameter: 1, 170 + rearDiameter: 1, 171 + length: 1, 172 + frontFace: undefined, 173 + fill: true, 174 + }); 175 + 176 + var TAU = utils.TAU; 177 + 178 + Horn.prototype.create = function(/* options */) { 179 + // call super 180 + Shape.prototype.create.apply( this, arguments ); 181 + // composite shape, create child shapes 182 + // HornGroup to render horn surface then bases 183 + this.group = new HornGroup({ 184 + addTo: this, 185 + color: this.color, 186 + visible: this.visible, 187 + }); 188 + var baseZ = this.length/2; 189 + var baseColor = this.backface || true; 190 + // front outside base 191 + this.frontBase = this.group.frontBase = new HornCap({ 192 + addTo: this.group, 193 + translate: { z: (baseZ - this.frontDiameter/2) }, 194 + rotate: { y: TAU/2 }, 195 + color: this.color, 196 + stroke: this.frontDiameter, 197 + fill: this.fill, 198 + backface: this.frontFace || baseColor, 199 + visible: this.visible, 200 + }); 201 + // back outside base 202 + this.rearBase = this.group.rearBase = new HornCap({ 203 + addTo: this.group, 204 + translate: { z: (-baseZ + this.rearDiameter/2) }, 205 + rotate: { y: 0 }, 206 + color: this.color, 207 + stroke: this.rearDiameter, 208 + fill: this.fill, 209 + backface: baseColor, 210 + visible: this.visible, 211 + }); 212 + 213 + }; 214 + 215 + Horn.prototype.updateFrontCapDiameter = function(size) { 216 + this.frontBase.stroke = size; 217 + var baseZ = this.length/2; 218 + this.frontBase.translate.z = (baseZ - size/2); 219 + } 220 + 221 + Horn.prototype.updateRearCapDiameter = function(size) { 222 + this.rearBase.stroke = size; 223 + var baseZ = this.length/2; 224 + this.rearBase.translate.z = (-baseZ + size/2); 225 + } 226 + 227 + // Horn shape does not render anything 228 + Horn.prototype.render = function() {}; 229 + 230 + // ----- set child properties ----- // 231 + 232 + var childProperties = [ 'stroke', 'fill', 'color', 'visible', 233 + 'frontDiameter', 'rearDiameter' ]; 234 + childProperties.forEach( function( property ) { 235 + // use proxy property for custom getter & setter 236 + var _prop = '_' + property; 237 + Object.defineProperty( Horn.prototype, property, { 238 + get: function() { 239 + return this[ _prop ]; 240 + }, 241 + set: function( value ) { 242 + this[ _prop ] = value; 243 + // set property on children 244 + if ( this.frontBase ) { 245 + if (property === 'frontDiameter') { 246 + this.updateFrontCapDiameter(value); 247 + } 248 + if (property === 'rearDiameter') { 249 + this.updateRearCapDiameter(value); 250 + } 251 + this.frontBase[ property ] = value; 252 + this.rearBase[ property ] = value; 253 + this.group[ property ] = value; 254 + } 255 + }, 256 + }); 257 + }); 258 + 259 + // TODO child property setter for backface, frontBaseColor, & rearBaseColor 260 + 261 + return Horn; 262 + 263 + }));
+3 -1
js/index.js
··· 24 24 require('./hemisphere'), 25 25 require('./cylinder'), 26 26 require('./cone'), 27 + require('./horn'), 27 28 require('./box'), 28 29 require('./texture') 29 30 ); ··· 34 35 /* eslint-disable max-params */ 35 36 } )( this, function factory( Zdog, CanvasRenderer, SvgRenderer, Vector, Anchor, 36 37 Dragger, Illustration, PathCommand, Shape, Group, Rect, RoundedRect, 37 - Ellipse, Polygon, Hemisphere, Cylinder, Cone, Box, Texture ) { 38 + Ellipse, Polygon, Hemisphere, Cylinder, Cone, Horn, Box, Texture ) { 38 39 /* eslint-enable max-params */ 39 40 40 41 Zdog.CanvasRenderer = CanvasRenderer; ··· 53 54 Zdog.Hemisphere = Hemisphere; 54 55 Zdog.Cylinder = Cylinder; 55 56 Zdog.Cone = Cone; 57 + Zdog.Horn = Horn; 56 58 Zdog.Box = Box; 57 59 Zdog.Texture = Texture; 58 60