Skip to content

Instantly share code, notes, and snippets.

@espinielli
Last active April 11, 2023 06:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save espinielli/ddae4eceaca841f229d63cc91c2c03c3 to your computer and use it in GitHub Desktop.
Save espinielli/ddae4eceaca841f229d63cc91c2c03c3 to your computer and use it in GitHub Desktop.
Platonic Geodesic

Use the range slider to change the degree of subdivision in this geodesic sphere.

The base shape, visible when subdivision is disabled, is one of the 5 regular convex polyhedra (with triangularized faces for cube and dodecahedron), a.k.a. Platonic solids:

  • tethrahedron
  • hexahedron (cube)
  • octahedron
  • dodecahedron
  • icosahedron

Built with a modified version of the d3.geodesic plugin which adds all Platonic solids.

Adapted to use D3v4.

Evolved from my previous block which was as well inspired by Mike Bostock's Geodesic Rainbow.

(function() {
var phi = (1 + Math.sqrt(5)) / 2, // golden ratio
pi = Math.PI,
a, b, c, r;
var degrees = 180 / pi;
// faces for platonic solids from http://paulbourke.net/geometry/platonic/
// (some faces vertices order reverted to make the relavant sequence clockwise)
// ****** tetrahedron ******
r = Math.sqrt(3);
var vertices_tetrahedron = [
[ 1, 1, 1],
[-1, 1, -1],
[ 1, -1, -1],
[-1, -1, 1]
].map(function(vertex) {
return [vertex[0] / r, vertex[1] /r, vertex[2] /r];
});
var vertices_faces_tetrahedron = [
0, 1, 2,
1, 3, 2,
0, 2, 3,
0, 3, 1,
].map(function(idx) {
return vertices_tetrahedron[idx];
});
var faces_tetrahedron = chunk(vertices_faces_tetrahedron, 3);
d3.tetrahedron = {
vertices: function() {
return vertices_tetrahedron;
},
faces: function() {
return faces_tetrahedron;
},
multipolygon: function(n) {
return {
type: "MultiPolygon",
coordinates: subdivideFaces(~~n, this).map(function(face) {
face = face.map(project);
face.push(face[0]);
face = [face];
return face;
})
};
},
polygons: function(n) {
return d3.tetrahedron.multipolygon(~~n).coordinates.map(function(face) {
return {type: "Polygon", coordinates: face};
});
},
multilinestring: function(n) {
return {
type: "MultiLineString",
coordinates: subdivideEdges(~~n, this).map(function(edge) {
return edge.map(project);
})
};
}
};
// ****** octahedron ******
a = 1 / (2 * Math.sqrt(2));
b = 1 / 2;
r = b;
var vertices_octahedron = [
[-a, 0, a],
[-a, 0, -a],
[ 0, b, 0],
[ a, 0, -a],
[ 0, -b, 0],
[ a, 0, a]
].map(function(vertex) {
return [vertex[0] / r, vertex[1] /r, vertex[2] /r];
});
var vertices_faces_octahedron = [
0, 1, 2,
1, 3, 2,
3, 5, 2,
5, 0, 2,
3, 1, 4,
1, 0, 4,
5, 3, 4,
0, 5, 4,
].map(function(idx) {
return vertices_octahedron[idx];
});
var faces_octahedron = chunk(vertices_faces_octahedron, 3);
d3.octahedron = {
vertices: function() {
return vertices_octahedron;
},
faces: function() {
return faces_octahedron;
},
multipolygon: function(n) {
return {
type: "MultiPolygon",
coordinates: subdivideFaces(~~n, this).map(function(face) {
face = face.map(project);
face.push(face[0]);
face = [face];
return face;
})
};
},
polygons: function(n) {
return d3.octahedron.multipolygon(~~n).coordinates.map(function(face) {
return {type: "Polygon", coordinates: face};
});
},
multilinestring: function(n) {
return {
type: "MultiLineString",
coordinates: subdivideEdges(~~n, this).map(function(edge) {
return edge.map(project);
})
};
}
};
// ****** hexahedron (cube) ******
r = Math.sqrt(3);
var vertices_hexahedron = [
[-1, -1, -1],
[ 1, -1, -1],
[ 1, -1, 1],
[-1, -1, 1],
[-1, 1, -1],
[ 1, 1, -1],
[ 1, 1, 1],
[-1, 1, 1]
].map(function(vertex) {
return [vertex[0] / r, vertex[1] /r, vertex[2] /r];
});
var vertices_faces_hexahedron = [
0, 3, 2, 1,
3, 0, 4, 7,
3, 7, 6, 2,
4, 5, 6, 7,
5, 1, 2, 6,
0, 1, 5, 4
].map(function(idx) {
return vertices_hexahedron[idx];
});
var faces_hexahedron = chunk(vertices_faces_hexahedron, 4)
// split each square into two triangles
.reduce(
function(accumulator, face, faceIndex, faces) {
accumulator.push(
[face[0], face[1], face[3]],
[face[1], face[2], face[3]]);
return accumulator;
},
[]
);
d3.hexahedron = {
vertices: function() {
return vertices_hexahedron;
},
faces: function() {
return faces_hexahedron;
},
multipolygon: function(n) {
return {
type: "MultiPolygon",
coordinates: subdivideFaces(~~n, this).map(function(face) {
face = face.map(project);
face.push(face[0]);
face = [face];
return face;
})
};
},
polygons: function(n) {
return d3.hexahedron.multipolygon(~~n).coordinates.map(function(face) {
return {type: "Polygon", coordinates: face};
});
},
multilinestring: function(n) {
return {
type: "MultiLineString",
coordinates: subdivideEdges(~~n, this).map(function(edge) {
return edge.map(project);
})
};
}
};
// ****** dodecahedron ******
b = 1 / phi,
c = 2 - phi,
r = b * Math.sqrt(3);
var vertices_dodecahedron = [
[ c, 0, 1],
[-c, 0, 1],
[-b, b, b],
[ 0, 1, c],
[ b, b, b],
[ b, -b, b],
[ 0, -1, c],
[-b, -b, b],
[ c, 0, -1],
[-c, 0, -1],
[-b, -b, -b],
[ 0, -1, -c],
[ b, -b, -b],
[ b, b, -b],
[ 0, 1, -c],
[-b, b, -b],
[ 1, c, 0],
[-1, c, 0],
[-1, -c, 0],
[ 1, -c, 0]
].map(function(vertex) {
return [vertex[0] / r, vertex[1] /r, vertex[2] /r];
});
var vertices_faces_dodecahedron = [
0, 1, 2, 3, 4,
1, 0, 5, 6, 7,
8, 9, 10, 11, 12,
9, 8, 13, 14, 15,
13, 16, 4, 3, 14,
2, 17, 15, 14, 3,
10, 18, 7, 6, 11,
5, 19, 12, 11, 6,
16, 19, 5, 0, 4,
19, 16, 13, 8, 12,
17, 18, 10, 9, 15,
18, 17, 2, 1, 7
].map(function(idx) {
return vertices_dodecahedron[idx];
});
var faces_dodecahedron = chunk(vertices_faces_dodecahedron, 5)
.reduce(
function(accumulator, face, faceIndex, faces) {
accumulator.push(
[face[0], face[1], face[4]],
[face[1], face[2], face[4]],
[face[2], face[3], face[4]]);
return accumulator;
},
[]
);
d3.dodecahedron = {
vertices: function() {
return vertices_dodecahedron;
},
faces: function() {
return faces_dodecahedron;
},
multipolygon: function(n) {
return {
type: "MultiPolygon",
coordinates: subdivideFaces(~~n, this).map(function(face) {
face = face.map(project);
face.push(face[0]);
face = [face];
return face;
})
};
},
polygons: function(n) {
return d3.dodecahedron.multipolygon(~~n).coordinates.map(function(face) {
return {type: "Polygon", coordinates: face};
});
},
multilinestring: function(n) {
return {
type: "MultiLineString",
coordinates: subdivideEdges(~~n, this).map(function(edge) {
return edge.map(project);
})
};
}
};
// ****** icosahedron ******
a = 0.5,
b = 1 / (2 * phi),
r = Math.sqrt(a * a + b * b);
var vertices_icosahedron = [
[ 0, b, -a],
[ b, a, 0],
[-b, a, 0],
[ 0, b, a],
[ 0, -b, a],
[-a, 0, b],
[ a, 0, b],
[ 0, -b, -a],
[ a, 0, -b],
[-a, 0, -b],
[ b, -a, 0],
[-b, -a, 0]
].map(function(vertex) {
return [vertex[0] / r, vertex[1] /r, vertex[2] /r];
});
var vertices_faces_icosahedron = [
0, 1, 2,
3, 2, 1,
3, 4, 5,
3, 6, 4,
0, 7, 8,
0, 9, 7,
4, 10, 11,
7, 11, 10,
2, 5, 9,
11, 9, 5,
1, 8, 6,
10, 6, 8,
3, 5, 2,
3, 1, 6,
0, 2, 9,
0, 8, 1,
7, 9, 11,
7, 10, 8,
4, 11, 5,
4, 6, 10,
].map(function(idx) {
return vertices_icosahedron[idx];
});
var faces_icosahedron = chunk(vertices_faces_icosahedron, 3);
d3.icosahedron = {
vertices: function() {
return vertices_icosahedron;
},
faces: function() {
return faces_icosahedron;
},
multipolygon: function(n) {
return {
type: "MultiPolygon",
coordinates: subdivideFaces(~~n, this).map(function(face) {
face = face.map(project);
face.push(face[0]);
face = [face];
return face;
})
};
},
polygons: function(n) {
return d3.icosahedron.multipolygon(~~n).coordinates.map(function(face) {
return {type: "Polygon", coordinates: face};
});
},
multilinestring: function(n) {
return {
type: "MultiLineString",
coordinates: subdivideEdges(~~n, this).map(function(edge) {
return edge.map(project);
})
};
}
};
function subdivideFaces(n, polyhedron) {
return d3.merge(polyhedron.faces().map(function(face) {
var i01 = interpolate(face[0], face[1]),
i02 = interpolate(face[0], face[2]),
faces = [];
faces.push([
face[0],
i01(1 / n),
i02(1 / n)
]);
for (var i = 1; i < n; ++i) {
var i1 = interpolate(i01(i / n), i02(i / n)),
i2 = interpolate(i01((i + 1) / n), i02((i + 1) / n));
for (var j = 0; j <= i; ++j) {
faces.push([
i1(j / i),
i2(j / (i + 1)),
i2((j + 1) / (i + 1))
]);
}
for (var j = 0; j < i; ++j) {
faces.push([
i1(j / i),
i2((j + 1) / (i + 1)),
i1((j + 1) / i)
]);
}
}
return faces;
}));
}
function subdivideEdges(n, polyhedron) {
var edges = {};
subdivideFaces(n, polyhedron).forEach(function(face) {
add(face[0], face[1]);
add(face[1], face[2]);
add(face[2], face[0]);
});
function add(p0, p1) {
var t;
if (p0[0] < p1[0] || (p0[0] == p1[0] && (p0[1] < p1[1] || (p0[1] == p1[1] && p0[2] < p1[2])))) t = p0, p0 = p1, p1 = t;
polyhedron.edges()[p0.map(round) + " " + p1.map(round)] = [p0, p1];
}
function round(d) {
return d3.round(d, 4);
}
return d3.values(edges);
}
function chunk(arr, n) {
return arr.reduce(function(p, cur, i) {
(p[i/n|0] || (p[i/n|0] = [])).push(cur);
return p;
},[]);
};
function centroid(polygon) {
var k = polygon.length;
var c = polygon.reduce(function(accumulator, item) {
return [
accumulator[0] + item[0],
accumulator[1] + item[1],
accumulator[2] + item[2]
];
}, [0, 0, 0]);
return [c[0]/k, c[1]/k, c[2]/k];
}
function interpolate(p0, p1) {
var x0 = p0[0],
y0 = p0[1],
z0 = p0[2],
x1 = p1[0] - x0,
y1 = p1[1] - y0,
z1 = p1[2] - z0;
return function(t) {
return [
x0 + t * x1,
y0 + t * y1,
z0 + t * z1
];
};
}
function cartesian(spherical) {
var lambda = spherical[0], phi = spherical[1], cosPhi = Math.cos(phi);
return [cosPhi * Math.cos(lambda), cosPhi * Math.sin(lambda), Math.sin(phi)];
}
function project(p) {
var x = p[0],
y = p[1],
z = p[2];
return [
Math.atan2(y, x) * degrees,
Math.acos(z / Math.sqrt(x * x + y * y + z * z)) * degrees - 90
];
}
function polygonArea(polygon) {
var i = -1,
n = polygon.length,
a,
b = polygon[n - 1],
area = 0;
while (++i < n) {
a = b;
b = polygon[i];
area += a[1] * b[0] - a[0] * b[1];
}
return area / 2;
}
// v1 - v2
function vectorDifference(v1, v2) {
return [
v1[0] - v2[0],
v1[1] - v2[1],
v1[2] - v2[2]
];
}
// v1 x v2
function vectorCrossproduct(v1, v2) {
return [
v1[1] * v2[2] - v1[2] * v2[1],
v1[2] * v2[0] - v1[0] * v2[2],
v1[0] * v2[1] - v1[1] * v2[0]
];
}
function vectorLength(v) {
return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
}
function triangleArea(triangle) {
var p0 = triangle[0],
p1 = triangle[1],
p2 = triangle[2];
var d1 = vectorDifference(p1, p0),
d2 = vectorDifference(p2, p0);
var c = vectorCrossproduct(d1, d2),
l = vectorLength(c);
return l / 2;
}
function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
})();
<!DOCTYPE html>
<meta charset="utf-8">
<style>
#instructions {
position: absolute;
top: 20px;
left: 250px;
}
#subdivision {
position: absolute;
top: 10px;
left: 20px;
}
#subdivision input {
width: 200px;
}
#polyhedrontype {
position: absolute;
top: 40px;
left: 20px;
}
</style>
<div id="subdivision">
<input type="range" min="1" max="16" value="4">
<output name="subdivision"></output>
</div>
<div id="polyhedrontype">
<input type="radio" name="polyhedron" value="tetrahedron">tetrahedron<br>
<input type="radio" name="polyhedron" value="octahedron">octaedron<br>
<input type="radio" name="polyhedron" value="hexahedron">hexahedron (cube)<br>
<input type="radio" name="polyhedron" value="dodecahedron">dodecahedron<br>
<input type="radio" name="polyhedron" value="icosahedron" checked="checked">icosahedron<br>
</div>
<div id="instructions"><p>Drag to rotate!</p></div>
<script src="//d3js.org/d3.v4.min.js"></script>
<!-- <script src="d3.min.js"></script> -->
<script src="d3.geodesic.js"></script>
<script src="versor.js"></script>
<script>
var width = 960,
height = 500;
// var velocity = [.030, .005],
// t0 = Date.now();
var projection = d3.geoOrthographic()
.scale(height / 2 - 10);
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
canvas.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged));
var render = function() {},
v0, // Mouse position in Cartesian coordinates at start of drag gesture.
r0, // Projection rotation as Euler angles at start.
q0; // Projection rotation as versor at start.
function dragstarted() {
v0 = versor.cartesian(projection.invert(d3.mouse(this)));
r0 = projection.rotate();
q0 = versor(r0);
}
function dragged() {
var v1 = versor.cartesian(projection.rotate(r0).invert(d3.mouse(this))),
q1 = versor.multiply(q0, versor.delta(v0, v1)),
r1 = versor.rotation(q1);
projection.rotate(r1);
redraw();
}
var context = canvas.node().getContext("2d");
context.strokeStyle = "#000";
context.lineWidth = .5;
var faces;
var output = d3.select("output");
var poly = "icosahedron";
var subdivision = 1;
var input_subdivision = d3.select("#subdivision input")
.on("change", function() {
subdivision = +this.value;
geodesic(subdivision, poly);
})
.each(function() {
subdivision = +this.value;
geodesic(subdivision, poly);
});
var input_poly = d3.selectAll("input[name='polyhedron']")
.on("change", function() {
poly = this.value;
geodesic(subdivision, poly);
});
// d3.timer(function() {
// var time = Date.now() - t0;
// projection.rotate([time * velocity[0], time * velocity[1]]);
// redraw();
// });
function redraw() {
context.clearRect(0, 0, width, height);
faces.forEach(function(d) {
d.polygon[0] = projection(d[0]);
d.polygon[1] = projection(d[1]);
d.polygon[2] = projection(d[2]);
if (d.visible = d3.polygonArea(d.polygon) > 0) {
context.fillStyle = d.fill;
context.beginPath();
drawTriangle(d.polygon);
context.fill();
}
});
context.beginPath();
faces.forEach(function(d) {
if (d.visible) {
drawTriangle(d.polygon);
}
});
context.strokeStyle = "black";
context.stroke();
}
function drawTriangle(triangle) {
context.moveTo(triangle[0][0], triangle[0][1]);
context.lineTo(triangle[1][0], triangle[1][1]);
context.lineTo(triangle[2][0], triangle[2][1]);
context.closePath();
}
function geodesic(subdivision, poly) {
output.text(subdivision);
var polyhedron;
switch(poly) {
case "tetrahedron":
polyhedron = d3.tetrahedron;
break;
case "hexahedron":
polyhedron = d3.hexahedron;
break;
case "octahedron":
polyhedron = d3.octahedron;
break;
case "dodecahedron":
polyhedron = d3.dodecahedron;
break;
default:
polyhedron = d3.icosahedron;
}
faces = polyhedron.polygons(subdivision).map(function(d) {
d = d.coordinates[0];
d.pop(); // use an open polygon
d.fill = d3.hsl(d[0][0], 1, .5) + "";
d.polygon = d.map(projection);
return d;
});
redraw();
}
</script>
// Version 0.0.0. Copyright 2017 Mike Bostock.
(function(global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.versor = factory());
}(this, (function() {'use strict';
var acos = Math.acos,
asin = Math.asin,
atan2 = Math.atan2,
cos = Math.cos,
max = Math.max,
min = Math.min,
PI = Math.PI,
sin = Math.sin,
sqrt = Math.sqrt,
radians = PI / 180,
degrees = 180 / PI;
// Returns the unit quaternion for the given Euler rotation angles [λ, φ, γ].
function versor(e) {
var l = e[0] / 2 * radians, sl = sin(l), cl = cos(l), // λ / 2
p = e[1] / 2 * radians, sp = sin(p), cp = cos(p), // φ / 2
g = e[2] / 2 * radians, sg = sin(g), cg = cos(g); // γ / 2
return [
cl * cp * cg + sl * sp * sg,
sl * cp * cg - cl * sp * sg,
cl * sp * cg + sl * cp * sg,
cl * cp * sg - sl * sp * cg
];
}
// Returns Cartesian coordinates [x, y, z] given spherical coordinates [λ, φ].
versor.cartesian = function(e) {
var l = e[0] * radians, p = e[1] * radians, cp = cos(p);
return [cp * cos(l), cp * sin(l), sin(p)];
};
// Returns the Euler rotation angles [λ, φ, γ] for the given quaternion.
versor.rotation = function(q) {
return [
atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * degrees,
asin(max(-1, min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * degrees,
atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * degrees
];
};
// Returns the quaternion to rotate between two cartesian points on the sphere.
versor.delta = function(v0, v1) {
var w = cross(v0, v1), l = sqrt(dot(w, w));
if (!l) return [1, 0, 0, 0];
var t = acos(max(-1, min(1, dot(v0, v1)))) / 2, s = sin(t); // t = θ / 2
return [cos(t), w[2] / l * s, -w[1] / l * s, w[0] / l * s];
};
// Returns the quaternion that represents q0 * q1.
versor.multiply = function(q0, q1) {
return [
q0[0] * q1[0] - q0[1] * q1[1] - q0[2] * q1[2] - q0[3] * q1[3],
q0[0] * q1[1] + q0[1] * q1[0] + q0[2] * q1[3] - q0[3] * q1[2],
q0[0] * q1[2] - q0[1] * q1[3] + q0[2] * q1[0] + q0[3] * q1[1],
q0[0] * q1[3] + q0[1] * q1[2] - q0[2] * q1[1] + q0[3] * q1[0]
];
};
function cross(v0, v1) {
return [
v0[1] * v1[2] - v0[2] * v1[1],
v0[2] * v1[0] - v0[0] * v1[2],
v0[0] * v1[1] - v0[1] * v1[0]
];
}
function dot(v0, v1) {
return v0[0] * v1[0] + v0[1] * v1[1] + v0[2] * v1[2];
}
return versor;
})));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment