Cluster Growth Algorithm Animation

Something fun I created this afternoon! Here is a cluster growth algorithm where each perimeter cell grows depending on whether a certain probability threshold is met! :microbe:

Animation below:

Deconstruction tutorial here:

Source here:
kirupa/examples/cluster_growth_algorithm.htm at master · kirupa/kirupa · GitHub

Reposted below:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Cluster Growth Animation</title>
    <style>
      body {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
        background-color: #f0f0f0;
      }

      canvas {
        border: 1px solid black;
        background-color: #fff;
      }
    </style>
  </head>

  <body>
    <canvas id="canvas" width="420" height="420"></canvas>
    <script>
      // Get the canvas element and its 2D rendering context
      let myCanvas = document.getElementById("canvas");
      let ctx = myCanvas.getContext("2d");

      // Configuration variables
      let activeCells = 0;
      let iterations = 0;
      let cellSize = 3; // Size of each cell in pixels
      let gap = 3; // Size of the gap between cells in pixels
      let totalOffset = cellSize + gap;

      // Calculate the grid dimensions based on canvas size and cell size
      let width = Math.floor(myCanvas.width / totalOffset);
      let height = Math.floor(myCanvas.height / totalOffset);
      let totalCells = (width - 1) * (height - 1);

      // Create an empty grid to start with
      let tempGrid = createEmptyGrid();
      let growthProbability = customRandom(0.05, 0.2); // Set initial growth probability

      // Array of dark colors for the clusters
      const dark_colors = [
        "#9D0000", // Dark Red
        "#C70039", // Deep Pink
        "#800080", // Purple
        "#4C3378", // Dark Purple (bluish)
        "#003080", // Navy Blue (more vivid)
        "#007BFF", // Material Blue (darker, vivid)
        "#388E3C", // Dark Green (vivid)
        "#00695C", // Dark Teal
        "#663399", // Dark Amethyst
        "#8B0000", // Maroon (more vivid)
        "#A020F0", // Deep Purple (more intense)
        "#B32830", // Dark Red (brownish)
        "#BF360C", // Dark Orange
        "#C2C255", // Dark Lime (more muted)
        "#FF9933", // Dark Orange (reddish)
      ];

      // Select a random color for the current iteration
      let color = getRandomColor(dark_colors);

      // Function to create an empty grid filled with zeros
      function createEmptyGrid() {
        let emptyGrid = [];
        for (let i = 0; i < width + 1; i++) {
          let row = [];
          for (let j = 0; j < height + 1; j++) {
            row.push(0); // Initialize each cell to 0
          }
          emptyGrid.push(row);
        }
        return emptyGrid;
      }

      // Function to set the initial cluster of active cells
      function setInitialCluster(x, y, size) {
        let newCells = [];

        for (let i = x - size; i < x + size; i++) {
          for (let j = y - size; j < y + size; j++) {
            if (i >= 0 && i < width && j >= 0 && j < height) {
              tempGrid[i][j] = 1;
              newCells.push([i, j]);
            }
          }
        }

        // Draw only the new cells
        newCells.forEach(([x, y]) => drawCell(x, y));
      }

      // Function to draw the current state of the grid
      function drawCell(x, y) {
        ctx.fillStyle = color;

        let xPos = Math.round(x * totalOffset - cellSize / 2);
        let yPos = Math.round(y * totalOffset - cellSize / 2)
        ctx.fillRect(xPos, yPos, cellSize, cellSize);

        activeCells++;
      }

      // Function to update the grid based on growth rules
      function updateGrid() {
        let newCells = [];

        let updatedGrid = createEmptyGrid(); // Create a new empty grid

        // Iterate through each cell in the grid
        for (let i = 1; i < tempGrid.length - 1; i++) {
          for (let j = 1; j < tempGrid[0].length - 1; j++) {
            if (tempGrid[i][j] === 0) {
              let neighbors = countNeighbors(tempGrid, i, j); // Count the number of neighboring cells

              if (neighbors > 0) {
                if (growthProbability > Math.random()) {
                  newCells.push([i, j]);
                  updatedGrid[i][j] = 1; // Grow a new cell
                }
              }
            } else {
              updatedGrid[i][j] = tempGrid[i][j]; // Keep the existing cell
            }
          }
        }

        // Draw only the new cells
        newCells.forEach(([x, y]) => drawCell(x, y));

        tempGrid = updatedGrid; // Update the grid
      }

      // Function to count the number of active neighboring cells
      function countNeighbors(grid, x, y) {
        let sum = 0;

        for (let i = -1; i <= 1; i++) {
          for (let j = -1; j <= 1; j++) {
            sum += tempGrid[x + i][y + j]; // Sum the values of the neighboring cells
          }
        }
        
        return sum;
      }

      // Function to reset the animation
      function reset() {
        activeCells = 0;
        tempGrid = createEmptyGrid();
        growthProbability = customRandom(0.05, 0.2); // Set new growth probability

        // Alternate between colored and white clusters
        if (iterations % 2 !== 0) {
          color = getRandomColor(dark_colors);
        } else {
          color = "white";
        }

        setInitialCluster(Math.ceil(width / 2), Math.ceil(height / 2), 2);

        iterations++;
      }

      // Set the initial cluster in the center of the grid
      setInitialCluster(Math.ceil(width / 2), Math.ceil(height / 2), 2);

      // Main animation function
      function growClusters() {
        updateGrid();

        // Reset if the entire grid is filled
        if (activeCells >= totalCells) {
          reset();
        }

        requestAnimationFrame(growClusters);
      }

      // Start the animation
      growClusters();

      // Utility function to get a random color from the array
      function getRandomColor(colors) {
        return colors[Math.floor(Math.random() * colors.length)];
      }

      // Utility function to generate a random number within a range
      function customRandom(min, max) {
        return Math.random() * (max - min) + min;
      }
    </script>
  </body>
</html>

Hope you all enjoy this. This will likely be part of a future tutorial :slight_smile:

Cheers,
Kirupa

1 Like

In this Tweet thread, I shared a few variations of how this cluster growth animation can look very different with just a few minor changes.

The full source for these variations can be seen in the links below:

My latest newsletter post on this goes into more detail as well:

Cheers,
Kirupa :slight_smile: