Compare commits

..

No commits in common. "f9354d0a17940813c33c6d70f51f1550868c3dcf" and "980be551c1acdc7d573487464b29deeddf044893" have entirely different histories.

28 changed files with 31547 additions and 3305 deletions

View File

@ -1,25 +1,12 @@
module.exports = { module.exports = {
env: {
node: true, // remove
es2021: true,
},
extends: [ extends: [
// add more generic rulesets here, such as: // add more generic rulesets here, such as:
"eslint:recommended", 'eslint:recommended',
"plugin:vue/vue3-essential", //'plugin:vue/vue3-recommended',
"plugin:vue/vue3-strongly-recommended", 'plugin:vue/recommended', // Use this if you are using Vue.js 2.x.
"plugin:vue/vue3-recommended",
"prettier",
], ],
rules: { rules: {
// override/add rules settings here, such as: // override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error' // 'vue/no-unused-vars': 'error'
"vue/match-component-import-name": "warn", }
"vue/no-ref-object-destructure": "warn", }
"vue/no-required-prop-with-default": "warn",
"vue/no-restricted-class": "warn",
"vue/no-template-target-blank": "warn",
"vue/no-this-in-before-route-enter": "warn",
"vue/prefer-prop-type-boolean-first": "warn",
},
};

View File

@ -1 +0,0 @@
{}

View File

@ -1,23 +0,0 @@
FROM node:lts-alpine
# install simple http server for serving static content
RUN npm install -g http-server
# make the 'app' folder the current working directory
WORKDIR /app
# copy both 'package.json' and 'package-lock.json' (if available)
COPY package*.json ./
# build stage
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

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

View File

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

View File

@ -1,15 +0,0 @@
version: "3.4"
services:
explorata:
container_name: explorata
build: .
restart: unless-stopped
ports:
- "8080:8080"
networks:
- ariona
networks:
ariona:
external: true

View File

@ -1,21 +0,0 @@
<!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="/favicon.ico" />
<title>Explorata</title>
</head>
<body>
<noscript>
<strong>
Althought you're right to browse the Web with Javascript disabled,
Explorata doesn't work properly without it. Please enable Javascript to
continue.
</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

