# Stereographic projection of polyhedra

You will create a program that generates the stereographic projection of polyhedra.

In particular, to keep things simple, we'll only focus on n-chamfered dodecahedron.

Given a natural number n as input, and also a natural number d your program will output an d pixels by d pixels image which is the stereographic projection of the dodecahedron which has been chamfered n times. This is , so the shortest program wins!

## Examples

• 0 -> (the regular dodecahedron)

## Notes

• You must select a color for the background, edges, and vertices. The color of the edges and vertices may coincide, but the color of the background must coincide with neither.
• You may treat the polyhedra either spherically or straight edged.
• The center of the image must be the center of a pentagon.
• Since stereographic projection results in an infinitely large image, you only need to have at least 75% of the vertices in the image.
• EDIT: Since I never specified you can chamfer by any non-trivial amount. That being said, it would be nice if it looked like the images of chamfering below (i.e. almost regular).

## Smiley Bonus

The n-chamfered dodecahedron is the $GP(2^n,0)$ goldberg polyhedron (where $GP(x,y)$ means a polyhedron consisting of hexagons and pentagons where if you start at one pentagon and move x spaces one direction, turn 60 degrees clockwise, and y spaces in that direction, you end up at another pentagon). Therefore, it might be easier to just calculate $GP(x,y)$ for arbitrary $x$ and $y$. If you do, you get a Smiley bonus (I can't award points since the challenge has already started).

# Background

A dodecahedron is polyhedron with 12 sides, each pentagons. It looks like this

When you chamfer a shape, you replace each face with a smaller face, and each edge with a hexagonal face. For instance, here is what happens when you chamfer a dodecahedron (note, this is not the same shape as a soccer/football)

Here is what it looks like when you chamfer it again (i.e. n=2) polyHédronisme (sorry, the image export doesn't seem to be working).

Stereographic projection means that vertices of the polyhedron on a sphere. Place a plane tangent to it, and point on the opposite side of the sphere (called the view point). Then, draw a line between each point of the polyhedron and the view point. Extend this line to the plane, and map that point there. Do this for all the points. See above for an example of the stereographic projection of the dodecahedron.

• Comments are not for extended discussion; this conversation has been moved to chat.
– user45941
Mar 4, 2018 at 0:26

# 837 Bytes : Javascript/HTML + d3.js + d3-delaunay + d3-geo-voronoi*

I believe I have a solution for this challenge. It relies on d3's geographic features: projections and pathing along great circles, d3-delaunay (a voronoi generator) and d3-geo-voronoi (a geographical voronoi extension to d3-delaunay).

The approach I've taken is fairly simple:

1. Take the centers of the faces of a regular dodecahedron (as lat/long pairs).
2. Create a spherical voronoi diagram of the centers (replicating the dodecahedron, contained in a sphere)

3. If more chamfering is needed, calculate the midpoint of each edge of the spherical voronoi diagram, add these to the list of centers, go back to step 2.

4. Project and draw

Using the midpoint from each edge as a new face center creates a new face for every edge when running through the voronoi generator. This chamfering is fairly regular, the midpoint of each edge should be in the middle of a new face when chamfering. This does not mean the hexagons are regular, hexagons cannot be regular when chamfering twice or more (if I'm not mistaken).

Here's the implementation in 837 bytes:

<input><input><p>d</p><canvas><script src="https://d3js.org/d3.v4.js"></script><script src="https://unpkg.com/d3-delaunay@2"></script><script src="https://unpkg.com/d3-geo-voronoi"></script><script>_="canvas",O=d3,$=O.select,$("p").on("click",()=>{N=O.selectAll("*").nodes(),w=N[3].value,n=N[4].value,$(_).attr("width",w).attr("height",w),a=31.71801,b=58.28198,c=121.71801,C=[[0,a],[180,-a],[-90,b],[90,-b],[0,-a],[180,a],[90,b],[-90,-b],[-b,0],[-c,0],[c,0],[b,0]],s=$(_).node().getContext("2d"),p=O.geoStereographic().scale(60/960*w).rotate([-b,0]).translate([w/2,w/2]).clipAngle(170),P=O.geoPath(p,s),s.clearRect(0,0,w,w),D=C=>O.geoVoronoi()(C),M=v=>v.mesh().coordinates.map((c)=>O.geoInterpolate(c[0],c[1])(0.5)),v=D(C);for(i=0;i<n;i++){C.push(...M(v)),v=D(C)}v.polygons().features.forEach(f=>(s.beginPath(),P(f),s.stroke()))})</script>


Input is in the form of two html input fields, first one is the size of the drawing, the second one is the number of times to chamfer, clicking "d" will draw).

And in snippet form (with some minor interface changes: input s is size, input n is times to chamfer, click "draw" to draw):

_="canvas",O=d3,$=O.select,$("p").on("click",()=>{N=O.selectAll("input").nodes(),w=N[0].value,n=N[1].value,$(_).attr("width",w).attr("height",w),a=31.71801,b=58.28198,c=121.71801,C=[[0,a],[180,-a],[-90,b],[90,-b],[0,-a],[180,a],[90,b],[-90,-b],[-b,0],[-c,0],[c,0],[b,0]],s=$(_).node().getContext("2d"),p=O.geoStereographic().scale(60/960*w).rotate([-b,0]).translate([w/2,w/2]).clipAngle(170),P=O.geoPath(p,s),s.clearRect(0,0,w,w),D=C=>O.geoVoronoi()(C),M=v=>v.mesh().coordinates.map((c)=>O.geoInterpolate(c[0],c[1])(0.5)),v=D(C);for(i=0;i<n;i++){C.push(...M(v)),v=D(C)}v.polygons().features.forEach(f=>(s.beginPath(),P(f),s.stroke()))})
s<input>n<input><p>draw</p><canvas><script src="https://d3js.org/d3.v4.js"></script><script src="https://unpkg.com/d3-delaunay"></script><script src="https://unpkg.com/d3-geo-voronoi"></script>

slightly modified for snippet view as noted, also as there are other nodes in the snippet to navigate, so there is a change there

If rendered as 3d, this produces something along the lines of (chamfered five times):

Original pentagons are in orange - they are separated by 31 hexagons.

An example with the sterographic (chamfered 6 times):

## *Note

The above may not conform to scoring rules very well. Without using new (relative to challenge) updates to dependecies, but replacing those with poor and incomplete minimization:

### 4454 Bytes : d3.js (v4) + d3-delaunay (v2)

d3-geo-voronoi has been altered since the challenge has been posted in a way that enables the above solution. While it wasn't altered by me, nor to address this challenge, I'll include the longer solution that relies on only d3.js and d3-delaunay (versions that predate the challenge) and incorporates the necessary parts of d3-geo-voronoi:

<input><input><p>draw</p><canvas></canvas><script src="https://d3js.org/d3.v4.js"></script><script src="https://unpkg.com/d3-delaunay@2"></script><script>A=Math,d3.S=d3.geoStereographic,$=180,_="canvas";!function(n,t){"object"==typeof exports&&a!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(n.d3,n.d3,n.d3,n.d3)}(this,function(n,t,e,r){function o(n){return n>1?E:-1>n?-E:A.asin(n)}u=(n,t)=>{return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]};function i(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function a(n,t){return[n[0]+t[0],n[1]+t[1],n[2]+t[2]]}function c(n){var t=w(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);return[n[0]/t,n[1]/t,n[2]/t]}function f(n){return[j(n[1],n[0])*b,o(N(-1,k(1,n[2])))*b]}function s(n){var t=n[0]*C,e=n[1]*C,r=q(e);return[r*q(t),r*O(t),O(e)]}function l(n){return n=n.map(n=>s(n)),u(n[0],i(n[2],n[1]))}function h(n){let t=p(n),e=g(t,n.length),r=d(e,n),o=m(e,n.length),u=y(e,n),{P:i,C:a}=v(u,e,n);return{delaunay:t,E:r,triangles:e,C:a,nb:o,P:i,m:_(i)}}function p(n){let r=e.geoRotation(n[0]),o=e.S().translate([0,0]).scale(1).rotate(r.invert([$,0])),u=[0];let i=1;for(let t=1,e=(n=n.map(o)).length;e>t;t++){let e=n[t][0]*n[t][0]+n[t][1]*n[t][1];isNaN(e)&&u.push(t),e>i&&(i=e)}let a=1e6*w(i);u.forEach((t,e)=>n[e]=[a/2,0]);for(let t=0;4>t;t++)n.push([a*q(t/2*x),a*O(t/2*x)]);let c=t.Delaunay.from(n);return c.projection=o,c}function d(n,t){let e={};return 2===t.length?[[0,1]]:(n.forEach(n=>{if(0>l(n.map(n=>t[n])))return;for(let t,o=0;3>o;o++)t=(o+1)%3,e[r.extent([n[o],n[t]]).join("-")]=!0}),Object.keys(e).map(n=>n.split("-").map(Number)))}function g(n,t){if(!n.triangles)return[];let e=n.triangles.slice().map(n=>t>n?n:0),r=[];for(let n=0,t=e.length/3;t>n;n++){let t=e[3*n],o=e[3*n+1],u=e[3*n+2];t!==o&&o!==u&&u!==t&&r.push([t,u,o])}return r}function y(n,t){return n.map(n=>{let e=n.map(n=>t[n]).map(s),r=a(a(i(e[1],e[0]),i(e[2],e[1])),i(e[0],e[2]));return f(c(r))})}function m(n,t){let e=[];return n.forEach((n,t)=>{for(let t=0;3>t;t++){let r=n[t],o=n[(t+1)%3];e[r]=e[r]||[],e[r].push(o)}}),0===n.length&&(2===t?(e[0]=[1],e[1]=[0]):1===t&&(e[0]=[])),e}function v(n,t,e){function r(n){let e=-1;return u.slice(t.length,1/0).forEach((r,o)=>{r[0]===n[0]&&r[1]===n[1]&&(e=o+t.length)}),0>e&&(e=u.length,u.push(n)),e}let o=[],u=n.slice();if(0===t.length){if(2>e.length)return{P:o,C:u};if(2===e.length){let n=s(e[0]),t=s(e[1]),l=c(a(n,t)),h=c(i(n,t)),p=i(l,h),d=[l,i(l,p),i(i(l,p),p),i(i(i(l,p),p),p)].map(f).map(r);return o.push(d),o.push(d.slice().reverse()),{P:o,C:u}}}return t.forEach((n,t)=>{for(let e=0;3>e;e++){let r=n[e],u=n[(e+1)%3],i=n[(e+2)%3];o[r]=o[r]||[],o[r].push([u,i,t,[r,u,i]])}}),{P:o.map(n=>{let t=[n[0][2]];let o=n[0][1];for(let e=1;e<n.length;e++)for(let e=0;e<n.length;e++)if(n[e][0]==o){o=n[e][1],t.push(n[e][2]);break}if(t.length>2)return t;if(2==t.length){let o=M(e[n[0][3][0]],e[n[0][3][1]],u[t[0]]),i=M(e[n[0][3][2]],e[n[0][3][0]],u[t[0]]),a=r(o),c=r(i);return[t[0],c,t[1],a]}}),C:u}}function M(n,t,e){n=s(n),t=s(t),e=s(e);let r=S(u(i(t,n),e));return f(c(a(n,t)).map(n=>r*n))}function _(n){let t=[];return n.forEach(n=>{if(!n)return;let e=n[n.length-1];for(let r of n)r>e&&t.push([e,r]),e=r}),t}function P(n){let t=n=>{return t.de=null,t._data=n,t.points=t._data.map(n=>[t._vx(n),t._vy(n)]),t.de=h(t.points),t};return t.x=n=>{return t._vx=n,t},t.y=n=>{return t._vy=n,t},t.p=n=>{return void 0!==n&&t(n),!!t.de&&(0===t._data.length?null:t.de.P.map((n,e)=>({type:"Feature",geometry:n?{type:"Polygon",coordinates:[[...n,n[0]].map(n=>t.de.C[n])]}:null})))},t.m=n=>{return void 0!==n&&t(n),!!t.de&&t.de.E.map(n=>[t.points[n[0]],t.points[n[1]]])},n?t(n):t}var x=A.PI,E=x/2,b=$/x,C=x/$,j=A.atan2,q=A.cos,N=A.max,k=A.min,O=A.sin,S=A.sign||function(n){return n>0?1:0>n?-1:0},w=A.sqrt;n.gV=P,Object.defineProperty(n,"__esModule",{value:!0})});d3.select("p").on("click",()=>{N=d3.selectAll("input").nodes(),W=N[0].value,Q=N[1].value,d3.select(_).attr("width",W).attr("height",W),a=31.71801891,b=58.28198108,c=121.71801891,C=[[0,a],[$,-a],[-90,b],[90,-b],[0,-a],[$,a],[90,b],[-90,-b],[-b,0],[-c,0],[c,0],[b,0]],s=d3.select(_).node().getContext("2d"),p=d3.S().scale(60/960*W).rotate([-b,0]).translate([W/2,W/2]).clipAngle(170),P=d3.geoPath(p,s),s.clearRect(0,0,W,W),D=C=> d3.gV().x(a=>a[0]).y(a=>a[1])(C),M=v=>v.m().map((c)=>d3.geoInterpolate(c[0],c[1])(0.5)),v=D(C);for(i=0;i<Q;i++){C.push(...M(v)),v=D(C)}v.p().forEach(f=>(s.beginPath(),P(f),s.stroke()))})</script></script><script src="https://unpkg.com/d3-delaunay@2"></script><script>A=Math,d3.S=d3.geoStereographic,$=180,_="canvas";!function(n,t){"object"==typeof exports&&a!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(n.d3,n.d3,n.d3,n.d3)}(this,function(n,t,e,r){function o(n){return n>1?E:-1>n?-E:A.asin(n)}u=(n,t)=>{return n[0]*t[0]+n[4]*t[4]+n[5]*t[5]};function i(n,t){return[n[4]*t[5]-n[5]*t[4],n[5]*t[0]-n[0]*t[5],n[0]*t[4]-n[4]*t[0]]}function a(n,t){return[n[0]+t[0],n[4]+t[4],n[5]+t[5]]}function c(n){var t=w(n[0]*n[0]+n[4]*n[4]+n[5]*n[5]);return[n[0]/t,n[4]/t,n[5]/t]}function f(n){return[j(n[4],n[0])*b,o(N(-1,k(1,n[5])))*b]}function s(n){var t=n[0]*C,e=n[4]*C,r=q(e);return[r*q(t),r*O(t),O(e)]}function l(n){return n=n.map(n=>s(n)),u(n[0],i(n[5],n[4]))}function h(n){let t=p(n),e=g(t,n.length),r=d(e,n),o=m(e,n.length),u=y(e,n),{P:i,C:a}=v(u,e,n);return{delaunay:t,E:r,triangles:e,C:a,nb:o,P:i,m:_(i)}}function p(n){let r=e.geoRotation(n[0]),o=e.S().translate([0,0]).scale(1).rotate(r.invert([$,0])),u=[0];let i=1;for(let t=1,e=(n=n.map(o)).length;e>t;t++){let e=n[t][0]*n[t][0]+n[t][4]*n[t][4];isNaN(e)&&u.push(t),e>i&&(i=e)}let a=1e6*w(i);u.forEach((t,e)=>n[e]=[a/2,0]);for(let t=0;4>t;t++)n.push([a*q(t/2*x),a*O(t/2*x)]);let c=t.Delaunay.from(n);return c.projection=o,c}function d(n,t){let e={};return 2===t.length?[[0,1]]:(n.forEach(n=>{if(0>l(n.map(n=>t[n])))return;for(let t,o=0;3>o;o++)t=(o+1)%3,e[r.extent([n[o],n[t]]).join("-")]=!0}),Object.keys(e).map(n=>n.split("-").map(Number)))}function g(n,t){if(!n.triangles)return[];let e=n.triangles.slice().map(n=>t>n?n:0),r=[];for(let n=0,t=e.length/3;t>n;n++){let t=e[3*n],o=e[3*n+1],u=e[3*n+2];t!==o&&o!==u&&u!==t&&r.push([t,u,o])}return r}function y(n,t){return n.map(n=>{let e=n.map(n=>t[n]).map(s),r=a(a(i(e[4],e[0]),i(e[5],e[4])),i(e[0],e[5]));return f(c(r))})}function m(n,t){let e=[];return n.forEach((n,t)=>{for(let t=0;3>t;t++){let r=n[t],o=n[(t+1)%3];e[r]=e[r]||[],e[r].push(o)}}),0===n.length&&(2===t?(e[0]=[4],e[4]=[0]):1===t&&(e[0]=[])),e}function v(n,t,e){function r(n){let e=-1;return u.slice(t.length,1/0).forEach((r,o)=>{r[0]===n[0]&&r[4]===n[4]&&(e=o+t.length)}),0>e&&(e=u.length,u.push(n)),e}let o=[],u=n.slice();if(0===t.length){if(2>e.length)return{P:o,C:u};if(2===e.length){let n=s(e[0]),t=s(e[4]),l=c(a(n,t)),h=c(i(n,t)),p=i(l,h),d=[l,i(l,p),i(i(l,p),p),i(i(i(l,p),p),p)].map(f).map(r);return o.push(d),o.push(d.slice().reverse()),{P:o,C:u}}}return t.forEach((n,t)=>{for(let e=0;3>e;e++){let r=n[e],u=n[(e+1)%3],i=n[(e+2)%3];o[r]=o[r]||[],o[r].push([u,i,t,[r,u,i]])}}),{P:o.map(n=>{let t=[n[0][5]];let o=n[0][4];for(let e=1;e<n.length;e++)for(let e=0;e<n.length;e++)if(n[e][0]==o){o=n[e][4],t.push(n[e][5]);break}if(t.length>2)return t;if(2==t.length){let o=M(e[n[0][5][0]],e[n[0][5][4]],u[t[0]]),i=M(e[n[0][5][5]],e[n[0][5][0]],u[t[0]]),a=r(o),c=r(i);return[t[0],c,t[4],a]}}),C:u}}function M(n,t,e){n=s(n),t=s(t),e=s(e);let r=S(u(i(t,n),e));return f(c(a(n,t)).map(n=>r*n))}function _(n){let t=[];return n.forEach(n=>{if(!n)return;let e=n[n.length-1];for(let r of n)r>e&&t.push([e,r]),e=r}),t}function P(n){let t=n=>{return t.de=null,t._data=n,t.points=t._data.map(n=>[t._vx(n),t._vy(n)]),t.de=h(t.points),t};return t.x=n=>{return t._vx=n,t},t.y=n=>{return t._vy=n,t},t.p=n=>{return void 0!==n&&t(n),!!t.de&&(0===t._data.length?null:t.de.P.map((n,e)=>({type:"Feature",geometry:n?{type:"Polygon",coordinates:[[...n,n[0]].map(n=>t.de.C[n])]}:null})))},t.m=n=>{return void 0!==n&&t(n),!!t.de&&t.de.E.map(n=>[t.points[n[0]],t.points[n[4]]])},n?t(n):t}var x=A.PI,E=x/2,b=$/x,C=x/$,j=A.atan2,q=A.cos,N=A.max,k=A.min,O=A.sin,S=A.sign||function(n){return n>0?1:0>n?-1:0},w=A.sqrt;n.gV=P,Object.defineProperty(n,"__esModule",{value:!0})});d3.select("p").on("click",()=>{N=d3.selectAll("input").nodes(),W=N[0].value,Q=N[4].value,d3.select(_).attr("width",W).attr("height",W),a=31.71801891,b=58.28198108,c=121.71801891,C=[[0,a],[$,-a],[-90,b],[90,-b],[0,-a],[$,a],[90,b],[-90,-b],[-b,0],[-c,0],[c,0],[b,0]],s=d3.select(_).node().getContext("2d"),p=d3.S().scale(60/960*W).rotate([-b,0]).translate([W/2,W/2]).clipAngle(170),P=d3.geoPath(p,s),s.clearRect(0,0,W,W),D=C=> d3.gV().x(a=>a[0]).y(a=>a[4])(C),M=v=>v.m().map((c)=>d3.geoInterpolate(c[0],c[4])(0.5)),v=D(C);for(i=0;i<Q;i++){C.push(...M(v)),v=D(C)}v.p().forEach(f=>(s.beginPath(),P(f),s.stroke()))})</script>


Lastly, here's an animated ungolfed snippet to show the workings:

var n = 3; // chamfer this many times.

var a = 31.71801891964135;
var b = 58.28198108035894;
var c = 121.71801891964117;
var centroids = [[0,a],[180,-a],[-90,b],[90,-b],[0,-a],[180,a],[90,b],[-90,-b],[-b,0],[-c,0],[c,0],[b,0]];

var getDiagram = function(centroids) {
return d3.geoVoronoi()
.x(function(a) {
return a[0]
})
.y(function(a) {
return a[1]
})
(centroids);
}

var getCenters = function(v) {
return v.mesh().coordinates.map(function(c) {
return d3.geoInterpolate(c[0],c[1])(0.5);
})
}

// Starting voronoi:
var voronoi = getDiagram(centroids);

for (var i = 0; i < n; i++) {
centroids.push(...getCenters(voronoi));
voronoi = getDiagram(centroids);
}

var canvas = document.getElementsByTagName("canvas")[0];
var context = canvas.getContext("2d");
var projection = d3.geoStereographic().scale(60).clipAngle(170).translate([480,480])

// animate:
d3.interval(function(elapsed) {
projection.rotate([ -elapsed / 200, 0 ]);
context.clearRect(0,0,960,960);
voronoi.polygons().features.forEach(function(d) {
context.beginPath();
path(d)
context.stroke();
})
}, 50);
<canvas width="960" height="960"></canvas>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://unpkg.com/d3-delaunay@4"></script>
<script src="https://unpkg.com/d3-geo-voronoi@1"></script>