Skip to content

Instantly share code, notes, and snippets.

@espinielli
Forked from mbostock/.block
Created Sep 28, 2017
Embed
What would you like to do?
Geodesic Rainbow
license: gpl-3.0

Use the range slider to change the degree of subdivision in this geodesic sphere. The base shape, visible when subdivision is disabled, is either the icosahedron or the tetrahedron.

Built with a modified version of the d3.geodesic plugin. Adapted to use D3v4.

(function() {
// ****** icosahedron ******
var φ = 1.618033988749895,
ρ = 180 / Math.PI;
var vertices_icosahedron = [
[1,φ,0], [-1,φ,0], [1,-φ,0], [-1,-φ,0],
[0,1,φ], [0,-1,φ], [0,1,-φ], [0,-1,-φ],
[φ,0,1], [-φ,0,1], [φ,0,-1], [-φ,0,-1]
];
// icosahedron faces
var faces_icosahedron = [
[0,1,4], [1,9,4], [4,9,5], [5,9,3], [2,3,7],
[3,2,5], [7,10,2], [0,8,10], [0,4,8], [8,2,10],
[8,4,5], [8,5,2], [1,0,6], [11,1,6], [3,9,11],
[6,10,7], [3,11,7], [11,6,7], [6,0,10], [9,1,11]
].map(function(face) {
return face.map(function(i) {
return vertices_icosahedron[i];
});
});
// ****** tetraahedron ******
var pi = Math.PI;
var degrees = 180 / pi;
var asin1_3 = Math.asin(1 / 3);
var vertices_tetrahedron = [
[0 , 0 , 1],
[Math.sqrt(8/9), 0 , -1/3],
[-Math.sqrt(2/9), Math.sqrt(2/3), -1/3],
[-Math.sqrt(2/9), -Math.sqrt(2/3), -1/3]
// [ 0, pi / 2],
// [-180, -asin1_3],
// [ -60, -asin1_3],
// [ 60, -asin1_3]
].map(function(vertex){
return vertex;
// return cartesian(vertex);
});
var faces_tetrahedron = [
[0,2,1],
[0,3,2],
[0,1,3],
[1,2,3]
].map(function(face) {
return face.map(function(i) {
return vertices_tetrahedron[i];
});
});
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);
})
};
}
};
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);
})
};
}
};
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 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) * ρ,
Math.acos(z / Math.sqrt(x * x + y * y + z * z)) * ρ - 90
];
}
})();
<!DOCTYPE html>
<meta charset="utf-8">
<style>
#subdivision {
position: absolute;
top: 20px;
left: 20px;
}
#subdivision input {
width: 200px;
}
#polyhedrontype {
position: absolute;
top: 50px;
left: 20px;
}
</style>
<div id="subdivision">
<input type="range" min="1" max="12" value="1">
<output name="subdivision"></output>
</div>
<div id="polyhedrontype">
<input type="radio" name="polyhedron" value="tetrahedron">tetrahedron<br>
<input type="radio" name="polyhedron" value="icosahedron" checked="checked">icosahedron<br>
</div>
<script src="//d3js.org/d3.v4.min.js"></script>
<!-- <script src="d3.min.js"></script> -->
<script src="d3.geodesic.js"></script>
<script>
var width = 960,
height = 500;
var velocity = [.010, .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);
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;
console.log(poly);
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.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, type) {
output.text(subdivision);
var polyhedron = (poly == "tetrahedron" ? d3.tetrahedron : 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment