var world = {
width: 160,
height: 90,
renderedCellSize: 7,
paused: false,
cells: [],
initiallyClickedCellState: 0
};
TODO:
This is the object that holds all of the state for the simulation. When something
changes in the simulation we change it here. We initialize this object first and at
the beginning of the file so that setup
and draw
can access it.
changeCellState
function
var world = {
width: 160,
height: 90,
renderedCellSize: 7,
paused: false,
cells: [],
initiallyClickedCellState: 0
};
function setup() {
noStroke();
TODO: Run at 60fps but change the speed with which the world advances, keeping in mind that frames can be dropped, so just using “frameCount” will not produce a smooth simulation
frameRate(20);
Populate the world with a default percentage filled
world = populateWorld(world, 0.35);
Add a pause button that when clicked flips the value of world.pause
var pauseButton = createButton('Pause');
pauseButton.mousePressed(function () {
world.paused = !world.paused;
});
Add a button to clear set all cells to 0
var clearButton = createButton('Clear');
clearButton.mousePressed(function () {
world = clearWorld(world);
});
Add a button to wipe and reset the world randomly
var generateButton = createButton('Regenerate');
generateButton.mousePressed(function () {
world = populateWorld(world, sl.value()/ 100);
});
Add a slider to adjust the percentage of the grid to fill with live cells when regenerating. This value is between 0 and 100, however values closer to 100 tend to lead to a very quick die off.
var sl = createSlider(0,100,35);
With all of the buttons and interface created, create the canvas with the world width and height
createCanvas(world.width * world.renderedCellSize, world.height * world.renderedCellSize);
}
function draw() {
fill(255,255,255);
background();
drawWorld(world);
Update the world only if the world is not paused.
if (!world.paused) {
var neighborCounts = census(world);
world = nextGeneration(world, neighborCounts);
}
if (isMouseInBounds(world)) {
var xx = posToCellCoords(world.renderedCellSize,mouseX);
var yy = posToCellCoords(world.renderedCellSize,mouseY);
drawHoveredCell(xx, yy, world.renderedCellSize);
if (mouseIsPressed) {
changeCellState(world, xx / world.renderedCellSize, yy / world.renderedCellSize);
}
}
}
function mousePressed() {
if (isMouseInBounds(world)) {
var xx = posToCellCoords(world.renderedCellSize,mouseX);
var yy = posToCellCoords(world.renderedCellSize,mouseY);
world.initiallyClickedCellState = world.cells[yy / world.renderedCellSize][xx / world.renderedCellSize] ? 0 : 1;
}
}
function keyPressed(e) {
if (key === ' ') {
e.preventDefault();
world.paused = !world.paused;
}
}
Use this function to draw a highlighted cell at a given x
and y
coordinate in the
p5 sketch. The function sets its own fill color to a semi-transulecent blue.
rect
atrect
atrect
function drawHoveredCell(x, y, side) {
fill(0, 0, 255, 128);
rect(x, y, side, side);
}
Draws black squares for all cells in world
that have a value of 1.
function drawWorld(world) {
fill(0,0,0);
Loop through the world.cells
to get each row and then each cell, setting
variables for the yIndex and xIndex which are used to position the rectangle.
world.cells.forEach(function(rowArray, yIndex) {
rowArray.forEach(function(cellState, xIndex) {
If the cell is alive, draw a rectangle. Set the x position to be the number of cells from the left times the width of each cell and the y position as the number of cells from the top times the width of each cell.
if (cellState === 1) {
rect(xIndex * world.renderedCellSize, yIndex * world.renderedCellSize, world.renderedCellSize, world.renderedCellSize);
}
});
});
}
example:
-1 % 15 === -1; // Desired result is 14
actualModulo(-1, 15) === 14
function actualModulo(divisor, dividend) {
var fakeMod = divisor % dividend;
if (fakeMod < 0) {
return dividend + fakeMod;
} else {
return fakeMod;
}
}
Passed a world object, returns a boolean of whether or not the mouse is inside the edges of the simulation or not.
var isMouseInBounds = function(world) {
var w = world.width * world.renderedCellSize;
var h = world.height * world.renderedCellSize;
if (mouseX > 0 && mouseY > 0 && mouseX < w && mouseY < h) {
return true;
} else {
return false;
}
}
var posToCellCoords = function (cellSize, n) {
return n - n % cellSize;
}
Create a two dimensional array given two dimensions and a function to fill the array the predicate function is passed the x and y coordinates of the array position.
function buildArray(w, h, pred) {
var arr = Array(h);
for (var i = 0; i < h; i += 1) {
var arrRow = Array(w);
for (var j = 0; j < w; j += 1) {
arrRow[j] = pred(j,i);
}
arr[i] = arrRow;
}
return arr;
}
Random (and soon hopefully something like a Perlin Noise thing) make cells. Give it a coefficient for likelihood that the cell is alive, between 0 and 1. A value closer to 1 means more living cells. TODO: Switch to 2d Perlin noise when I’m back on the internet.
Returns a closure that generates 1s and 0s.
function populate(coefficient) {
return function (x, y) {
if (Math.random() < coefficient) {
return 1;
} else {
return 0;
}
}
}
Get the sum of the neighbors of a cell. Given the entire world array, width and height precomputed to save on lenght lookups and the x and y coordinate of the cell. Loops around the edges.
function sumNeighbors(cells, w, h, x, y) {
return cells[y][actualModulo(x - 1, w)] + cells[y][actualModulo(x + 1, w)] + cells[actualModulo(y - 1, h)][x] + cells[actualModulo(y + 1, h)][x] + cells[actualModulo(y - 1, h)][actualModulo(x - 1, w)] + cells[actualModulo(y - 1, h)][actualModulo(x + 1, w)] + cells[actualModulo(y + 1, h)][actualModulo(x - 1, w)] + cells[actualModulo(y + 1, h)][actualModulo(x + 1, w)];
}
function census(world) {
var newNeighborCounts = buildArray(world.width, world.height, function() { return 0; });
world.cells.forEach(function(rowArray, yIndex) {
rowArray.forEach(function(cellState, xIndex) {
newNeighborCounts[yIndex][xIndex] = sumNeighbors(world.cells, world.width, world.height, xIndex, yIndex);
});
});
return newNeighborCounts;
}
function nextGeneration(world, neighborCounts) {
world.cells.forEach(function(rowArray, yIndex) {
rowArray.forEach(function(cellState, xIndex) {
var count = neighborCounts[yIndex][xIndex];
var cellState = world.cells[yIndex][xIndex];
If the cell has the proper number of neighbors to turn from dead to alive set the cell to alive. Else, if the cell is currently alive and meets the requirements to die, set the cell to dead. In all other cases do not update the state of the cell.
if (count == 3) {
world.cells[yIndex][xIndex] = 1;
} else if (cellState === 1 && (count < 2 || count > 3)) {
world.cells[yIndex][xIndex] = 0;
}
});
});
return world;
}
function changeCellState(world, x, y) {
world.cells[y][x] = world.initiallyClickedCellState;
}
function populateWorld(world, density) {
world.cells = buildArray(world.width, world.height, populate(density));
return world;
}
function clearWorld(world) {
world.cells = buildArray(world.width, world.height, function () { return 0; });
return world;
}