33361
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,24 +3,26 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "serve": "vue-cli-service serve",
"build": "vite build", "build": "vue-cli-service build",
"serve": "vite preview", "lint": "vue-cli-service lint"
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
}, },
"dependencies": { "dependencies": {
"@vitejs/plugin-vue": "^3.2.0", "@vue/cli": "^4.5.15",
"install": "^0.13.0", "@vue/cli-service-global": "^4.5.15",
"vite": "^3.2.4", "core-js": "^3.6.5",
"vue": "3.2", "vue": "^2.6.11",
"vuex": "4.1" "vuex": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.28.0", "@vue/cli-plugin-babel": "~4.5.0",
"eslint-config-prettier": "^8.5.0", "@vue/cli-plugin-eslint": "~4.5.0",
"eslint-plugin-vue": "^9.8.0", "@vue/cli-plugin-vuex": "~4.5.0",
"prettier": "2.8.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": { "eslintConfig": {
"root": true, "root": true,

View File

Before

Width:  |  Height:  |  Size: 11 KiB

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>

View File

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

View File

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

@ -1,146 +0,0 @@
<template>
<main id="mainContent">
<canvas
id="canvas-board"
ref="canvas-board"
:width="canvasWidth"
:height="canvasHeight"
/>
</main>
</template>
<script>
import {
create1dState,
create1dStateOneCell,
create2dState,
createBoard,
conwayRules,
evolve2d,
} from "../modules/automata.js";
import { getRandomInt, sleep } from "../modules/common.js";
import { mapGetters } from "vuex";
export default {
name: "CanvasBoard",
data() {
return {
canvas: null,
ctx: null,
};
},
computed: {
...mapGetters({
cellProperties: "getCellProperties",
rules: "get1dRules",
canvasWidth: "getCanvasWidth",
canvasHeight: "getCanvasHeight",
refreshRate: "getRefreshRate",
initial1dState: "getInitial1dState",
drawingDirection: "getDrawingDirection",
canDraw: "getCanDraw",
lastBoard: "getLastBoard",
getDraw1d: "getDraw1d",
getDraw2d: "getDraw2d",
getDraw2dLast: "getDraw2dLast",
getReset: "getReset",
}),
boardWidth: function () {
return Math.floor(this.canvasWidth / this.cellProperties.size);
},
boardHeight: function () {
return Math.floor(this.canvasHeight / this.cellProperties.size);
},
},
watch: {
getDraw1d(value) {
if (value == true) this.draw1d();
},
getDraw2d(value) {
if (value == true) this.draw2dNew();
},
getDraw2dLast(value) {
if (value == true) this.draw2dLast();
},
getReset(value) {
if (value == true) this.reset();
},
},
mounted() {
this.canvas = Object.freeze(document.getElementById("canvas-board"));
this.ctx = this.canvas.getContext("2d");
this.$store.commit("setCanvasWidth", this.canvas.parentElement.clientWidth);
this.$store.commit(
"setCanvasHeight",
this.canvas.parentElement.clientHeight
);
},
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;
})();
if (this.drawingDirection === "x")
this.ctx.fillRect(y * d, x * d, d, d);
else this.ctx.fillRect(x * d, y * d, d, d);
return cell;
});
});
},
compute1dInitialState() {
if (this.initial1dState === "onecell")
return create1dStateOneCell(this.boardWidth);
return create1dState(this.boardWidth, getRandomInt, [0, 2]);
},
draw1d() {
const initialState = this.compute1dInitialState();
const board = createBoard(
initialState,
this.rules.rules,
this.boardWidth
);
this.$store.commit("setLastBoard", Object.freeze(board));
this.drawCanvas(board);
this.$store.dispatch("stop");
},
draw2d(board) {
if (!this.canDraw) return;
const draw2dNext = async (b) => {
if (!this.canDraw) return;
const newBoard = evolve2d(b, conwayRules);
this.drawCanvas(b, this.cellProperties);
await sleep(this.refreshRate);
draw2dNext(newBoard);
};
return draw2dNext(board);
},
draw2dNew() {
const initialState = create2dState(
this.boardWidth,
this.boardHeight,
getRandomInt,
[0, 2]
);
const board = evolve2d(initialState, conwayRules);
this.$store.commit("setLastBoard", Object.freeze(board));
this.draw2d(board);
},
async draw2dLast() {
this.draw2d(this.lastBoard);
},
reset() {
this.$store.dispatch("stop");
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.$store.commit("toggleReset", 0);
},
},
};
</script>
<style>
#mainContent {
min-width: 70%;
}
</style>

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

@ -8,55 +8,25 @@
</template> </template>
<script> <script>
import MenuCellProperties from "./MenuCellProperties.vue"; import MenuCellProperties from './MenuCellProperties.vue'
import MenuGeneralOptions from "./MenuGeneralOptions.vue"; import MenuGeneralOptions from './MenuGeneralOptions.vue'
import MenuElementaryCA from "./MenuElementaryCA.vue"; import MenuElementaryCA from './MenuElementaryCA.vue'
import Menu2dCA from "./Menu2dCA.vue"; import Menu2dCA from './Menu2dCA.vue'
export default { export default {
name: "MainMenu", name: 'MainMenu',
components: { components: {
MenuCellProperties, MenuCellProperties,
MenuGeneralOptions, MenuGeneralOptions,
MenuElementaryCA, MenuElementaryCA,
Menu2dCA, Menu2dCA
}, },
}; }
</script> </script>
<style> <style>
#sidebar { #sidebar {
width: 25%; flex-basis: fit-content;
padding: 0 10px; padding: 0 10px;
overflow-y: scroll; width: fit-content;
}
/* Hide scrollbar for Chrome, Safari and Opera */
#sidebar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
#sidebar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@media screen and (max-width: 800px) {
#container {
display: flex;
flex-direction: column;
justify-content: center;
}
#mainContent {
flex: 1;
width: 100%;
}
#sidebar {
flex: 1;
padding: 0;
width: 100%;
}
} }
</style> </style>

