Create cells of an hexagonal tiling by following a Gosper space-filling curve (with a fancy animation that sometimes fails to run! If does not work for you, see a simpler example).
This example uses the rendering approach introduced here, and the fractal generation technique introduced here.
The animation should help seeing the path followed by the curve, but ended to be not as clear as I wanted. This is partly because the space-filling curve tend to keep the cells of the sequence near to each other, thus making the colors of the “snake” to have little contrast for adjacent cells.
(function() {
var 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, _len, _ref;
input = config.axiom;
for (i = 0, _ref = config.steps; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) {
output = '';
for (_i = 0, _len = input.length; _i < _len; _i++) {
char = input[_i];
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(490,30)');
/* 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'
}
});
/* 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
*/
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', '#EB5B5B').transition().duration(100).each('end', (function() {
if (size > data.length) return;
return redraw(data, size + 1);
})).transition().duration(600).attr('fill', '#FF9F4A').transition().duration(600).attr('fill', '#5FD05F').transition().duration(600).attr('fill', '#76B0DA');
};
}).call(this);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>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>
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(490,30)')
### 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'
### 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 ###
redraw = (data, size) ->
global.vis.selectAll('.hex')
.data(data[0...size])
.enter().append('path')
.attr('class', 'hex')
.attr('d', global.path_generator)
.attr('fill','#EB5B5B')
.transition().duration(100)
.each('end', (() ->
if size > data.length
return
redraw(data, size+1)
))
.transition().duration(600)
.attr('fill','#FF9F4A')
.transition().duration(600)
.attr('fill','#5FD05F')
.transition().duration(600)
.attr('fill','#76B0DA')
.hex {
stroke: black;
stroke-width: 1;
}
.hex
stroke: black
stroke-width: 1