Compare commits

...

6 Commits

27 changed files with 32709 additions and 232 deletions

12
.eslintrc.js Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
extends: [
// add more generic rulesets here, such as:
'eslint:recommended',
//'plugin:vue/vue3-recommended',
'plugin:vue/recommended', // Use this if you are using Vue.js 2.x.
],
rules: {
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
}
}

View File

@ -1,11 +0,0 @@
{
"env": {
"browser": true,
"es2015": true
},
"extends": [
"airbnb-base"
],
"rules": {
}
}

26
.gitignore vendored
View File

@ -1,2 +1,24 @@
package-lock.json
node_modules
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.#*

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# explorata
Explore 1D and 2D cellular automata, with a few bells and whistles.
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
### References
- https://natureofcode.com/book/chapter-7-cellular-automata/
- https://en.wikipedia.org/wiki/Hashlife

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
}

31972
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,46 @@
{
"name": "exploraton",
"version": "1.0.0",
"description": "",
"main": "main.js",
"name": "explorata",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "python3 -m http.server 8001 --directory src"
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@vue/cli": "^4.5.15",
"@vue/cli-service-global": "^4.5.15",
"core-js": "^3.6.5",
"vue": "^2.6.11",
"vuex": "^3.6.2"
},
"author": "gator",
"license": "MIT",
"devDependencies": {
"eslint": "^8.5.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.25.3"
}
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

17
public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

72
src/App.vue Normal file
View File

@ -0,0 +1,72 @@
<template>
<div id="main">
<h1
id="main-title"
>
Cellular Automata Explorer
</h1>
<div id="container">
<MainMenu />
<Canvas />
</div>
</div>
</template>
<script>
import MainMenu from './components/MainMenu.vue'
import Canvas from './components/Canvas.vue'
export default {
name: 'App',
components: {
MainMenu,
Canvas
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
* {
margin: 0;
padding: 0;
}
body {
background: black;
color: white;
font-family: Courier New;
}
canvas {
flex: auto;
background: #110812;
margin-right: 10px;
}
h1, h2 {
font-weight: bold;
}
h1 {
margin : 10px auto;
font-size: larger;
text-align: center;
}
#main {
flex: 4;
}
#container {
display: flex;
}
</style>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

100
src/components/Canvas.vue Normal file
View File

