<body class="antialiased sans-serif text-slate-800 bg-opacity-50">
  <div class="fixed top-0 h-screen w-screen bg-[url('https://images.unsplash.com/photo-1577083862054-7324cd025fa6?ixlib=rb-1.2.1&raw_url=true&q=80&fm=jpg&crop=entropy&cs=tinysrgb&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2741')] bg-center"></div>

  <div x-data="state" x-cloak class="absolute z-10 p-4 bg-white rounded drop-shadow-md w-full sm:m-2 sm:w-auto flex justify-center">
    <div>
      <label class="font-bold">Generation rule:</label>
      <div>
        <template x-for="i in 8">
          <div class="inline-block">
            <input type="checkbox" x-model="data.ruleCheckboxes" x-bind:value="i" x-on:change="updateRule()" class="appearance-none w-5 h-5 border-2 border-slate-800 rounded mr-1 grid place-content-center before:content-[''] before:w-3 before:h-3 before:shadow-[inset_12px_12px_#1e293b] before:transition before:ease-in-out before:scale-0 checked:before:scale-100" />
          </div>
        </template>
      </div>
      <div class="mt-4">
        <button x-on:click="stepUntilFilled()" x-bind:disabled="data.stepsLeftToFill <= 0" class="rounded px-3 py-1 mr-1 bg-green-600 text-white text-sm font-semibold shadow disabled:bg-gray-100 disabled:text-slate-500 hover:bg-green-700 transition-all">Run</button>
        <button x-on:click="step()" x-bind:disabled="data.stepsLeftToFill <= 0" class="rounded px-3 py-1 mr-1 bg-sky-600 text-white text-sm font-semibold shadow disabled:bg-gray-100 disabled:text-slate-500 hover:bg-sky-700 transition-all">+1</button>
        <button x-on:click="step(10)" x-bind:disabled="data.stepsLeftToFill <= 0" class="rounded px-3 py-1 mr-1 bg-sky-600 text-white text-sm font-semibold shadow disabled:bg-gray-100 disabled:text-slate-500 hover:bg-sky-700 transition-all">+10</button>
        <button x-on:click="clear()" class="rounded px-3 py-1 mr-1 bg-amber-600 text-white text-sm font-semibold shadow disabled:bg-gray-100 disabled:text-slate-500 hover:bg-amber-700 transition-all">Clear</button>
      </div>
    </div>
  </div>

  <main class="relative"></main>
</body>
const PIXEL_SIZE = 4;
const OFFSET_Y = 36 * PIXEL_SIZE;
const DEFAULT_RULE = 0b00011110;

function ruleToCheckboxes(rule) {
  const checkboxes = [];
  for (let i = 0; i < 8; i++) {
    if ((rule >> (7 - i)) & 0b1) {
      checkboxes.push(i + 1);
    }
  }
  return checkboxes;
}

function checkboxesToRule(checkboxes) {
  let rule = 0;
  for (let value of checkboxes) {
    rule |= 1 << (8 - value);
  }
  return rule;
}

function calculateNextGeneration(rule, generation) {
  if (!generation) {
    return [1];
  }

  const currentGeneration = [0, 0, ...generation, 0, 0];
  const nextGeneration = [];
  for (let i = 0; i < currentGeneration.length - 2; i++) {
    const pattern = currentGeneration.slice(i, i + 3);
    const bit = parseInt(pattern.join(""), 2);
    const active = (rule >> bit) & 0b1;
    nextGeneration[i] = active;
  }
  return nextGeneration;
}

document.addEventListener("alpine:init", function init() {
  function initialState() {
    const totalStepsToFill = Math.ceil((window.innerHeight - OFFSET_Y) / PIXEL_SIZE);
      
    return {
      rule: DEFAULT_RULE,
      ruleCheckboxes: ruleToCheckboxes(DEFAULT_RULE),
      stepsLeftToFill: 0,      
      currentGeneration: null,
      currentGenerationNumber: 0,
      totalStepsToFill: totalStepsToFill,
      stepsLeftToFill: totalStepsToFill,
      scheduledToDraw: [],
    }
  }
  
  Alpine.data("state", () => ({
    data: initialState(),
    init: function() {
      this.restart();
      this.setupP5();
      this.stepUntilFilled();
    },
    setupP5: function() {
      const scope = this;
      new p5(function(p) {
        scope.p5 = p;
        
        p.setup = function() {
          p.createCanvas(window.innerWidth, window.innerHeight);
          p.rectMode(p.CENTER);
          p.noStroke();
          p.clear();
        };
        
        p.windowResized = function() {
          p.resizeCanvas(window.innerWidth, window.innerHeight);
          scope.clear();
        }
        
        p.draw = function() {
          while (scope.data.scheduledToDraw.length) {
            const { line, generation } = scope.data.scheduledToDraw.shift();

            const y = line * PIXEL_SIZE + OFFSET_Y;
            const startX = window.innerWidth / 2 - PIXEL_SIZE * line;
            for (let i = 0, x = startX; i < generation.length; i++, x += PIXEL_SIZE) {
              if (generation[i] == 1) {
                p.fill(0, 0, 0, 160);
              } else {
                p.fill(255, 255, 255, 160);
              }
              p.square(x, y, PIXEL_SIZE);
            }
          }
        }
      });
    },
    restart: function() {
      const { rule, ruleCheckboxes } = this.data;
      this.data = {
        ...initialState(),
        rule,
        ruleCheckboxes
      }
    },
    updateRule: function() {
      this.data.rule = checkboxesToRule(this.data.ruleCheckboxes);
      if (this.data.stepsLeftToFill <= 0) {
        this.clear();
      }
    },
    step: function(cycles = 1) {
      this.drawGenerations(cycles);
    },
    stepUntilFilled: function() {
      this.step(this.data.totalStepsToFill - this.data.currentGenerationNumber);
    },
    clear: function() {
      this.restart();
      this.p5.clear();
    },
    drawOneGeneration: function(line, generation) {
      this.data.scheduledToDraw.push({ line, generation });
    },
    drawGenerations: function(cycles) {
      let { rule, currentGeneration, currentGenerationNumber, totalStepsToFill } = this.data;

      for (let i = 0; i < cycles; i++) {
        const nextGeneration = calculateNextGeneration(rule, currentGeneration);
        this.drawOneGeneration(currentGenerationNumber, nextGeneration);
        currentGeneration = nextGeneration;
        currentGenerationNumber++;
      }

      const stepsLeftToFill = totalStepsToFill - currentGenerationNumber;

      this.data = {
        ...this.data,
        currentGeneration,
        currentGenerationNumber,
        stepsLeftToFill
      };
    }
  }));
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.min.js