<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
};
}
}));
});
This Pen doesn't use any external CSS resources.