@manufosela/living-wccell

@manufosela/living-wccell

Cellular automaton cell component implementing Conway's Game of Life rules

living-wccell Demo - Conway's Game of Life

Click cells to toggle their state. Use the controls to start, stop, or step through generations.

200ms

Generation

0

Alive Cells

0

Births

0

Deaths

0

Load Pattern

Demo code (CodePen-ready HTML, CSS, JS)
HTML (html)
<h1>living-wccell Demo - Conway's Game of Life</h1>
  <p class="description">Click cells to toggle their state. Use the controls to start, stop, or step through generations.</p>

  <div class="controls">
    <button id="startBtn">Start</button>
    <button id="stepBtn">Step</button>
    <button id="clearBtn" class="danger">Clear</button>
    <button id="randomBtn">Randomize</button>
    <div class="speed-control">
      <label for="speedSlider">Speed:</label>
      <input type="range" id="speedSlider" min="50" max="1000" value="200">
      <span id="speedValue">200ms</span>
    </div>
    <select id="rulesSelect">
      <option value="CONWAY">Conway's Game of Life (B3/S23)</option>
      <option value="HIGHLIFE">HighLife (B36/S23)</option>
      <option value="DAY_AND_NIGHT">Day & Night (B3678/S34678)</option>
      <option value="SEEDS">Seeds (B2/S)</option>
      <option value="MAZE">Maze (B3/S12345)</option>
      <option value="REPLICATOR">Replicator (B1357/S1357)</option>
    </select>
  </div>

  <div class="stats">
    <div class="stat-card">
      <h3>Generation</h3>
      <div class="stat-value" id="genCounter">0</div>
    </div>
    <div class="stat-card">
      <h3>Alive Cells</h3>
      <div class="stat-value" id="aliveCounter">0</div>
    </div>
    <div class="stat-card">
      <h3>Births</h3>
      <div class="stat-value" id="birthCounter">0</div>
    </div>
    <div class="stat-card">
      <h3>Deaths</h3>
      <div class="stat-value" id="deathCounter">0</div>
    </div>
  </div>

  <div class="presets">
    <h3>Load Pattern</h3>
    <div class="preset-buttons">
      <button id="gliderBtn">Glider</button>
      <button id="blinkerBtn">Blinker</button>
      <button id="toadBtn">Toad</button>
      <button id="beaconBtn">Beacon</button>
      <button id="pulsarBtn">Pulsar</button>
      <button id="gliderGunBtn">Gosper Glider Gun</button>
    </div>
  </div>

  <div class="grid-container">
    <div class="grid" id="grid" role="grid"></div>
  </div>
CSS (css)
:root {
  --bg: #0c0f14;
  --bg-elevated: #141923;
  --bg-panel: #171d28;
  --border: #262f3f;
  --text: #f4f6fb;
  --text-muted: #a7b0c2;
  --text-dim: #7d879b;
  --accent: #ff8a3d;
  --accent-strong: #ff6a00;
  --accent-soft: rgba(255, 138, 61, 0.16);
  --shadow: 0 20px 50px rgba(5, 8, 14, 0.45);
  --radius-lg: 22px;
  --radius-md: 14px;
  --radius-sm: 10px;
  --max-width: 1160px;
}

* {
      box-sizing: border-box;
    }

    h1 {
      color: var(--accent);
      margin-bottom: 10px;
    }

    .description {
      color: var(--text-muted);
      margin-bottom: 20px;
    }

    .controls {
      display: flex;
      gap: 10px;
      margin-bottom: 20px;
      flex-wrap: wrap;
      align-items: center;
    }

    button {
      padding: 10px 20px;
      font-size: 14px;
      background: var(--accent);
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      transition: background 0.2s;
    }

    button:hover {
      background: var(--accent-strong);
    }

    button.active {
      background: #2ecc71;
    }

    button.danger {
      background: #e74c3c;
    }

    button.danger:hover {
      background: #c0392b;
    }

    select {
      padding: 10px;
      font-size: 14px;
      background: var(--bg-panel);
      color: var(--text);
      border: 1px solid var(--accent);
      border-radius: 4px;
      cursor: pointer;
    }

    .grid-container {
      display: inline-block;
      background: var(--bg-elevated);
      padding: 10px;
      border-radius: 8px;
      margin-bottom: 20px;
    }

    .grid {
      display: grid;
      gap: 1px;
    }

    .stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: 15px;
      margin-bottom: 20px;
      max-width: 800px;
    }

    .stat-card {
      background: var(--bg-elevated);
      padding: 15px;
      border-radius: 8px;
      border: 1px solid var(--border);
    }

    .stat-card h3 {
      margin: 0 0 10px 0;
      color: var(--accent);
      font-size: 14px;
    }

    .stat-value {
      font-size: 24px;
      font-weight: bold;
    }

    .presets {
      margin-bottom: 20px;
    }

    .presets h3 {
      color: var(--accent);
      margin-bottom: 10px;
    }

    .preset-buttons {
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
    }

    .preset-buttons button {
      background: var(--bg-panel);
      font-size: 12px;
      padding: 8px 15px;
    }

    .preset-buttons button:hover {
      background: var(--accent);
    }

    .speed-control {
      display: flex;
      align-items: center;
      gap: 10px;
    }

    .speed-control input {
      width: 150px;
    }

    /* Custom cell theming */
    living-wccell {
      --living-wccell-size: 15px;
      --living-wccell-border-color: var(--bg-panel);
      --living-wccell-dead-color: var(--bg-elevated);
      --living-wccell-alive-color: var(--accent);
      --living-wccell-hover-scale: 1.2;
    }
JS (js)
import "https://esm.sh/@manufosela/living-wccell";
    import { CellularRules } from "https://esm.sh/@manufosela/living-wccell";

    const GRID_WIDTH = 50;
    const GRID_HEIGHT = 35;

    const grid = document.getElementById('grid');
    const cells = [];
    let running = false;
    let intervalId = null;
    let generation = 0;
    let births = 0;
    let deaths = 0;
    let speed = 200;

    // Create grid
    grid.style.gridTemplateColumns = `repeat(${GRID_WIDTH}, var(--living-wccell-size, 15px))`;

    for (let y = 0; y < GRID_HEIGHT; y++) {
      cells[y] = [];
      for (let x = 0; x < GRID_WIDTH; x++) {
        const cell = document.createElement('living-wccell');
        cell.x = x;
        cell.y = y;
        cell.cellId = `cell-${x}-${y}`;
        cells[y][x] = cell;
        grid.appendChild(cell);

        cell.addEventListener('state-change', (e) => {
          if (e.detail.alive) {
            births++;
          } else {
            deaths++;
          }
          updateStats();
        });
      }
    }

    // Get neighbor count for a cell
    function getNeighborCount(x, y) {
      let count = 0;
      for (let dy = -1; dy <= 1; dy++) {
        for (let dx = -1; dx <= 1; dx++) {
          if (dx === 0 && dy === 0) continue;

          const nx = (x + dx + GRID_WIDTH) % GRID_WIDTH;
          const ny = (y + dy + GRID_HEIGHT) % GRID_HEIGHT;

          if (cells[ny][nx].alive) {
            count++;
          }
        }
      }
      return count;
    }

    // Advance one generation
    function step() {
      // Calculate next states
      for (let y = 0; y < GRID_HEIGHT; y++) {
        for (let x = 0; x < GRID_WIDTH; x++) {
          const neighbors = getNeighborCount(x, y);
          cells[y][x].calculateNextState(neighbors);
        }
      }

      // Apply next states
      for (let y = 0; y < GRID_HEIGHT; y++) {
        for (let x = 0; x < GRID_WIDTH; x++) {
          cells[y][x].applyNextState();
        }
      }

      generation++;
      updateStats();
    }

    // Update statistics display
    function updateStats() {
      document.getElementById('genCounter').textContent = generation;
      document.getElementById('birthCounter').textContent = births;
      document.getElementById('deathCounter').textContent = deaths;

      let aliveCount = 0;
      for (let y = 0; y < GRID_HEIGHT; y++) {
        for (let x = 0; x < GRID_WIDTH; x++) {
          if (cells[y][x].alive) aliveCount++;
        }
      }
      document.getElementById('aliveCounter').textContent = aliveCount;
    }

    // Clear grid
    function clearGrid() {
      for (let y = 0; y < GRID_HEIGHT; y++) {
        for (let x = 0; x < GRID_WIDTH; x++) {
          cells[y][x].reset();
        }
      }
      generation = 0;
      births = 0;
      deaths = 0;
      updateStats();
    }

    // Randomize grid
    function randomize() {
      clearGrid();
      for (let y = 0; y < GRID_HEIGHT; y++) {
        for (let x = 0; x < GRID_WIDTH; x++) {
          if (Math.random() < 0.3) {
            cells[y][x].setAlive(true);
          }
        }
      }
      updateStats();
    }

    // Set pattern at position
    function setPattern(pattern, offsetX = 0, offsetY = 0) {
      for (const [x, y] of pattern) {
        const cellX = (x + offsetX + GRID_WIDTH) % GRID_WIDTH;
        const cellY = (y + offsetY + GRID_HEIGHT) % GRID_HEIGHT;
        cells[cellY][cellX].setAlive(true);
      }
      updateStats();
    }

    // Pattern definitions
    const patterns = {
      glider: [[1, 0], [2, 1], [0, 2], [1, 2], [2, 2]],
      blinker: [[0, 1], [1, 1], [2, 1]],
      toad: [[1, 0], [2, 0], [3, 0], [0, 1], [1, 1], [2, 1]],
      beacon: [[0, 0], [1, 0], [0, 1], [3, 2], [2, 3], [3, 3]],
      pulsar: [
        [2, 0], [3, 0], [4, 0], [8, 0], [9, 0], [10, 0],
        [0, 2], [5, 2], [7, 2], [12, 2],
        [0, 3], [5, 3], [7, 3], [12, 3],
        [0, 4], [5, 4], [7, 4], [12, 4],
        [2, 5], [3, 5], [4, 5], [8, 5], [9, 5], [10, 5],
        [2, 7], [3, 7], [4, 7], [8, 7], [9, 7], [10, 7],
        [0, 8], [5, 8], [7, 8], [12, 8],
        [0, 9], [5, 9], [7, 9], [12, 9],
        [0, 10], [5, 10], [7, 10], [12, 10],
        [2, 12], [3, 12], [4, 12], [8, 12], [9, 12], [10, 12]
      ],
      gliderGun: [
        [24, 0], [22, 1], [24, 1], [12, 2], [13, 2], [20, 2], [21, 2], [34, 2], [35, 2],
        [11, 3], [15, 3], [20, 3], [21, 3], [34, 3], [35, 3], [0, 4], [1, 4], [10, 4],
        [16, 4], [20, 4], [21, 4], [0, 5], [1, 5], [10, 5], [14, 5], [16, 5], [17, 5],
        [22, 5], [24, 5], [10, 6], [16, 6], [24, 6], [11, 7], [15, 7], [12, 8], [13, 8]
      ]
    };

    // Event handlers
    document.getElementById('startBtn').addEventListener('click', () => {
      running = !running;
      const btn = document.getElementById('startBtn');
      if (running) {
        btn.textContent = 'Stop';
        btn.classList.add('active');
        intervalId = setInterval(step, speed);
      } else {
        btn.textContent = 'Start';
        btn.classList.remove('active');
        clearInterval(intervalId);
      }
    });

    document.getElementById('stepBtn').addEventListener('click', () => {
      if (!running) step();
    });

    document.getElementById('clearBtn').addEventListener('click', () => {
      if (running) {
        running = false;
        document.getElementById('startBtn').textContent = 'Start';
        document.getElementById('startBtn').classList.remove('active');
        clearInterval(intervalId);
      }
      clearGrid();
    });

    document.getElementById('randomBtn').addEventListener('click', () => {
      randomize();
    });

    document.getElementById('speedSlider').addEventListener('input', (e) => {
      speed = 1050 - parseInt(e.target.value);
      document.getElementById('speedValue').textContent = `${speed}ms`;
      if (running) {
        clearInterval(intervalId);
        intervalId = setInterval(step, speed);
      }
    });

    document.getElementById('rulesSelect').addEventListener('change', (e) => {
      const rules = CellularRules[e.target.value];
      for (let y = 0; y < GRID_HEIGHT; y++) {
        for (let x = 0; x < GRID_WIDTH; x++) {
          cells[y][x].rules = rules;
        }
      }
    });

    // Pattern buttons
    document.getElementById('gliderBtn').addEventListener('click', () => {
      clearGrid();
      setPattern(patterns.glider, 5, 5);
    });

    document.getElementById('blinkerBtn').addEventListener('click', () => {
      clearGrid();
      setPattern(patterns.blinker, 10, 10);
    });

    document.getElementById('toadBtn').addEventListener('click', () => {
      clearGrid();
      setPattern(patterns.toad, 10, 10);
    });

    document.getElementById('beaconBtn').addEventListener('click', () => {
      clearGrid();
      setPattern(patterns.beacon, 10, 10);
    });

    document.getElementById('pulsarBtn').addEventListener('click', () => {
      clearGrid();
      setPattern(patterns.pulsar, 15, 10);
    });

    document.getElementById('gliderGunBtn').addEventListener('click', () => {
      clearGrid();
      setPattern(patterns.gliderGun, 1, 5);
    });

    // Initial stats
    updateStats();