Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active December 25, 2015 17:39
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/7015186 to your computer and use it in GitHub Desktop.
Save nitaku/7015186 to your computer and use it in GitHub Desktop.
Gosper hex tiling (fancy)

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.

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