@manufosela/living-wccell
Cellular automaton cell component implementing Conway's Game of Life rules
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();