Create cells of an hexagonal tiling by following a Gosper space-filling curve.
This example uses the rendering approach introduced here, and the fractal generation technique introduced here.
Same as this example, but with a simpler animation.
Create cells of an hexagonal tiling by following a Gosper space-filling curve.
This example uses the rendering approach introduced here, and the fractal generation technique introduced here.
Same as this example, but with a simpler animation.
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','#76B0DA') | |
.transition().duration(100) | |
.each('end', (() -> | |
if size > data.length | |
return | |
redraw(data, size+1) | |
)) | |
.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', '#76B0DA').transition().duration(100).each('end', (function() { | |
if (size > data.length) return; | |
return redraw(data, size + 1); | |
})); | |
}; | |
}).call(this); |
.hex | |
stroke: black | |
stroke-width: 1 | |