Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active December 20, 2015 17:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/6166918 to your computer and use it in GitHub Desktop.
Save nitaku/6166918 to your computer and use it in GitHub Desktop.
Hex landscape

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.

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;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hex landscape</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="index.js"></script>
</head>
<body onload="main()"></body>
</html>
(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);
.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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment