diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..12e96b4
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,11 @@
+{
+ "env": {
+ "browser": true,
+ "es2015": true
+ },
+ "extends": [
+ "airbnb-base"
+ ],
+ "rules": {
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..074ab52
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+package-lock.json
+node_modules
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9aa5f53
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "exploraton",
+ "version": "1.0.0",
+ "description": "",
+ "main": "main.js",
+ "scripts": {
+ "serve": "python3 -m http.server 8001 --directory src"
+ },
+ "author": "gator",
+ "license": "MIT",
+ "devDependencies": {
+ "eslint": "^8.5.0",
+ "eslint-config-airbnb-base": "^15.0.0",
+ "eslint-plugin-import": "^2.25.3"
+ }
+}
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..fc5d490
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,109 @@
+
+
+ Cellular Automaton Explorer
+
+
+
+
+ Cellular Automaton Explorer
+
+
+
+
+
+
+
+
+
diff --git a/src/js/automata.js b/src/js/automata.js
new file mode 100644
index 0000000..588e83e
--- /dev/null
+++ b/src/js/automata.js
@@ -0,0 +1,52 @@
+// TODO: Hide accumulator inside
+function evolve(state, acc, rules) {
+ const [x, y, z, ...xs] = state;
+ if (!xs.length) {
+ return acc[acc.length - 1] + acc + acc[0];
+ }
+
+ const rule = x + y + z;
+ const newAcc = acc.concat(rules[rule]);
+ return evolve(y + z + xs.join(''), newAcc, rules);
+}
+
+function conwayRules(cell, neighbors) {
+ // loneliness rule
+ if (cell === 1 && neighbors < 2) return 0;
+ // overpopulation rule
+ if (cell === 1 && neighbors > 3) return 0;
+ // born when three live neighbors rule
+ if (cell === 0 && neighbors === 3) return 1;
+ // the cell remains the same if none apply
+ return cell;
+}
+
+function getDrawingValues(state, acc, cell) {
+ const d = cell.dimension;
+ return Object.keys(state).map(
+ (key) => {
+ const fillStyle = state[key] === '1' ? cell.liveColor : cell.deadColor;
+ return {
+ move: [key * d, acc * d],
+ fill: [key * d, acc * d, d, d],
+ fillStyle,
+ };
+ },
+ );
+}
+
+function initialState1D(width, initFn, args) {
+ return [...Array(width)].map(
+ () => initFn(...args).toString(),
+ ).join('');
+}
+
+function initialState2D(width, height, initFn, args) {
+ return [...Array(height)].map(
+ () => [...Array(width)].map(() => initFn(...args)),
+ );
+}
+
+export {
+ evolve, getDrawingValues, initialState1D, initialState2D,
+};
diff --git a/src/js/common.js b/src/js/common.js
new file mode 100644
index 0000000..1b471e5
--- /dev/null
+++ b/src/js/common.js
@@ -0,0 +1,12 @@
+function getRandomInt(min, max) {
+ const minVal = Math.ceil(min);
+ const maxVal = Math.floor(max);
+ // The maximum is exclusive and the minimum is inclusive
+ return Math.floor(Math.random() * (maxVal - minVal) + minVal);
+}
+
+async function sleep(ms) {
+ await new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export { getRandomInt, sleep };
diff --git a/src/js/main.js b/src/js/main.js
new file mode 100644
index 0000000..aea920d
--- /dev/null
+++ b/src/js/main.js
@@ -0,0 +1,104 @@
+import { getRandomInt, sleep } from './common.js';
+import { evolve, getDrawingValues, initialState1D } from './automata.js';
+
+let drawing = 1;
+
+const form = Array.from(document.forms.rules.elements);
+const canvas = document.querySelector('#canvas');
+const ctx = canvas.getContext('2d');
+const main = document.querySelector('#main');
+const dead = document.querySelector('#dead');
+const live = document.querySelector('#live');
+const startBtn = document.querySelector('#start');
+const resetBtn = document.querySelector('#reset');
+const stopBtn = document.querySelector('#stop');
+const cellSize = document.querySelector('#cellSize');
+const loop = document.querySelector('#loop');
+const menuRow = document.querySelectorAll('.menu-row');
+
+canvas.width = main.offsetWidth * 0.9;
+canvas.height = main.offsetHeight * 0.9;
+
+function getRules() {
+ const rules = {};
+
+ form.reduce((_, i) => {
+ if (i !== undefined
+ && i.type === 'checkbox') {
+ if (i.checked) rules[i.name] = '1';
+ else rules[i.name] = '0';
+ }
+ return rules;
+ }, rules);
+
+ return rules;
+}
+
+function reset() {
+ drawing = 0;
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+}
+
+async function draw(state, acc) {
+ if (drawing === 0) {
+ return;
+ }
+
+ const cell = {
+ dimension: cellSize.value,
+ liveColor: live.value,
+ deadColor: dead.value,
+ };
+
+ const position = acc * cell.dimension;
+ if (position >= canvas.height && !loop.checked) return;
+
+ const rules = getRules();
+ const newState = evolve(state, '', rules);
+ const line = getDrawingValues(newState, acc, cell);
+
+ line.map((c) => {
+ ctx.moveTo(...c.move);
+ ctx.fillRect(...c.fill);
+ ctx.fillStyle = c.fillStyle;
+ return c;
+ });
+
+ await sleep(40);
+
+ const newAcc = () => {
+ if (position >= canvas.height && loop.checked) return 0;
+ return acc;
+ };
+
+ await draw(newState, newAcc() + 1);
+}
+
+startBtn.addEventListener('click', async () => {
+ reset();
+
+ await sleep(60);
+
+ drawing = 1;
+
+ const initialState = initialState1D(canvas.width, getRandomInt, [0, 2]);
+
+ draw(initialState, 1);
+});
+
+resetBtn.addEventListener('click', async () => {
+ reset();
+});
+
+stopBtn.addEventListener('click', async () => {
+ drawing = 0;
+});
+
+menuRow.forEach((elem) => {
+ elem.querySelector('h2').addEventListener('click', async (e) => {
+ const parent = e.currentTarget.parentNode;
+ const menuDisplay = parent.querySelector('.menu-row-content').style;
+ if (menuDisplay.display !== 'none') menuDisplay.setProperty('display', 'none');
+ else menuDisplay.setProperty('display', 'block');
+ });
+});
diff --git a/src/style.css b/src/style.css
new file mode 100644
index 0000000..b2e3975
--- /dev/null
+++ b/src/style.css
@@ -0,0 +1,61 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background: black;
+ color: white;
+ display: flex;
+ font-family: Courier New;
+}
+
+canvas {
+ background: #110812;
+ margin: 20px 0 0 0;
+}
+
+h1, h2 {
+ font-weight: bold;
+}
+
+h1 {
+ font-size: large;
+}
+
+.menu-row h2 {
+ font-size: medium;
+ padding: 10px;
+ cursor: pointer;
+ border: 2px solid darkgrey;
+}
+
+sidebar {
+ flex: auto;
+ padding: 10px;
+}
+
+input[type="button"] {
+ min-width: 60px;
+ padding: 5px;
+ font-weight: bold;
+}
+
+.form-field {
+ display: flex;
+ margin: 10px;
+}
+
+.menu-row {
+ flex: 1;
+ margin: 10px 0;
+}
+
+label, .form-field label {
+ margin-right: 10px;
+ font-weight: bold;
+}
+
+#main {
+ flex: 4;
+}