View File

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

View File

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

View File

@ -1,245 +1,79 @@
<template> <template>
<MenuRow row-title="Elementary Cellular Automata"> <MenuRow row-title="Elementary Cellular Automata">
<div class="form-field">
<label>Rules</label>
</div>
<form> <form>
<div class="form-field">
<label>
Initial state presets
<br />
<select
name="initialStates"
:value="initialState"
@input="updateInitialState"
>
<option
v-for="(state, index) in initialStates"
:key="'initial-state-elementary' + index"
:value="state.id"
>
{{ state.name }}
</option>
</select>
</label>
</div>
<div class="form-field">
<label>Rules</label>
</div>
<div class="form-field">
<label
>Rules presets
<br />
<select
name="ruleset-elementary"
:value="rules.name"
@input="updateRules"
>
<option
v-for="(ruleset, index) in presetRules"
:key="'ruleset-preset-elementary-' + index"
:value="ruleset.name"
>
{{ ruleset.name }}
</option>
</select>
</label>
</div>
<div class="form-field">
<a style="cursor: pointer" @click="copyRules">copy rules</a>
</div>
<div <div
v-for="(rule, name, index) in rules.rules" v-for="rule in rules"
:key="'rule-' + index" :key="rule"
class="form-field" class="form-field"
> >
<label <label>{{ rule }}
>{{ name }}
<input <input
:value="rule" :value="getRule(rule)"
type="checkbox" type="checkbox"
:name="name" :name="rule"
:checked="rule" :checked="getRule(rule) == 1"
@input="updateSingleRule" @input="update1dRules"
/> >
</label> </label>
</div> </div>
</form> </form>
<div class="form-field"> <div class="form-field">
<input type="button" name="start" value="start" @click="draw1d" /> <input
type="button"
name="start"
value="start"
@click="draw1d"
>
<!-- <input -->
<!-- type="button" -->
<!-- name="stop" -->
<!-- class="stop" -->
<!-- value="stop" -->
<!-- > -->
<input <input
type="button" type="button"
name="reset" name="reset"
class="reset" class="reset"
value="reset" value="reset"
@click="reset" @click="reset()"
/> >
</div> </div>
</MenuRow> </MenuRow>
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import MenuRow from './MenuRow.vue'
import MenuRow from "./MenuRow.vue";
export default { export default {
name: "MenuElementaryCA", name: 'MenuElementaryCA',
components: { components: {
MenuRow, MenuRow
}, },
data() { data() {
// TODO: Why not a getter in the store?
return { return {
presetRules: [ rules: ["111", "110", "101", "100", "011", "010", "001", "000"]
{ }
name: "rule 73",
rules: {
100: 0,
101: 0,
110: 1,
111: 0,
"011": 1,
"010": 0,
"001": 0,
"000": 1,
},
},
{
name: "rule 86",
rules: {
100: 1,
101: 0,
110: 0,
111: 1,
"011": 0,
"010": 1,
"001": 0,
"000": 1,
},
},
{
name: "rule 90",
rules: {
100: 1,
101: 0,
110: 1,
111: 0,
"011": 0,
"010": 0,
"001": 1,
"000": 0,
},
},
{
name: "rule 45?",
rules: {
100: 0,
101: 0,
110: 1,
111: 0,
"011": 1,
"010": 0,
"001": 1,
"000": 1,
},
},
{
name: "rule 54?",
rules: {
100: 1,
101: 0,
110: 1,
111: 1,
"011": 0,
"010": 1,
"001": 1,
"000": 0,
},
},
{
name: "unknown rule",
rules: {
100: 0,
101: 0,
110: 0,
111: 1,
"011": 0,
"010": 0,
"001": 1,
"000": 1,
},
},
],
initialStates: [
{
id: "onecell",
name: "One cell at center",
description: "State with a single cell in the middle",
},
{
id: "random",
name: "Random cell",
description: "State populated with random cells",
},
],
};
},
computed: {
...mapGetters({
initialState: "getInitial1dState",
rules: "get1dRules",
}),
rules1dFileName() {
return (
Object.keys(this.rules)
.map((index) => {
return this.rules[index];
})
.join("_") + ".json"
);
},
}, },
methods: { methods: {
copyRules() { update1dRules(event) {
const rules = JSON.stringify(this.rules); const elem = event.target
navigator.clipboard.writeText(rules); const value = elem.checked ? 1 : 0
}, const data = { 'rule' : elem.name, 'value' : value}
isCurrentPreset(event) { this.$store.commit('update1dRules', data)
const elem = event.target;
return this.initialState === elem.value;
},
updateSingleRule(event) {
const elem = event.target;
const value = elem.checked ? 1 : 0;
const data = { rule: elem.name, value: value };
this.$store.commit("update1dSingleRule", data);
},
updateRules(event) {
// TODO : change this, awfully confusing
const elem = event.target;
const name = elem.value;
const rules = this.presetRules.find((ruleset) => {
return ruleset.name === name;
});
Object.keys(rules.rules).map((value) => {
const data = { name: name, rule: value, value: rules.rules[value] };
this.$store.commit("update1dSingleRule", data);
});
},
updateInitialState(event) {
const elem = event.target;
this.$store.commit("setInitial1dState", elem.value);
}, },
draw1d() { draw1d() {
this.$store.dispatch("draw1d"); this.$root.$store.state.drawing = 1
this.$root.$emit('draw1d')
},
getRule(id) {
const rules = this.$store.state.rules1d
return rules[id]
}, },
reset() { reset() {
this.$store.dispatch("reset"); this.$root.$emit('reset')
}, },
}, }
};
</script>
<style>
.menu-row a {
color: white;
font-weight: bold;
text-decoration: none;
font-size: small;
} }
</style> </script>

