block by nitaku 0c991d6ed0e994e1c7ed

Node Gosper hex tiling

Full Screen

This experiment creates cells of an hexagonal tiling by following a Node Gosper curve. The drawing highlights groups of 7 and 49 (7^2) cells.

Thanks to the properties of the curve, each fractal recursion gives a nice, almost hexagonal shape (not exactly an hexagon, since hexagons are not rep-tiles). The perimeter of the obtained “island” is also less jagged with respect to a tiling that follows the standard Gosper curve (in fact, the two cannot even be overlapped onto each other: compare).

The aforementioned properties can prove to be useful when using such tilings to produce GosperMaps like this one, or this older one.

index.js

// Generated by CoffeeScript 1.4.0
(function() {
  var color, fractalize, global, hex_coords, new_hex, redraw;

  global = {};

  /* compute a Lindenmayer system given an axiom, a number of steps and rules
  */


  fractalize = function(config) {
    var char, i, input, output, _i, _j, _len, _ref;
    input = config.axiom;
    for (i = _i = 0, _ref = config.steps; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
      output = '';
      for (_j = 0, _len = input.length; _j < _len; _j++) {
        char = input[_j];
        if (char in config.rules) {
          output += config.rules[char];
        } else {
          output += char;
        }
      }
      input = output;
    }
    return output;
  };

  /* convert a Lindenmayer string into an array of hexagonal coordinates
  */


  hex_coords = function(config) {
    var char, current, dir, dir_i, directions, path, _i, _len, _ref;
    directions = [
      {
        x: +1,
        y: -1,
        z: 0
      }, {
        x: +1,
        y: 0,
        z: -1
      }, {
        x: 0,
        y: +1,
        z: -1
      }, {
        x: -1,
        y: +1,
        z: 0
      }, {
        x: -1,
        y: 0,
        z: +1
      }, {
        x: 0,
        y: -1,
        z: +1
      }
    ];
    /* start the walk from the origin cell, facing east
    */

    path = [
      {
        x: 0,
        y: 0,
        z: 0
      }
    ];
    dir_i = 0;
    _ref = config.fractal;
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
      char = _ref[_i];
      if (char === '+') {
        dir_i = (dir_i + 1) % directions.length;
      } else if (char === '-') {
        dir_i = dir_i - 1;
        if (dir_i === -1) {
          dir_i = 5;
        }
      } else if (char === 'F') {
        dir = directions[dir_i];
        current = path[path.length - 1];
        path.push({
          x: current.x + dir.x,
          y: current.y + dir.y,
          z: current.z + dir.z
        });
      }
    }
    return path;
  };

  window.main = function() {
    var d, data, dx, dy, gosper, height, hexes, radius, svg, width;
    width = 960;
    height = 500;
    svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
    global.vis = svg.append('g').attr('transform', 'translate(660,360)');
    /* create the Gosper curve
    */

    gosper = fractalize({
      axiom: 'A',
      steps: 3,
      rules: {
        A: 'A-FB--FB-F++AF++A-F+AF+B-',
        B: '+A-FB-F+B--FB--F+AF++AF+B'
      }
    });
    /* convert the curve into coordinates of hex cells
    */

    data = hex_coords({
      fractal: gosper
    });
    /* create the GeoJSON hexes
    */

    hexes = {
      type: 'FeatureCollection',
      features: (function() {
        var _i, _len, _results;
        _results = [];
        for (_i = 0, _len = data.length; _i < _len; _i++) {
          d = data[_i];
          _results.push(new_hex(d));
        }
        return _results;
      })()
    };
    /* custom projection to make hexagons appear regular (y axis is also flipped)
    */

    radius = 12;
    dx = radius * 2 * Math.sin(Math.PI / 3);
    dy = radius * 1.5;
    global.path_generator = d3.geo.path().projection(d3.geo.transform({
      point: function(x, y) {
        return this.stream.point(x * dx / 2, -(y - (2 - (y & 1)) / 3) * dy / 2);
      }
    }));
    /* start the animation
    */

    redraw(hexes.features, 1);
    /* draw the origin
    */

    return global.vis.append('circle').attr('cx', 0).attr('cy', 0).attr('r', 3);
  };

  /* create a new hexagon
  */


  new_hex = function(d) {
    /* conversion from hex coordinates to rect
    */

    var x, y;
    x = 2 * (d.x + d.z / 2.0);
    y = 2 * d.z;
    return {
      type: 'Feature',
      geometry: {
        type: 'Polygon',
        coordinates: [[[x, y + 2], [x + 1, y + 1], [x + 1, y], [x, y - 1], [x - 1, y], [x - 1, y + 1], [x, y + 2]]]
      }
    };
  };

  /* update the drawing, then call again this function till data ends
  */


  color = function(i) {
    var i_49, i_7;
    i_7 = Math.floor(i / 7);
    i_49 = Math.floor(i / 49);
    return d3.hcl((i_7 % 7) * 360 / 7, 30, (function() {
      switch (i_49) {
        case 0:
          return 80;
        case 1:
          return 55;
        case 2:
          return 30;
        case 3:
          return 55;
        case 4:
          return 80;
        case 5:
          return 55;
        case 6:
          return 80;
      }
    })());
  };

  redraw = function(data, size) {
    return global.vis.selectAll('.hex').data(data.slice(0, size)).enter().append('path').attr('class', 'hex').attr('d', global.path_generator).attr('fill', function(d, i) {
      return color(i);
    }).transition().duration(60).each('end', (function() {
      if (size > data.length) {
        return;
      }
      return redraw(data, size + 1);
    }));
  };

}).call(this);

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Node Gosper Hexagon Tiling</title>
        <link type="text/css" href="index.css" rel="stylesheet"/>
        <script src="//d3js.org/d3.v3.min.js"></script>
        <script src="index.js"></script>
    </head>
    <body onload="main()"></body>
