|
// Start by building our model |
|
var fieldWidth = 40, |
|
fieldHeight = 40, |
|
field = randomField(), |
|
|
|
// Define our visual space |
|
pxWidth = 300, |
|
pxHeight = 300, |
|
pxWidthSquare = pxWidth / fieldWidth, |
|
pxHeightSquare = pxHeight / fieldHeight, |
|
|
|
// How fast does it go? |
|
msTimestep = 60; |
|
|
|
// Scales to turn field coordinates into rendering coordinates |
|
var x = d3.scale.linear() |
|
.domain([0, fieldWidth]) |
|
.range([0, pxWidth]) |
|
|
|
var y = d3.scale.linear() |
|
.domain([0, fieldHeight]) |
|
.range([0, pxHeight]); |
|
|
|
var svg = d3.select('svg.container') |
|
.attr('height', pxHeight) |
|
.attr('width', pxWidth), |
|
time = 0; |
|
|
|
// Render initial |
|
render(field); |
|
|
|
// Then continuously iterate |
|
d3.timer(iterate, msTimestep) |
|
|
|
function iterate() { |
|
// If we've been looking at this one a while... |
|
if (time % 200 == 0) { |
|
// Make a new one! |
|
field = randomField() |
|
} else { |
|
// Otherwise, just refresh the old one |
|
field = createNewGeneration(field) |
|
} |
|
|
|
render(field) |
|
|
|
time++ |
|
|
|
d3.timer(iterate, msTimestep) |
|
|
|
return true; |
|
|
|
} |
|
|
|
// Actual d3 rendering. Should not be redrawing the whole thing every time |
|
// But it does reselect everything. Is that healthy? |
|
function render(data) { |
|
var up = svg.selectAll('g.row').data(data), |
|
en = up.enter(), |
|
ex = up.exit(); |
|
|
|
en.append('svg:g') |
|
.classed('row', true) |
|
|
|
var upSquare = up.selectAll('g.square') |
|
.data(function (d) { return d } ), |
|
enSquare = upSquare.enter(); |
|
|
|
enSquare.append('svg:g') |
|
.classed('square', true) |
|
.append('rect') |
|
.attr('x', function(d, i, j) { return x(i) }) |
|
.attr('y', function(d, i, j) { return y(j) }) |
|
.attr('width', pxWidthSquare) |
|
.attr('height', pxHeightSquare); |
|
|
|
// Finally, just switch class on and off to visualize binary state |
|
upSquare |
|
.classed('on', function (d) { return d == 1;}) |
|
} |
|
|
|
// Utility to generate a fresh totally random field |
|
function randomField() { |
|
return _.map(d3.range(0, fieldWidth), function (i) { |
|
return _.map(d3.range(0, fieldHeight), function (j) { |
|
return Math.random() < 0.5 ? 1 : 0 |
|
}) |
|
}); |
|
} |
|
|
|
// Taken from: http://www.janwillemtulp.com/2011/03/22/tutorial-conways-game-of-life-in-d3/ |
|
// (and edited by me to not rely on globals/closures so much) |
|
function createNewGeneration(states) { |
|
var nextGen = new Array() |
|
var ccx = states.length; |
|
for (x = 0; x < ccx; x++) { |
|
var ccy = states[x].length; |
|
nextGen[x] = new Array() |
|
for (y = 0; y < ccy; y++) { |
|
var ti = y - 1 < 0 ? ccy - 1 : y - 1 // top index |
|
var ri = x + 1 == ccx ? 0 : x + 1 // right index |
|
var bi = y + 1 == ccy ? 0 : y + 1 // bottom index |
|
var li = x - 1 < 0 ? ccx - 1 : x - 1 // left index |
|
|
|
var thisState = states[x][y] |
|
var liveNeighbours = 0 |
|
liveNeighbours += states[li][ti] ? 1 : 0 |
|
liveNeighbours += states[x][ti] ? 1 : 0 |
|
liveNeighbours += states[ri][ti] ? 1 : 0 |
|
liveNeighbours += states[li][y] ? 1 : 0 |
|
liveNeighbours += states[ri][y] ? 1 : 0 |
|
liveNeighbours += states[li][bi] ? 1 : 0 |
|
liveNeighbours += states[x][bi] ? 1 : 0 |
|
liveNeighbours += states[ri][bi] ? 1 : 0 |
|
|
|
var newState = false |
|
|
|
if (thisState) { |
|
newState = liveNeighbours == 2 || liveNeighbours == 3 ? 1 : 0 |
|
} else { |
|
newState = liveNeighbours == 3 ? 1 : 0 |
|
} |
|
|
|
nextGen[x][y] = newState |
|
} |
|
} |
|
|
|
return nextGen |
|
} |