View File

@ -2,94 +2,53 @@
<MenuRow row-title="General Options"> <MenuRow row-title="General Options">
<form> <form>
<div class="form-field"> <div class="form-field">
<label>Canvas Resolution</label> <label for="live">Canvas Resolution</label>
</div> </div>
<div class="form-field"> <div class="form-field">
<label>Width</label>
<input <input
id="canvasWidth" id="canvasWidth"
name="canvasWidth" name="canvasWidth"
type="number" type="number"
step="10"
:value="canvasWidth" :value="canvasWidth"
@input="updateCanvasWidth" @input="updateCanvasWidth"
/> >
</div> </div>
<div class="form-field"> <div class="form-field">
<label>Height</label>
<input <input
id="canvasHeight" id="canvasHeight"
name="canvasHeight" name="canvasHeight"
type="number" type="number"
step="10"
:value="canvasHeight" :value="canvasHeight"
@input="updateCanvasHeight" @input="updateCanvasHeight"
/> >
</div> </div>
<div class="form-field"> </form>
<label>Refresh Rate (ms)</label>
</div>
<div class="form-field">
<input
id="refreshRate"
name="refreshRate"
type="number"
min="100"
step="100"
:value="refreshRate"
@input="updateRefreshRate"
/>
</div>
<div class="form-field">
<label
>Invert Drawing Direction
<input
type="checkbox"
:checked="drawingDirection === 'x'"
:value="drawingDirection"
@input="updateDrawingDirection"
/>
</label>
</div>
</form>
</MenuRow> </MenuRow>
</template> </template>
<script> <script>
import MenuRow from "./MenuRow.vue"; import MenuRow from './MenuRow.vue'
import { mapGetters } from "vuex"; import {mapGetters} from 'vuex'
export default { export default {
name: "MenuGeneralOptions", name: 'MenuGeneralOptions',
components: { components: {
MenuRow, MenuRow
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
canvasWidth: "getCanvasWidth", canvasWidth: 'getCanvasWidth',
canvasHeight: "getCanvasHeight", canvasHeight: 'getCanvasHeight'
refreshRate: "getRefreshRate", })
drawingDirection: "getDrawingDirection",
}),
}, },
methods: { methods: {
updateCanvasHeight: function (event) { updateCanvasHeight: function(event) {
const elem = event.target; const elem = event.target
this.$store.commit("setCanvasHeight", elem.value); this.$store.commit('setCanvasHeight', elem.value)
}, },
updateCanvasWidth: function (event) { updateCanvasWidth: function(event) {
const elem = event.target; const elem = event.target
this.$store.commit("setCanvasWidth", elem.value); this.$store.commit('setCanvasWidth', elem.value)
}, }
updateRefreshRate: function (event) { }
const elem = event.target; }
this.$store.commit("setRefreshRate", elem.value);
},
updateDrawingDirection: function (event) {
const elem = event.target;
const value = elem.checked ? "x" : "y";
this.$store.commit("setDrawingDirection", value);
console.log(this.drawingDirection);
},
},
};
</script> </script>

