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; +}