Concept
Cellular automata are mathematical models used to simulate complex systems with simple rules. I’ve built an interactive visualization using p5.js that not only demonstrates these concepts but also allows users to manipulate the simulation in real time.
Highlight of the Code
I’m particularly proud of how I implemented the function to update the grid based on cellular automata rules. Here’s the snippet that captures this logic:
function updateGrid(oldGrid, numRows, numCols) { let newGrid = new Array(numRows); for (let i = 0; i < numRows; i++) { newGrid[i] = new Array(numCols); for (let j = 0; j < numCols; j++) { let state = oldGrid[i][j]; let neighbors = countNeighbors(oldGrid, i, j, numRows, numCols); if (state == 0 && neighbors == 3) { newGrid[i][j] = 1; // Birth } else if (state == 1 && (neighbors < 2 || neighbors > 3)) { newGrid[i][j] = 0; // Death } else { newGrid[i][j] = state; // Survival } } } return newGrid; }
This function is central to the automata’s behavior, dynamically calculating each cell’s state in the next generation, creating patterns that can evolve from ordered or chaotic initial conditions.
Full Code
let grid; let cellSizeSlider; let colorButton; let cellSize = 10; let numRows, numCols; let prevCellSize = 10; // Stores the previous cell size for comparison. let colorIndex = 0; // Index to track the current color scheme. let colorSchemes; // Array to hold different color schemes for the grid. function setup() { createCanvas(600, 600); // Creates and position the slider for adjusting the cell size. cellSizeSlider = createSlider(5, 20, 10, 1); cellSizeSlider.position((width - cellSizeSlider.width) / 2 - 60, height + 10); // Initializes color schemes using the color function. colorSchemes = [ [color(255), color(0)], // White and Black [color(255, 255, 0), color(0)], // Yellow and Black [color(64, 224, 208), color(128, 0, 128)], // Turquoise and Purple [color(255, 105, 180), color(255)] // Hot pink and White ]; // Creates and position the button for changing colors, and set its event handler. colorButton = createButton('Color'); colorButton.position(cellSizeSlider.x + cellSizeSlider.width + 10, height + 10); colorButton.mousePressed(changeColor); // Initializes the grid based on the current slider value. updateGridSize(); grid = initializeGrid(numRows, numCols); displayGrid(grid, numRows, numCols); } function draw() { // Checks if the slider value has changed and update the grid if necessary. if (prevCellSize !== cellSizeSlider.value()) { prevCellSize = cellSizeSlider.value(); updateGridSize(); grid = initializeGrid(numRows, numCols); } // Updates and displays the grid according to the cellular automata rules. grid = updateGrid(grid, numRows, numCols); displayGrid(grid, numRows, numCols); } function updateGridSize() { // Adjusts the number of rows and columns based on the cell size. cellSize = cellSizeSlider.value(); numCols = floor(width / cellSize); numRows = floor(height / cellSize); } function initializeGrid(numRows, numCols) { // Creates a new grid with random initial states. let grid = new Array(numRows); for (let i = 0; i < numRows; i++) { grid[i] = new Array(numCols); for (let j = 0; j < numCols; j++) { grid[i][j] = floor(random(2)); // Each cell randomly alive or dead. } } return grid; } function updateGrid(oldGrid, numRows, numCols) { // Applys cellular automata rules to create the next generation of the grid. let newGrid = new Array(numRows); for (let i = 0; i < numRows; i++) { newGrid[i] = new Array(numCols); for (let j = 0; j < numCols; j++) { let state = oldGrid[i][j]; let neighbors = countNeighbors(oldGrid, i, j, numRows, numCols); if (state == 0 && neighbors == 3) { newGrid[i][j] = 1; // Birth } else if (state == 1 && (neighbors < 2 || neighbors > 3)) { newGrid[i][j] = 0; // Death } else { newGrid[i][j] = state; // Survival } } } return newGrid; } function countNeighbors(grid, x, y, numRows, numCols) { // Count the live neighbors around a given cell, considering wrap-around. let count = 0; for (let i = -1; i <= 1; i++) { for (let j = -1; j <= 1; j++) { if (i == 0 && j == 0) continue; // Skip the cell itself let col = (x + i + numRows) % numRows; let row = (y + j + numCols) % numCols; count += grid[col][row]; } } return count; } function displayGrid(grid, numRows, numCols) { // Displays the grid on the canvas, coloring cells based on their state. background(255); // Clear the canvas before redrawing. for (let i = 0; i < numRows; i++) { for (let j = 0; j < numCols; j++) { let x = j * cellSize; let y = i * cellSize; noStroke(); fill(grid[i][j] ? colorSchemes[colorIndex][1] : colorSchemes[colorIndex][0]); rect(x, y, cellSize, cellSize); } } } function changeColor() { // Cycles through the color schemes when the button is pressed. colorIndex = (colorIndex + 1) % colorSchemes.length; }
Sketch
Reflection and Future Work
Reflecting on this project, I am satisfied with the final look. However, there’s always room for improvement and expansion. In future iterations, I would like to explore:
- Additional Rulesets: Integrating more complex automata rules to show different dynamics and perhaps allow users to switch between them interactively.
- Performance Optimization: As the grid grows larger, performance can become an issue. Optimizing the sketch to handle larger grids efficiently would be beneficial.
- User Interaction: Adding more controls for users to configure initial conditions manually or even create presets of interesting patterns to start with.