</html>

index.coffee

global = {}

### compute a Lindenmayer system given an axiom, a number of steps and rules ###
fractalize = (config) ->
    input = config.axiom

    for i in [0...config.steps]
        output = ''

        for char in input
            if char of config.rules
                output += config.rules[char]
            else
                output += char

        input = output

    return output

### convert a Lindenmayer string into an array of hexagonal coordinates ###
hex_coords = (config) ->
    directions = [
        {x:+1, y:-1, z: 0},
        {x:+1, y: 0, z:-1},
        {x: 0, y:+1, z:-1},
        {x:-1, y:+1, z: 0},
        {x:-1, y: 0, z:+1},
        {x: 0, y:-1, z:+1}
    ]

    ### start the walk from the origin cell, facing east ###
    path = [{x:0,y:0,z:0}]
    dir_i = 0

    for char in config.fractal
        if char == '+'
            dir_i = (dir_i+1) % directions.length
        else if char == '-'
            dir_i = dir_i-1
            if dir_i == -1
                dir_i = 5
        else if char == 'F'
            dir = directions[dir_i]
            current = path[path.length-1]
            path.push {x:current.x+dir.x, y:current.y+dir.y, z:current.z+dir.z}

    return path

window.main = () ->
    width = 960
    height = 500

    svg = d3.select('body').append('svg')
        .attr('width', width)
        .attr('height', height)

    global.vis = svg.append('g')
        .attr('transform', 'translate(660,360)')

    ### create the Gosper curve ###
    gosper = fractalize
        axiom: 'A'
        steps: 3
        rules:
            # A: 'A+BF++BF-FA--FAFA-BF+'
            # B: '-FA+BFBF++BF+FA--FA-B'
            A: 'A-FB--FB-F++AF++A-F+AF+B-'
            B: '+A-FB-F+B--FB--F+AF++AF+B'

    ### convert the curve into coordinates of hex cells ###
    data = hex_coords
        fractal: gosper

    ### create the GeoJSON hexes ###
    hexes = {
        type: 'FeatureCollection',
        features: (new_hex(d) for d in data)
    }

    ### custom projection to make hexagons appear regular (y axis is also flipped) ###
    radius = 12
    dx = radius * 2 * Math.sin(Math.PI / 3)
    dy = radius * 1.5

    global.path_generator = d3.geo.path()
        .projection d3.geo.transform({
            point: (x,y) -> this.stream.point(x * dx / 2, -(y - (2 - (y & 1)) / 3) * dy / 2)
        })


    ### start the animation ###
    redraw(hexes.features, 1)

    ### draw the origin ###
    global.vis.append('circle')
        .attr('cx', 0)
        .attr('cy', 0)
        .attr('r', 3)

### create a new hexagon ###
new_hex = (d) ->
    ### conversion from hex coordinates to rect ###
    x = 2*(d.x + d.z/2.0)
    y = 2*d.z

    return {
        type: 'Feature',
        geometry: {
            type: 'Polygon',
            coordinates: [[
                [x, y+2],
                [x+1, y+1],
                [x+1, y],
                [x, y-1],
                [x-1, y],
                [x-1, y+1],
                [x, y+2]
            ]]
        }
    }

### update the drawing, then call again this function till data ends ###
color = (i) ->
  i_7 = Math.floor(i/7)
  i_49 = Math.floor(i/49)
  return d3.hcl((i_7%7)*360/7, 30, switch i_49
    when 0 then 80
    when 1 then 55
    when 2 then 30
    when 3 then 55
    when 4 then 80
    when 5 then 55
    when 6 then 80)

redraw = (data, size) ->
    global.vis.selectAll('.hex')
        .data(data[0...size])
      .enter().append('path')
        .attr('class', 'hex')
        .attr('d', global.path_generator)
        .attr('fill', (d, i) ->
          return color(i)
        )
      .transition().duration(60)
        .each('end', (() ->
            if size > data.length
                return
            redraw(data, size+1)
        ))

index.css

.hex {
  stroke: black;
  stroke-width: 1;
}

index.sass

.hex
    stroke: black
    stroke-width: 1