From a743e81689dd08c6b12696eeeb5fffbf7db82ee7 Mon Sep 17 00:00:00 2001 From: bendablegears <32841526+bendablegears@users.noreply.github.com> Date: Thu, 6 Jun 2019 16:19:30 -0400 Subject: [PATCH 1/6] More accurate Z sorting of Shapes Composite shapes use volume to find Z. Shapes with closed paths ignore doubled final point when averaging Z. --- js/cone.js | 13 +++++++++++++ js/ellipse.js | 11 ----------- js/hemisphere.js | 10 ++++++++++ js/rounded-rect.js | 8 -------- js/shape.js | 24 ++++++++++++++++++------ 5 files changed, 41 insertions(+), 25 deletions(-) diff --git a/js/cone.js b/js/cone.js index 4535e5a..6f1872e 100644 --- a/js/cone.js +++ b/js/cone.js @@ -44,6 +44,19 @@ Cone.prototype.create = function(/* options */) { ]; }; +Cone.prototype.updateSortValue = function() { + // call super + Ellipse.prototype.updateSortValue.apply( this, arguments ); + var apexNormal = new Vector(); + apexNormal.set( this.renderOrigin ) + .subtract( this.apex.renderOrigin ); + var apexAngleZ = Math.atan2( apexNormal.z, apexNormal.y ); + apexAngleZ = utils.modulo( apexAngleZ, TAU ); + //center of cone is one third of its length. + var apexZ = this.length/3 * Math.sin(apexAngleZ); + this.sortValue -= apexZ; +}; + Cone.prototype.render = function( ctx, renderer ) { this.renderConeSurface( ctx, renderer ); Ellipse.prototype.render.apply( this, arguments ); diff --git a/js/ellipse.js b/js/ellipse.js index fafbe98..8e3b6b0 100644 --- a/js/ellipse.js +++ b/js/ellipse.js @@ -58,17 +58,6 @@ Ellipse.prototype.setPath = function() { } }; -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; })); diff --git a/js/hemisphere.js b/js/hemisphere.js index fa92c4e..fac036d 100644 --- a/js/hemisphere.js +++ b/js/hemisphere.js @@ -20,6 +20,16 @@ var Hemisphere = Ellipse.subclass({ var TAU = utils.TAU; +Hemisphere.prototype.updateSortValue = function() { + // call super + Ellipse.prototype.updateSortValue.apply( this, arguments ); + var contourAngleZ = Math.atan2( this.renderNormal.z, this.renderNormal.y ); + contourAngleZ = utils.modulo( contourAngleZ, TAU ); + //center of dome is half the radius. + var domeZ = this.diameter/2/2 * Math.sin(contourAngleZ); + this.sortValue -= domeZ; +}; + Hemisphere.prototype.render = function( ctx, renderer ) { this.renderDome( ctx, renderer ); // call super diff --git a/js/rounded-rect.js b/js/rounded-rect.js index 30c8a3f..a4989fe 100644 --- a/js/rounded-rect.js +++ b/js/rounded-rect.js @@ -72,14 +72,6 @@ RoundedRect.prototype.setPath = function() { 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; })); diff --git a/js/shape.js b/js/shape.js index 40c927c..e009cf4 100644 --- a/js/shape.js +++ b/js/shape.js @@ -110,13 +110,25 @@ Shape.prototype.transform = function( translation, rotation, scale ) { Shape.prototype.updateSortValue = function() { + // average sort all z points. + // ignore the final point if it is a closed shape. + var howManyPoints = this.pathCommands.length; 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; + var firstPoint = this.pathCommands[0].endRenderPoint; + var lastPoint = this.pathCommands[this.pathCommands.length - 1].endRenderPoint; + if (howManyPoints > 2 && + firstPoint.x === lastPoint.x && + firstPoint.y === lastPoint.y && + firstPoint.z === lastPoint.z) { + howManyPoints -= 1; // closed shape; ignore final point. + } + + for (var i = 0; i < howManyPoints; i++) { + sortValueTotal += this.pathCommands[i].endRenderPoint.z; + } + // average sort value of all points + // def not geometrically correct, but works for me + this.sortValue = sortValueTotal / howManyPoints; }; // ----- render ----- // From 783f4c22eb7607141c653d86f8a642187b6b4ba8 Mon Sep 17 00:00:00 2001 From: bendablegears <32841526+bendablegears@users.noreply.github.com> Date: Fri, 7 Jun 2019 12:47:41 -0400 Subject: [PATCH 2/6] Minor Z fixes refactor; code formatting jslint doesn't complain anymore. Vector class has useful isSame function. --- js/shape.js | 6 ++---- js/vector.js | 9 +++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/js/shape.js b/js/shape.js index e009cf4..409c577 100644 --- a/js/shape.js +++ b/js/shape.js @@ -115,11 +115,9 @@ Shape.prototype.updateSortValue = function() { var howManyPoints = this.pathCommands.length; var sortValueTotal = 0; var firstPoint = this.pathCommands[0].endRenderPoint; - var lastPoint = this.pathCommands[this.pathCommands.length - 1].endRenderPoint; + var lastPoint = this.pathCommands[this.pathCommands.length-1].endRenderPoint; if (howManyPoints > 2 && - firstPoint.x === lastPoint.x && - firstPoint.y === lastPoint.y && - firstPoint.z === lastPoint.z) { + firstPoint.isSame(lastPoint)) { howManyPoints -= 1; // closed shape; ignore final point. } diff --git a/js/vector.js b/js/vector.js index b896526..abfeb6c 100644 --- a/js/vector.js +++ b/js/vector.js @@ -75,6 +75,15 @@ function rotateProperty( vec, angle, propA, propB ) { 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; From e661777bafe7767f9977a6d099063bb7406b4ff6 Mon Sep 17 00:00:00 2001 From: bendablegears <32841526+bendablegears@users.noreply.github.com> Date: Thu, 27 Jun 2019 15:06:22 -0400 Subject: [PATCH 3/6] New Shape; Horn Actually "truncated cone with spherical caps" but that's a mouthful. --- js/horn.js | 237 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 js/horn.js diff --git a/js/horn.js b/js/horn.js new file mode 100644 index 0000000..3b775b1 --- /dev/null +++ b/js/horn.js @@ -0,0 +1,237 @@ +/** + * Horn 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('./vector') ); + } else { + // browser global + var Zdog = root.Zdog; + Zdog.Horn = factory( Zdog, Zdog.PathCommand, Zdog.Shape, + Zdog.Group, Zdog.Vector ); + } +}( this, function factory( utils, PathCommand, Shape, Group, Vector ) { + +function noop() {} + +// ----- HornGroup ----- // + +var HornGroup = Group.subclass({ + color: '#333', + updateSort: true, +}); + +HornGroup.prototype.create = function() { + Group.prototype.create.apply( this, arguments ); + + // vectors used for calculation + this.renderApex = new Vector(); + this.tangentFrontA = new Vector(); + this.tangentFrontB = new Vector(); + this.tangentRearA = new Vector(); + this.tangentRearB = new Vector(); + + this.pathCommands = [ + new PathCommand( 'move', [ {} ] ), + new PathCommand( 'line', [ {} ] ), + new PathCommand( 'line', [ {} ] ), + new PathCommand( 'line', [ {} ] ), + ]; +}; + +HornGroup.prototype.render = function( ctx, renderer ) { + this.renderHornSurface( ctx, renderer ); + Group.prototype.render.apply( this, arguments ); +}; + +HornGroup.prototype.renderHornSurface = function( ctx, renderer ) { + if ( !this.visible ) { + return; + } + // render horn surface + var elem = this.getRenderElement( ctx, renderer ); + var frontBase = this.frontBase; + var frontDiameter = frontBase.stroke; + var rearBase = this.rearBase; + var rearDiameter = rearBase.stroke; + var scale = frontBase.renderNormal.magnitude(); + + this.renderApex.set( rearBase.renderOrigin ) + .subtract( frontBase.renderOrigin ); + + // calculate tangents. + var scale = frontBase.renderNormal.magnitude(); + var apexDistance = this.renderApex.magnitude2d(); + var normalDistance = frontBase.renderNormal.magnitude2d(); + // eccentricity + var eccenAngle = Math.acos( normalDistance / scale ); + var eccen = Math.sin( eccenAngle ); + var frontRadius = frontDiameter/2 * scale; + var rearRadius = rearDiameter/2 * scale; + // does apex extend beyond eclipse of face + var isApexVisible = frontRadius * eccen < apexDistance && + rearRadius * eccen < apexDistance; + if ( !isApexVisible ) { + return; + } + // update tangents + // TODO: try something more like horn_old.js updateSortValue() + var apexAngle = Math.atan2( frontBase.renderNormal.y, frontBase.renderNormal.x ) + + TAU/2; + var projectFrontLength = (apexDistance + frontRadius) / eccen; + var projectRearLength = (apexDistance + rearRadius) / eccen; + var projectFrontAngle = Math.acos( frontRadius / projectFrontLength ); + var projectRearAngle = Math.acos( rearRadius / -projectRearLength ); + // set tangent points + var tangentFrontA = this.tangentFrontA; + var tangentFrontB = this.tangentFrontB; + var tangentRearA = this.tangentRearA; + var tangentRearB = this.tangentRearB; + + tangentFrontA.x = Math.cos( projectFrontAngle ) * frontRadius * eccen; + tangentFrontA.y = Math.sin( projectFrontAngle ) * frontRadius; + tangentRearA.x = Math.cos( projectRearAngle ) * rearRadius * eccen; + tangentRearA.y = Math.sin( projectRearAngle ) * rearRadius; + + tangentFrontB.set( this.tangentFrontA ); + tangentFrontB.y *= -1; + tangentRearB.set( this.tangentRearA ); + tangentRearB.y *= -1; + + tangentFrontA.rotateZ( apexAngle); + tangentFrontB.rotateZ( apexAngle); + tangentFrontA.add( frontBase.renderOrigin ); + tangentFrontB.add( frontBase.renderOrigin ); + tangentRearA.rotateZ( apexAngle + TAU/2); + tangentRearB.rotateZ( apexAngle + TAU/2); + tangentRearA.add( rearBase.renderOrigin ); + tangentRearB.add( rearBase.renderOrigin ); + + + // set path command render points + this.pathCommands[0].renderPoints[0].set( tangentFrontA ); + this.pathCommands[1].renderPoints[0].set( tangentRearB ); + this.pathCommands[2].renderPoints[0].set( tangentRearA ); + this.pathCommands[3].renderPoints[0].set( tangentFrontB ); + + if ( renderer.isCanvas ) { + ctx.lineCap = 'butt'; // nice + } + renderer.renderPath( ctx, elem, this.pathCommands ); + //renderer.stroke( ctx, elem, true, '#333', 0.1 ); // remove once testing is done. + renderer.fill( ctx, elem, true, this.color ); + renderer.end( ctx, elem ); + + if ( renderer.isCanvas ) { + ctx.lineCap = 'round'; // reset + } +}; + +var svgURI = 'http://www.w3.org/2000/svg'; + +HornGroup.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 Horn.create() +HornGroup.prototype.copyGraph = noop; + +// ----- HornCap ----- // + +var HornCap = Shape.subclass(); + +HornCap.prototype.copyGraph = noop; + +// ----- Horn ----- // + +var Horn = Shape.subclass({ + frontDiameter: 1, + rearDiameter: 1, + length: 1, + frontFace: undefined, + fill: true, +}); + +var TAU = utils.TAU; + +Horn.prototype.create = function(/* options */) { + // call super + Shape.prototype.create.apply( this, arguments ); + // composite shape, create child shapes + // HornGroup to render horn surface then bases + this.group = new HornGroup({ + 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 HornCap({ + addTo: this.group, + translate: { z: (baseZ - this.frontDiameter/2) }, + rotate: { y: TAU/2 }, + color: this.color, + stroke: this.frontDiameter, + fill: this.fill, + backface: this.frontFace || baseColor, + visible: this.visible, + }); + // back outside base + this.rearBase = this.group.rearBase = new HornCap({ + addTo: this.group, + translate: { z: (-baseZ + this.rearDiameter/2) }, + rotate: { y: 0 }, + color: this.color, + stroke: this.rearDiameter, + fill: this.fill, + backface: baseColor, + visible: this.visible, + }); + +}; + +// Horn shape does not render anything +Horn.prototype.render = function() {}; + +// ----- set child properties ----- // + +var childProperties = [ 'stroke', 'fill', 'color', 'visible', + 'frontDiameter', 'rearDiameter' ]; +childProperties.forEach( function( property ) { + // use proxy property for custom getter & setter + var _prop = '_' + property; + Object.defineProperty( Horn.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 Horn; + +})); From c990e8bd41c1107fbc3af388a4392461a19599d0 Mon Sep 17 00:00:00 2001 From: bendablegears <32841526+bendablegears@users.noreply.github.com> Date: Tue, 2 Jul 2019 11:34:46 -0400 Subject: [PATCH 4/6] fix Horn's diameter properties frontDiameter and endDiameter can actually be changed, now. --- js/horn.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/js/horn.js b/js/horn.js index 3b775b1..889392a 100644 --- a/js/horn.js +++ b/js/horn.js @@ -74,6 +74,7 @@ HornGroup.prototype.renderHornSurface = function( ctx, renderer ) { var frontRadius = frontDiameter/2 * scale; var rearRadius = rearDiameter/2 * scale; // does apex extend beyond eclipse of face + apexDistance = apexDistance + frontRadius/4 + rearRadius/4; var isApexVisible = frontRadius * eccen < apexDistance && rearRadius * eccen < apexDistance; if ( !isApexVisible ) { @@ -204,6 +205,18 @@ Horn.prototype.create = function(/* options */) { }; +Horn.prototype.updateFrontCapDiameter = function(size) { + this.frontBase.stroke = size; + var baseZ = this.length/2; + this.frontBase.translate.z = (baseZ - size/2); +} + +Horn.prototype.updateRearCapDiameter = function(size) { + this.rearBase.stroke = size; + var baseZ = this.length/2; + this.rearBase.translate.z = (-baseZ + size/2); +} + // Horn shape does not render anything Horn.prototype.render = function() {}; @@ -222,6 +235,12 @@ childProperties.forEach( function( property ) { this[ _prop ] = value; // set property on children if ( this.frontBase ) { + if (property === 'frontDiameter') { + this.updateFrontCapDiameter(value); + } + if (property === 'rearDiameter') { + this.updateRearCapDiameter(value); + } this.frontBase[ property ] = value; this.rearBase[ property ] = value; this.group[ property ] = value; From 94ec7ef2921f2d2da0ae4d94532a25a266733867 Mon Sep 17 00:00:00 2001 From: bendablegears <32841526+bendablegears@users.noreply.github.com> Date: Tue, 2 Jul 2019 15:23:33 -0400 Subject: [PATCH 5/6] add Horn to index --- js/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/index.js b/js/index.js index e55d250..72f3256 100644 --- a/js/index.js +++ b/js/index.js @@ -24,6 +24,7 @@ require('./hemisphere'), require('./cylinder'), require('./cone'), + require('./horn'), require('./box') ); } else if ( typeof define == 'function' && define.amd ) { @@ -32,7 +33,7 @@ } })( this, function factory( Zdog, CanvasRenderer, SvgRenderer, Vector, Anchor, Dragger, Illustration, PathCommand, Shape, Group, Rect, RoundedRect, - Ellipse, Polygon, Hemisphere, Cylinder, Cone, Box ) { + Ellipse, Polygon, Hemisphere, Cylinder, Cone, Horn, Box ) { Zdog.CanvasRenderer = CanvasRenderer; Zdog.SvgRenderer = SvgRenderer; @@ -50,6 +51,7 @@ Zdog.Hemisphere = Hemisphere; Zdog.Cylinder = Cylinder; Zdog.Cone = Cone; + Zdog.Horn = Horn; Zdog.Box = Box; return Zdog; From 84870c7f4d297c45c96b86f820b76cc0d927c825 Mon Sep 17 00:00:00 2001 From: bendablegears <32841526+bendablegears@users.noreply.github.com> Date: Tue, 9 Jul 2019 13:24:24 -0400 Subject: [PATCH 6/6] Fix horn eccentricity calculation --- js/horn.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/js/horn.js b/js/horn.js index 889392a..687dc77 100644 --- a/js/horn.js +++ b/js/horn.js @@ -60,6 +60,8 @@ HornGroup.prototype.renderHornSurface = function( ctx, renderer ) { var rearBase = this.rearBase; var rearDiameter = rearBase.stroke; var scale = frontBase.renderNormal.magnitude(); + var frontRadius = frontDiameter/2 * scale; + var rearRadius = rearDiameter/2 * scale; this.renderApex.set( rearBase.renderOrigin ) .subtract( frontBase.renderOrigin ); @@ -70,9 +72,14 @@ HornGroup.prototype.renderHornSurface = function( ctx, renderer ) { var normalDistance = frontBase.renderNormal.magnitude2d(); // eccentricity var eccenAngle = Math.acos( normalDistance / scale ); - var eccen = Math.sin( eccenAngle ); - var frontRadius = frontDiameter/2 * scale; - var rearRadius = rearDiameter/2 * scale; + var biggerRadius = (frontRadius > rearRadius) ? frontRadius : rearRadius; + var eccenPercent; + if (frontRadius == 0 || rearRadius == 0) { + eccenPercent = 1.0; + } else { + eccenPercent = (Math.abs(frontRadius - rearRadius) / biggerRadius); + } + var eccen = Math.sin( eccenAngle ) * Math.sqrt(eccenPercent); // does apex extend beyond eclipse of face apexDistance = apexDistance + frontRadius/4 + rearRadius/4; var isApexVisible = frontRadius * eccen < apexDistance &&