View File

@ -1,75 +1,63 @@
<template> <template>
<div class="menu-row"> <div class="menu-row">
<h2 :id="rowTitle" @click="updateActiveMenu"> <h2
@click="isHidden = !isHidden"
>
{{ rowTitle }} {{ rowTitle }}
</h2> </h2>
<div v-if="activeMenu === rowTitle" class="menu-row-content"> <div
v-if="!isHidden"
class="menu-row-content"
>
<slot /> <slot />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from "vuex";
export default { export default {
name: "MenuRow", name: 'MenuRow',
props: { props: {
rowTitle: { rowTitle: {
type: String, type: String,
default: "", default : ''
}, }
}, },
computed: { data() {
...mapGetters({ return {
activeMenu: "getActiveMenu", isHidden: true
}), }
}, }
methods: { }
updateActiveMenu(event) {
const elem = event.target;
const value = elem.id;
if (value == this.activeMenu) this.$store.commit("setActiveMenu", "");
else this.$store.commit("setActiveMenu", value);
},
},
};
</script> </script>
<style> <style>
.menu-row h2 { .menu-row h2 {
font-size: medium; font-size: medium;
padding: 10px; padding: 10px;
cursor: pointer; cursor: pointer;
border: 2px solid darkgrey; border: 2px solid darkgrey;
margin: 0 0 10px 0; margin: 0 0 10px 0;
} }
select {
margin-top: 10px;
padding: 5px;
}
input[type="button"] { input[type="button"] {
min-width: 60px; min-width: 60px;
padding: 5px; padding: 5px;
font-weight: bold; font-weight: bold;
margin-right: 10px; margin-right: 10px;
} }
.form-field { .form-field {
display: flex; display: flex;
margin: 10px; margin: 10px;
justify-content: space-between;
} }
.menu-row { .menu-row {
flex: 1; flex: 1;
} }
label, label, .form-field label {
.form-field label { margin-right: 10px;
margin-right: 10px; font-weight: bold;
font-weight: bold;
} }
</style> </style>

View File

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

View File

