Forked from another example, that was in turn forked from one by Mike Bostock.
Click and drag above to increase hexagons’ height, forming hills and mountains. 10 is the maximum height. Press shift when clicking to decrease it. You can even create lakes and coastlines. 10 is the maximum depth.
Contour lines are computed automatically, by leveraging TopoJSON’s mesh
function.
(function() {
var global, hexProjection, hexTopology, mousedown, mousemove, mouseup, redraw,
__indexOf = Array.prototype.indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
global = {};
window.main = function() {
var height, radius, svg, width;
width = 960;
height = 500;
radius = 14;
global.MAX_DEPTH = 10;
global.MID_HEIGHT = 4;
global.MAX_HEIGHT = 10;
global.mousing = 0;
/* array of touched hexes, to avoid modifying the height of the same hexes in the same drag
*/
global.touched = [];
global.hex_topology = hexTopology(radius, width, height);
global.path_generator = d3.geo.path().projection(hexProjection(radius));
global.color_scale = d3.scale.linear().domain([-global.MAX_DEPTH, -1, 0, global.MID_HEIGHT, global.MAX_HEIGHT]).range([d3.rgb(5, 48, 97), d3.rgb(198, 219, 239), d3.rgb(230, 245, 208), d3.rgb(102, 189, 99), d3.rgb(84, 48, 5)]).interpolate(d3.interpolateHcl);
svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
svg.append('g').attr('class', 'hexagon').selectAll('path').data(global.hex_topology.objects.hexagons.geometries).enter().append('path').attr('d', function(d) {
return global.path_generator(topojson.feature(global.hex_topology, d));
}).style('fill', function(d) {
return global.color_scale(d.height);
}).on('mousedown', mousedown).on('mousemove', mousemove).on('mouseup', mouseup);
svg.append('path').datum(topojson.mesh(global.hex_topology, global.hex_topology.objects.hexagons)).attr('class', 'mesh').attr('d', global.path_generator);
return global.border = svg.append('path').attr('class', 'border').call(redraw);
};
/* create the hex mesh TopoJSON
*/
hexTopology = function(radius, width, height) {
var arcs, dx, dy, geometries, i, j, m, n, q, x, y;
dx = radius * 2 * Math.sin(Math.PI / 3);
dy = radius * 1.5;
m = Math.ceil((height + radius) / dy) + 1;
n = Math.ceil(width / dx) + 1;
geometries = [];
arcs = [];
for (j = -1; -1 <= m ? j <= m : j >= m; -1 <= m ? j++ : j--) {
for (i = -1; -1 <= n ? i <= n : i >= n; -1 <= n ? i++ : i--) {
y = j * 2;
x = (i + (j & 1) / 2) * 2;
arcs.push([[x, y - 1], [1, 1]], [[x + 1, y], [0, 1]], [[x + 1, y + 1], [-1, 1]]);
}
}
q = 3;
for (j = 0; 0 <= m ? j < m : j > m; 0 <= m ? j++ : j--) {
for (i = 0; 0 <= n ? i < n : i > n; 0 <= n ? i++ : i--) {
geometries.push({
type: 'Polygon',
arcs: [[q, q + 1, q + 2, ~(q + (n + 2 - (j & 1)) * 3), ~(q - 2), ~(q - (n + 2 + (j & 1)) * 3 + 2)]],
height: 0
});
q += 3;
}
q += 6;
}
return {
transform: {
translate: [0, 0],
scale: [1, 1]
},
objects: {
hexagons: {
type: 'GeometryCollection',
geometries: geometries
}
},
arcs: arcs
};
};
/* define a custom projection to make hexagons appear regular
*/
hexProjection = function(radius) {
var dx, dy;
dx = radius * 2 * Math.sin(Math.PI / 3);
dy = radius * 1.5;
return {
stream: function(stream) {
return {
point: (function(x, y) {
return stream.point(x * dx / 2, (y - (2 - (y & 1)) / 3) * dy / 2);
}),
lineStart: (function() {
return stream.lineStart();
}),
lineEnd: (function() {
return stream.lineEnd();
}),
polygonStart: (function() {
return stream.polygonStart();
}),
polygonEnd: (function() {
return stream.polygonEnd();
})
};
}
};
};
/* user interaction callbacks
*/
mousemove = function(d) {
if (global.mousing && __indexOf.call(global.touched, d) < 0) {
global.touched.push(d);
/* update the height of the tile
*/
d.height = Math.max(-global.MAX_DEPTH, Math.min(d.height + global.mousing, global.MAX_HEIGHT));
d3.select(this).style('fill', global.color_scale(d.height));
return global.border.call(redraw);
}
};
mousedown = function(d) {
/* mousing is +1 for increasing the height, -1 for decreasing it
*/ global.mousing = d3.event.shiftKey ? -1 : +1;
return mousemove.apply(this, arguments);
};
mouseup = function() {
mousemove.apply(this, arguments);
global.mousing = 0;
return global.touched = [];
};
/* redraw borders (altitude contour lines)
*/
redraw = function(border) {
return border.attr('d', global.path_generator(topojson.mesh(global.hex_topology, global.hex_topology.objects.hexagons, function(a, b) {
return a.height !== b.height;
})));
};
}).call(this);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hex landscape</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="index.js"></script>
</head>
<body onload="main()"></body>
</html>
global = {}
window.main = () ->
width = 960
height = 500
radius = 14
global.MAX_DEPTH = 10
global.MID_HEIGHT = 4
global.MAX_HEIGHT = 10
global.mousing = 0
### array of touched hexes, to avoid modifying the height of the same hexes in the same drag ###
global.touched = []
global.hex_topology = hexTopology(radius, width, height)
global.path_generator = d3.geo.path()
.projection(hexProjection(radius))
global.color_scale = d3.scale.linear()
.domain([-global.MAX_DEPTH, -1, 0, global.MID_HEIGHT, global.MAX_HEIGHT])
.range([d3.rgb(5, 48, 97), d3.rgb(198, 219, 239), d3.rgb(230, 245, 208), d3.rgb(102, 189, 99), d3.rgb(84, 48, 5)])
.interpolate(d3.interpolateHcl)
svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
svg.append('g')
.attr('class', 'hexagon')
.selectAll('path')
.data(global.hex_topology.objects.hexagons.geometries)
.enter().append('path')
.attr('d', (d) -> global.path_generator(topojson.feature(global.hex_topology, d)) )
.style('fill', (d) -> global.color_scale(d.height))
.on('mousedown', mousedown)
.on('mousemove', mousemove)
.on('mouseup', mouseup)
svg.append('path')
.datum(topojson.mesh(global.hex_topology, global.hex_topology.objects.hexagons))
.attr('class', 'mesh')
.attr('d', global.path_generator)
global.border = svg.append('path')
.attr('class', 'border')
.call(redraw)
### create the hex mesh TopoJSON ###
hexTopology = (radius, width, height) ->
dx = radius * 2 * Math.sin(Math.PI / 3)
dy = radius * 1.5
m = Math.ceil((height + radius) / dy) + 1
n = Math.ceil(width / dx) + 1
geometries = []
arcs = []
for j in [-1..m]
for i in [-1..n]
y = j * 2
x = (i + (j & 1) / 2) * 2
arcs.push([[x, y - 1], [1, 1]], [[x + 1, y], [0, 1]], [[x + 1, y + 1], [-1, 1]])
q = 3
for j in [0...m]
for i in [0...n]
geometries.push({
type: 'Polygon',
arcs: [[q, q + 1, q + 2, ~(q + (n + 2 - (j & 1)) * 3), ~(q - 2), ~(q - (n + 2 + (j & 1)) * 3 + 2)]],
height: 0
})
q += 3
q += 6
return {
transform: {translate: [0, 0], scale: [1, 1]},
objects: {hexagons: {type: 'GeometryCollection', geometries: geometries}},
arcs: arcs
}
### define a custom projection to make hexagons appear regular ###
hexProjection = (radius) ->
dx = radius * 2 * Math.sin(Math.PI / 3)
dy = radius * 1.5
return {
stream: (stream) -> {
point: ((x, y) -> stream.point(x * dx / 2, (y - (2 - (y & 1)) / 3) * dy / 2) ),
lineStart: (() -> stream.lineStart() ),
lineEnd: (() -> stream.lineEnd() ),
polygonStart: (() -> stream.polygonStart() ),
polygonEnd: (() -> stream.polygonEnd() )
}
}
### user interaction callbacks ###
mousemove = (d) ->
if (global.mousing and d not in global.touched)
global.touched.push(d)
### update the height of the tile ####
d.height = Math.max(-global.MAX_DEPTH, Math.min(d.height + global.mousing, global.MAX_HEIGHT))
d3.select(this).style('fill', global.color_scale(d.height))
global.border.call(redraw)
mousedown = (d) ->
### mousing is +1 for increasing the height, -1 for decreasing it ###
global.mousing = if d3.event.shiftKey then -1 else +1
mousemove.apply(this, arguments)
mouseup = () ->
mousemove.apply(this, arguments)
global.mousing = 0
global.touched = []
### redraw borders (altitude contour lines) ###
redraw = (border) ->
border.attr('d', global.path_generator(topojson.mesh(global.hex_topology, global.hex_topology.objects.hexagons, (a, b) -> a.height != b.height )))
.hexagon {
fill: white;
pointer-events: all;
}
.hexagon path {
-webkit-transition: fill 250ms linear;
transition: fill 250ms linear;
}
.hexagon path:hover {
stroke: black;
stroke-width: 2px;
}
.mesh {
fill: none;
stroke: black;
stroke-opacity: 0.1;
pointer-events: none;
}
.border {
fill: none;
stroke: black;
stroke-opacity: 0.8;
pointer-events: none;
}
.hexagon
pointer-events: all
path
-webkit-transition: fill 250ms linear
transition: fill 250ms linear
path:hover
stroke: black
stroke-width: 2px
.mesh
fill: none
stroke: black
stroke-opacity: .1
pointer-events: none
.border
fill: none
stroke: black
stroke-opacity: .8
pointer-events: none