block by nitaku 6166918

Hex landscape

Full Screen

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.

index.js

(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);

index.html

<!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>

index.coffee

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 )))
    

index.css

.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;
}

index.sass

.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