@ -1,7 +1,7 @@
// handles negative index and index bigger than its array length // handles negative index and index bigger than its array length
function guard(index, array) { function guard(index, array) {
if (index > array.length - 1) return 0; if (index > (array.length - 1)) return 0;
if (index < 0) return array.length - 1; if (index < 0) return (array.length - 1);
return index; return index;
} }
@ -12,39 +12,25 @@ function evolve1d(state, rules) {
return state[safeIndex]; return state[safeIndex];
} }
const newState = state.map((_, x) => { const newState = state.map((_, x) => {
const cells = [getCell(x - 1), getCell(x), getCell(x + 1)]; const cells = [
return rules[cells.join("")]; getCell(x - 1),
getCell(x),
getCell(x + 1)];
return rules[cells.join('')];
}); });
return newState.map(Number); return newState.map(Number);
} }
// create a 2D board from a 1D CA initial state // create a 2D board from a 1D CA initial state
// function createBoard(state, rules, height) {
// function createBoardAcc(s, h, acc) {
// if (h === 0) return acc;
// const newState = evolve1d(s, rules);
// const newAcc = acc.concat([s]);
// return createBoardAcc(newState, h - 1, newAcc);
// }
// return createBoardAcc(state, height, []);
// }
// performance "choke point" in full imperative
function createBoard(state, rules, height) { function createBoard(state, rules, height) {
var board = []; function createBoardAcc(s, h, acc) {
let prevState = []; if (h === 0) return acc;
for (let i = 0; i < height; i++) { const newState = evolve1d(s, rules);
let nextState = []; const newAcc = acc.concat([s]);
if (i == 0) { return createBoardAcc(newState, h - 1, newAcc);
nextState = evolve1d(state, rules);
} else {
nextState = evolve1d(prevState, rules);
}
board = board.concat([nextState]);
prevState = nextState;
} }
return board; return createBoardAcc(state, height, []);
} }
// Find the neighbor of a given cell in a 2D CA board // Find the neighbor of a given cell in a 2D CA board
@ -61,14 +47,10 @@ function getCellNeighbors(board, cellCoordinates) {
// the current cell is not included in the result // the current cell is not included in the result
return [ return [
getCell(x - 1, y - 1), getCell(x - 1, y - 1), getCell(x, y - 1),
getCell(x, y - 1), getCell(x + 1, y - 1), getCell(x - 1, y),
getCell(x + 1, y - 1), getCell(x + 1, y), getCell(x - 1, y + 1),
getCell(x - 1, y), getCell(x, y + 1), getCell(x + 1, y - 1),
getCell(x + 1, y),
getCell(x - 1, y + 1),
getCell(x, y + 1),
getCell(x + 1, y - 1),
]; ];
} }
@ -91,62 +73,50 @@ function conwayRules(cell, neighbors) {
} }
// get the next evolution of a 2D CA initial state // get the next evolution of a 2D CA initial state
// Rules : Moore neighborhood
function evolve2d(board, rulesFn) { function evolve2d(board, rulesFn) {
return board.map((row, x) => return board.map((row, x) => row.map((cell, y) => {
row.map((cell, y) => { const neighbors = getCellNeighbors(board, [x, y]);
const neighbors = getCellNeighbors(board, [x, y]); const sum = getNeighborsSum(neighbors);
const sum = getNeighborsSum(neighbors); return rulesFn(cell, sum);
return rulesFn(cell, sum); }));
})
);
} }
function getDrawingValues(state, acc, cell) { function getDrawingValues(state, acc, cell) {
const d = cell.dimension; const d = cell.dimension;
return Object.keys(state).map((key) => { return Object.keys(state).map(
const fillStyle = (() => { (key) => {
if (state[key] === "1") return cell.liveColor; const fillStyle = (() => {
return cell.deadColor; if (state[key] === '1') return cell.liveColor;
})(); return cell.deadColor;
})();
return { return {
move: [key * d, acc * d], move: [key * d, acc * d],
fill: [key * d, acc * d, d, d], fill: [key * d, acc * d, d, d],
fillStyle, fillStyle,
}; };
}); },
} );
// Populates the first state with a single living cell in the center
function create1dStateOneCell(width) {
return [...Array(width)].map((cell, index) => {
if (index === width / 2 || index === width + 1 / 2) return 1;
return 0;
});
} }
// Populates the first state of a 1D CA with cells returned // Populates the first state of a 1D CA with cells returned
// by initFn // by initFn
function create1dState(width, initFn, args) { function initialState1d(width, initFn, args) {
return [...Array(width)].map(() => initFn(...args)); return [...Array(width)].map(
() => initFn(...args),
);
} }
// Populates the first state of a 2D CA with cells returned // Populates the first state of a 2D CA with cells returned
// by initFn // by initFn
function create2dState(width, height, initFn, args) { function initialState2d(width, height, initFn, args) {
return [...Array(height)].map(() => return [...Array(height)].map(
[...Array(width)].map(() => initFn(...args)) () => [...Array(width)].map(
() => initFn(...args),
),
); );
} }
export { export {
getDrawingValues, getDrawingValues, initialState1d, initialState2d, evolve1d, evolve2d, conwayRules, createBoard,
create1dState,
create2dState,
createBoard,
create1dStateOneCell,
conwayRules,
evolve1d,
evolve2d,
}; };

146
src/modules/main.js Normal file
View File