@ -0,0 +1,100 @@
<template>
<main id="main">
<canvas
id="canvas"
ref="canvas"
:width="canvasWidth"
:height="canvasHeight"
/>
</main>
</template>
<script>
import { evolve2d, initialState1d, initialState2d, conwayRules, createBoard } from '../modules/automata.js'
import { getRandomInt, sleep } from '../modules/common.js'
import {mapGetters} from 'vuex'
export default {
name: 'Canvas',
data() {
return {
canvas: null,
ctx: null,
}
},
computed: {
...mapGetters({
cellProperties: 'getCellProperties',
rules: 'getRuleSet1d',
drawing: 'isDrawing',
canvasWidth: 'getCanvasWidth',
canvasHeight: 'getCanvasHeight'
})
},
mounted() {
this.canvas = this.$refs['canvas']
this.ctx = this.canvas.getContext('2d')
this.$root.$on('draw1d', () => { this.draw1d() })
this.$root.$on('draw2d', () => { this.draw2d() })
this.$root.$on('reset', () => { this.reset() })
this.$root.$on('stop', () => { this.stop() })
},
methods: {
drawCanvas(board) {
const props = this.cellProperties
board.map((row, y) => {
const d = props.size
return row.map(
(cell, x) => {
this.ctx.fillStyle = (
() => {
if (cell === 1) return props.liveColor
return props.deadColor
})()
this.ctx.fillRect(x * d, y * d, d, d)
return cell
},
)
})
},
async draw1d() {
const initialState = initialState1d(
Math.floor(this.canvas.width / this.cellProperties.size),
getRandomInt,
[0, 2],
)
const board = createBoard(
initialState,
this.rules,
Math.floor(this.canvas.height / this.cellProperties.size)
)
this.drawCanvas(board)
},
async draw2d() {
if (this.drawing === 0) return
const initialState = initialState2d(
Math.floor(this.canvas.width / this.cellProperties.size),
Math.floor(this.canvas.height / this.cellProperties.size),
getRandomInt,
[0, 2],
);
const board = evolve2d(initialState, conwayRules);
const draw2dNext = async (b) => {
if (this.drawing === 0) return
const newBoard = evolve2d(b, conwayRules)
this.drawCanvas(b, this.cellProperties)
await sleep(300)
draw2dNext(newBoard)
}
return draw2dNext(board)
},
stop() {
this.$store.commit('setDrawingStatus', 0)
},
reset() {
this.stop()
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
}
}
</script>

View File

@ -0,0 +1,9 @@
<template>
<div class="form-field" v-if="type === 'checkbox'">
<label>{{ name }}
<input type="checkbox" name="{{ name }}">
</label>
</div>
<div class="form-field" v-else-if="type === ''">
</div>
</template>

View File

@ -0,0 +1,32 @@
<template>
<div id="sidebar">
<MenuGeneralOptions />
<MenuCellProperties />
<MenuElementaryCA />
<Menu2dCA />
</div>
</template>
<script>
import MenuCellProperties from './MenuCellProperties.vue'
import MenuGeneralOptions from './MenuGeneralOptions.vue'
import MenuElementaryCA from './MenuElementaryCA.vue'
import Menu2dCA from './Menu2dCA.vue'
export default {
name: 'MainMenu',
components: {
MenuCellProperties,
MenuGeneralOptions,
MenuElementaryCA,
Menu2dCA
},
}
</script>
<style>
#sidebar {
flex-basis: fit-content;
padding: 0 10px;
width: fit-content;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<MenuRow row-title="2D Cellular Automata">
<div class="form-field">
<input
type="button"
name="start2d"
value="start"
@click="draw2d"
>
<input
type="button"
name="stop"
class="stop"
value="stop"
@click="stop"
>
<input
type="button"
name="reset"
class="reset"
value="reset"
@click="reset"
>
</div>
</MenuRow>
</template>
<script>
import MenuRow from './MenuRow.vue'
export default {
name: 'Menu2dCA',
components: {
MenuRow
},
methods: {
draw2d() {
this.$root.$store.state.drawing = 1
this.$root.$emit('draw2d')
},
reset() {
this.$root.$emit('reset')
},
stop() {
this.$root.$emit('stop')
},
}
}
</script>

View File

@ -0,0 +1,61 @@
<template>
<MenuRow row-title="Cell Properties">
<form>
<div class="form-field">
<label for="live">Living cell color</label>
<input
name="liveColor"
type="color"
@value="cellProperties.liveColor"
@input="updateCellProperties"
>
</div>
<div class="form-field">
<label for="dead">Dead cell color</label>
<input
name="deadColor"
type="color"
:value="cellProperties.deadColor"
@input="updateCellProperties"
>
</div>
<div class="form-field">
<label>Cell size</label>
<input
name="size"
type="number"
:value="cellProperties.size"
@input="updateCellProperties"
>
</div>
</form>
</MenuRow>
</template>
<script>
import MenuRow from './MenuRow.vue'
export default {
name: 'MainMenu',
components: {
MenuRow
},
data() {
return {
cellProperties: this.$store.state.cellProperties
}
},
methods: {
getCellProperties(event) {
const elem = event.target
const prop = this.$store.state.cellProperties
return prop[elem.name]
},
updateCellProperties(event) {
const elem = event.target
const prop = {'name' : elem.name, 'value' : elem.value}
//console.log(prop)
this.$store.commit('setCellProperties', prop)
}
},
}
</script>

View File

@ -0,0 +1,79 @@
<template>
<MenuRow row-title="Elementary Cellular Automata">
<div class="form-field">
<label>Rules</label>
</div>
<form>
<div
v-for="rule in rules"
:key="rule"
class="form-field"
>
<label>{{ rule }}
<input
:value="getRule(rule)"
type="checkbox"
:name="rule"
:checked="getRule(rule) == 1"
@input="update1dRules"
>
</label>
</div>
</form>
<div class="form-field">
<input
type="button"
name="start"
value="start"
@click="draw1d"
>
<!-- <input -->
<!-- type="button" -->
<!-- name="stop" -->
<!-- class="stop" -->
<!-- value="stop" -->
<!-- > -->
<input
type="button"
name="reset"
class="reset"
value="reset"
@click="reset()"
>
</div>
</MenuRow>
</template>
<script>
import MenuRow from './MenuRow.vue'
export default {
name: 'MenuElementaryCA',
components: {
MenuRow
},
data() {
return {
rules: ["111", "110", "101", "100", "011", "010", "001", "000"]
}
},
methods: {
update1dRules(event) {
const elem = event.target
const value = elem.checked ? 1 : 0
const data = { 'rule' : elem.name, 'value' : value}
this.$store.commit('update1dRules', data)
},
draw1d() {
this.$root.$store.state.drawing = 1
this.$root.$emit('draw1d')
},
getRule(id) {
const rules = this.$store.state.rules1d
return rules[id]
},
reset() {
this.$root.$emit('reset')
},
}
}
</script>

View File

@ -0,0 +1,54 @@
<template>
<MenuRow row-title="General Options">
<form>
<div class="form-field">
<label for="live">Canvas Resolution</label>
</div>
<div class="form-field">
<input
id="canvasWidth"
name="canvasWidth"
type="number"
:value="canvasWidth"
@input="updateCanvasWidth"
>
</div>
<div class="form-field">
<input
id="canvasHeight"
name="canvasHeight"
type="number"
:value="canvasHeight"
@input="updateCanvasHeight"
>
</div>
</form>
</MenuRow>
</template>
<script>
import MenuRow from './MenuRow.vue'
import {mapGetters} from 'vuex'
export default {
name: 'MenuGeneralOptions',
components: {
MenuRow
},
computed: {
...mapGetters({
canvasWidth: 'getCanvasWidth',
canvasHeight: 'getCanvasHeight'
})
},
methods: {
updateCanvasHeight: function(event) {
const elem = event.target
this.$store.commit('setCanvasHeight', elem.value)
},
updateCanvasWidth: function(event) {
const elem = event.target
this.$store.commit('setCanvasWidth', elem.value)
}
}
}
</script>

View File

@ -0,0 +1,63 @@
<template>
<div class="menu-row">
<h2
@click="isHidden = !isHidden"
>
{{ rowTitle }}
</h2>
<div
v-if="!isHidden"
class="menu-row-content"
>
<slot />
</div>
</div>
</template>
<script>
export default {
name: 'MenuRow',
props: {
rowTitle: {
type: String,
default : ''
}
},
data() {
return {
isHidden: true
}
}
}
</script>
<style>
.menu-row h2 {
font-size: medium;
padding: 10px;
cursor: pointer;
border: 2px solid darkgrey;
margin: 0 0 10px 0;
}
input[type="button"] {
min-width: 60px;
padding: 5px;
font-weight: bold;
margin-right: 10px;
}
.form-field {
display: flex;
margin: 10px;
}
.menu-row {
flex: 1;
}
label, .form-field label {
margin-right: 10px;
font-weight: bold;
}
</style>

View File

@ -1,122 +0,0 @@
<html>
<head>
<title>Cellular Automaton Explorer</title>
<link rel="stylesheet" href="./style.css" >
</head>
<body>
<h1>Cellular Automata Explorer</h1>
<div id="container">
<sidebar>
<div class="menu-row">
<h2>General Properties</h2>
<div class="menu-row-content">
<form>
<div class="form-field">
<label for="live">Canvas Resolution</label>
</div>
<div class="form-field">
<input name="canvasWidth" type="number" id="canvasWidth" value="1280"/>
</div>
<div class="form-field">
<input name="canvasHeight" type="number" id="canvasHeight" value="1024"/>
</div>
<div class="form-field">
<input type="button" name="canvasRefresh" id="canvasRefresh" value="refresh"/>
</div>
</form>
</div>
</div>
<div class="menu-row">
<h2>Cell Properties</h2>
<div class="menu-row-content">
<form>
<div class="form-field">
<label for="live">Living cell color</label>
<input name="live" type="color" id="live" value="#000000"/>
</div>
<div class="form-field">
<label for="dead">Dead cell color</label>
<input name="dead" type="color" id="dead" value="#AA78E8"/>
</div>
<div class="form-field">
<label>Cell size</label>
<input name="cellSize" type="number" id="cellSize" value="5"/>
</div>
</form>
</div>
</div>
<div class="menu-row">
<h2>Elementary Cellular Automata</h2>
<div class="menu-row-content">
<form name="rules">
<div class="form-field">
<label>Rules</label>
</div>
<div class="form-field">
<label>111
<input type="checkbox" name="111" checked>
</label>
</div>
<div class="form-field">
<label>110
<input type="checkbox" name="110">
</label>
</div>
<div class="form-field">
<label>101
<input type="checkbox" name="101" checked>
</label>
</div>
<div class="form-field">
<label>100
<input type="checkbox" name="100">
</label>
</div>
<div class="form-field">
<label>011
<input type="checkbox" name="011" checked>
</label>
</div>
<div class="form-field">
<label>010
<input type="checkbox" name="010">
</label>
</div>
<div class="form-field">
<label>001
<input type="checkbox" name="001">
</label>
</div>
<div class="form-field">
<label>000
<input type="checkbox" name="000" checked>
</label>
</div>
<div class="form-field">
<input type="button" name="start" id="start" value="start"/>
<input type="button" name="stop" class="stop" value="stop"/>
<input type="button" name="reset" class="reset" value="reset"/>
</div>
</form>
</div>
</div>
<div class="menu-row">
<h2>2D Cellular Automata</h2>
<div class="menu-row-content">
<form>
<div class="form-field">
<input type="button" name="start2d" id="start2d" value="start"/>
<input type="button" name="stop" class="stop" value="stop"/>
<input type="button" name="reset" class="reset" value="reset"/>
</div>
</form>
</div>
</div>
</sidebar>
<main id="main">
<canvas width="500" height="500" id="canvas"></canvas>
</main>
</div>
<script type="module" src="./js/main.js"></script>
</body>
</html>

10
src/main.js Normal file
View File

@ -0,0 +1,10 @@
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App)
}).$mount('#app')

View File

@ -144,23 +144,3 @@ canvasRefreshBtn.addEventListener('click', () => {
canvas.width = width;
canvas.height = height;
});
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');
});
});
window.addEventListener('load', () => {
resizeCanvas();
menuRowContent.forEach((elem) => {
elem.style.setProperty('display', 'none');
});
});
window.addEventListener('resize', () => {
// resizeCanvas();
});

68
src/store/index.js Normal file
View File

@ -0,0 +1,68 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
drawing: 0,
rules1d : {
"111" : 0,
"110" : 1,
"101" : 0,
"100" : 0,
"011" : 1,
"010" : 0,
"001" : 0,
"000" : 1
},
cellProperties: {
size: 3,
liveColor: '#000000',
deadColor: '#AA78E8',
},
canvasWidth: 1280,
canvasHeight: 720,
},
mutations: {
update1dRules(state, data) {
state.rules1d[data.rule] = data.value
},
setCellProperties(state, data) {
state.cellProperties[data.name] = data.value
},
setDrawingStatus(state, data) {
state.drawing = data
},
setCanvasWidth(state, data) {
state.canvasWidth = data;
},
setCanvasHeight(state, data) {
state.canvasHeight = data;
}
},
getters: {
getCellProperties(state) {
return state.cellProperties
},
getRuleSet1d(state) {
return state.rules1d
},
getRule1d(state) {
return (rule) => state.rules1d[rule]
},
isDrawing(state) {
return state.drawing
},
getCanvasWidth(state) {
return state.canvasWidth
},
getCanvasHeight(state) {
return state.canvasHeight
}
},
actions: {
},
modules: {
}
})

View File

@ -1,66 +0,0 @@
* {
margin: 0;
padding: 0;
}
body {
background: black;
color: white;
font-family: Courier New;
}
canvas {
background: #110812;
}
h1, h2 {
font-weight: bold;
}
h1 {
margin : 10px auto;
font-size: larger;
text-align: center;
}
.menu-row h2 {
font-size: medium;
padding: 10px;
cursor: pointer;
border: 2px solid darkgrey;
margin: 0 0 10px 0;
}
sidebar {
padding: 0 10px;
width: fit-content;
}
input[type="button"] {
min-width: 60px;
padding: 5px;
font-weight: bold;
margin-right: 10px;
}
.form-field {
display: flex;
margin: 10px;
}
.menu-row {
flex: 1;
}
label, .form-field label {
margin-right: 10px;
font-weight: bold;
}
#main {
flex: 4;
}
#container {
display: flex;
}

13
vue.config.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
configureWebpack: {
devServer: {
overlay: {
warnings: true,
errors: true
},
watchOptions: {
ignored: [/node_modules/, /public/, /\.#/],
}
}
}
}