@ -0,0 +1,146 @@
import { getRandomInt, sleep } from './common.js';
import {
evolve2d, initialState1d, initialState2d, conwayRules, createBoard,
} 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 cellSize = document.querySelector('#cellSize');
const startBtn = document.querySelector('#start');
const startBtn2d = document.querySelector('#start2d');
const canvasRefreshBtn = document.querySelector('#canvasRefresh');
const resetBtn = document.querySelectorAll('.reset');
const stopBtn = document.querySelectorAll('.stop');
// const loop = document.querySelector('#loop');
const menuRow = document.querySelectorAll('.menu-row');
const menuRowContent = document.querySelectorAll('.menu-row-content');
function drawCanvas(board, props) {
board.map((row, y) => {
const d = props.size;
return row.map(
(cell, x) => {
ctx.fillStyle = (
() => {
if (cell === 1) return props.liveColor;
return props.deadColor;
})();
ctx.fillRect(x * d, y * d, d, d);
return cell;
},
);
});
}
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 getCellProperties() {
return {
size: cellSize.value,
liveColor: live.value,
deadColor: dead.value,
};
}
function reset() {
drawing = 0;
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function resizeCanvas() {
canvas.width = main.offsetWidth * 0.9;
canvas.height = main.offsetHeight * 0.9;
}
async function draw1d() {
const rules = getRules();
const props = getCellProperties();
const initialState = initialState1d(
Math.floor(canvas.width / props.size),
getRandomInt,
[0, 2],
);
const board = createBoard(initialState, rules, Math.floor(canvas.height / props.size));
drawCanvas(board, props);
}
async function draw2d() {
const props = getCellProperties();
const initialState = initialState2d(
Math.floor(canvas.width / props.size),
Math.floor(canvas.height / props.size),
getRandomInt,
[0, 2],
);
const board = evolve2d(initialState, conwayRules);
async function draw2dNext(b) {
if (drawing === 0) return;
const newBoard = evolve2d(b, conwayRules);
drawCanvas(b, props);
await sleep(300);
draw2dNext(newBoard);
}
return draw2dNext(board);
}
// Listeners
startBtn.addEventListener('click', async () => {
reset();
await sleep(60);
drawing = 1;
draw1d();
});
startBtn2d.addEventListener('click', async () => {
reset();
await sleep(60);
drawing = 1;
draw2d();
});
resetBtn.forEach((elem) => {
elem.addEventListener('click', async () => {
reset();
});
});
stopBtn.forEach((elem) => {
elem.addEventListener('click', async () => {
drawing = 0;
});
});
canvasRefreshBtn.addEventListener('click', () => {
const width = document.querySelector('#canvasWidth').value;
const height = document.querySelector('#canvasWidth').value;
canvas.width = width;
canvas.height = height;
});

View File

@ -1,174 +1,68 @@
/* TODO: terminology is to be changed for : import Vue from 'vue'
canvas/board : import Vuex from 'vuex'
currently, the canvas object is named board,
while the structure used to store automata current state is named "board" as well. This is confusing
drawing board could be enough to lift any ambiguity
rules: Vue.use(Vuex)
confusion bewteen ruleset and rules.
it's never clear if we refers to a rule or the whole (named) set
*/
import { createStore } from "vuex";
export const store = createStore({ export default new Vuex.Store({
strict: process.env.NODE_ENV !== "production",
state: { state: {
rules1d: { drawing: 0,
name: "rule 73", rules1d : {
rules: { "111" : 0,
111: 0, "110" : 1,
110: 1, "101" : 0,
101: 0, "100" : 0,
100: 0, "011" : 1,
"011": 1, "010" : 0,
"010": 0, "001" : 0,
"001": 0, "000" : 1
"000": 1,
},
}, },
cellProperties: { cellProperties: {
size: 3, size: 3,
liveColor: "#000000", liveColor: '#000000',
deadColor: "#F5F5F5", deadColor: '#AA78E8',
}, },
canvasWidth: 0, canvasWidth: 1280,
canvasHeight: 0, canvasHeight: 720,
boardWidth: 0,
boardHeight: 0,
refreshRate: 300,
initial1dState: "onecell",
activeMenu: "",
drawingDirection: "y",
lastBoard: {},
draw1d: false,
draw2d: false,
draw2dLast: false,
reset: false,
canDraw: true,
}, },
mutations: { mutations: {
update1dSingleRule(state, data) {
state.rules1d.name = data.name;
state.rules1d.rules[data.rule] = data.value;
},
update1dRules(state, data) { update1dRules(state, data) {
state.rules1d = data; state.rules1d[data.rule] = data.value
}, },
setCellProperties(state, data) { setCellProperties(state, data) {
state.cellProperties[data.name] = data.value; state.cellProperties[data.name] = data.value
},
setDrawingStatus(state, data) {
state.drawing = data
}, },
setCanvasWidth(state, data) { setCanvasWidth(state, data) {
state.canvasWidth = data; state.canvasWidth = data;
}, },
setCanvasHeight(state, data) { setCanvasHeight(state, data) {
state.canvasHeight = data; state.canvasHeight = data;
}, }
setRefreshRate(state, data) {
state.refreshRate = data;
},
setInitial1dState(state, data) {
state.initial1dState = data;
},
setActiveMenu(state, data) {
state.activeMenu = data;
},
setDrawingDirection(state, data) {
state.drawingDirection = data;
},
setLastBoard(state, data) {
state.lastBoard = data;
},
setCanvas(state, data) {
state.canvas = data;
},
setContext(state, data) {
state.ctx = data;
},
toggleDraw1d(state, data) {
state.draw1d = data;
},
toggleDraw2d(state, data) {
state.draw2d = data;
},
toggleDraw2dLast(state, data) {
state.draw2dLast = data;
},
toggleReset(state, data) {
state.reset = data;
},
canDraw(state, data) {
state.canDraw = data;
},
}, },
getters: { getters: {
getCellProperties(state) { getCellProperties(state) {
return state.cellProperties; return state.cellProperties
}, },
get1dRules(state) { getRuleSet1d(state) {
return state.rules1d; return state.rules1d
}, },
getRule1d(state) { getRule1d(state) {
return state.rules1d; return (rule) => state.rules1d[rule]
},
isDrawing(state) {
return state.drawing
}, },
getCanvasWidth(state) { getCanvasWidth(state) {
return state.canvasWidth; return state.canvasWidth
}, },
getCanvasHeight(state) { getCanvasHeight(state) {
return state.canvasHeight; return state.canvasHeight
}, }
getRefreshRate(state) {
return state.refreshRate;
},
getInitial1dState(state) {
return state.initial1dState;
},
getActiveMenu(state) {
return state.activeMenu;
},
getDrawingDirection(state) {
return state.drawingDirection;
},
getLastBoard(state) {
return state.lastBoard;
},
getDraw1d(state) {
return state.draw1d;
},
getDraw2d(state) {
return state.draw2d;
},
getDraw2dLast(state) {
return state.draw2dLast;
},
getReset(state) {
return state.reset;
},
getCanDraw(state) {
return state.canDraw;
},
}, },
actions: { actions: {
draw1d({ commit }) {
commit("toggleDraw1d", true);
},
draw2d({ commit }) {
commit("canDraw", true);
commit("toggleDraw2d", true);
},
draw2dLast({ commit }) {
commit("canDraw", true);
commit("toggleDraw2dLast", true);
},
reset({ dispatch, commit }) {
dispatch("stop");
commit("toggleReset", true);
},
stop({ commit }) {
commit("toggleDraw1d", false);
commit("toggleDraw2d", false);
commit("toggleDraw2dLast", false);
commit("canDraw", false);
},
}, },
modules: {}, modules: {
}); }
})

View File

@ -1,16 +0,0 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
const path = require("path");
export default defineConfig({
plugins: [vue()],
server: {
host: "127.0.0.1",
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});

View File

@ -3,11 +3,11 @@ module.exports = {
devServer: { devServer: {
overlay: { overlay: {
warnings: true, warnings: true,
errors: true, errors: true
}, },
watchOptions: { watchOptions: {
ignored: [/node_modules/, /public/, /\.#/], ignored: [/node_modules/, /public/, /\.#/],
}, }
}, }
}, }
}; }