Compare commits

..

No commits in common. "09f900ee9586b9e423d317d3f7857ceb631700da" and "aa66c523a3b941b2837f94cdd679c79ecdebe0ba" have entirely different histories.

21 changed files with 767 additions and 926 deletions

View File

@ -1,9 +0,0 @@
repos:
- repo: https://github.com/pre-commit/mirrors-eslint
rev: "v8.28.0"
hooks:
- id: eslint
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v2.7.1"
hooks:
- id: prettier

View File

@ -1,3 +1 @@
{ {}
"vueIndentScriptAndStyle": true
}

View File

@ -2,8 +2,6 @@
Explore 1D and 2D cellular automata, with a few bells and whistles. Explore 1D and 2D cellular automata, with a few bells and whistles.
![rules73](./example.png)
## Project setup ## Project setup
``` ```
@ -13,7 +11,7 @@ npm install
### Compiles and hot-reloads for development ### Compiles and hot-reloads for development
``` ```
npm run dev npm run serve
``` ```
### Compiles and minifies for production ### Compiles and minifies for production
@ -28,15 +26,9 @@ npm run build
npm run lint npm run lint
``` ```
### Format files
```
npm run format
```
### Customize configuration ### Customize configuration
See [Configuration Reference](https://vitejs.dev/guide/). See [Configuration Reference](https://cli.vuejs.org/config/).
### References ### References

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

90
package-lock.json generated
View File

@ -10,9 +10,9 @@
"dependencies": { "dependencies": {
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue": "^3.2.0",
"install": "^0.13.0", "install": "^0.13.0",
"pinia": "^2.0.27",
"vite": "^3.2.4", "vite": "^3.2.4",
"vue": "3.2" "vue": "3.2",
"vuex": "4.1"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.28.0", "eslint": "^8.28.0",
@ -1548,56 +1548,6 @@
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
}, },
"node_modules/pinia": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.27.tgz",
"integrity": "sha512-nOnXP0OFeL8R4WjAHsterU+11vptda643gH02xKNtSCDPiRzVfRYodOLihLDoa0gL1KKuQKV+KOzEgdt3YvqEw==",
"dependencies": {
"@vue/devtools-api": "^6.4.5",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.2.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.19", "version": "8.4.19",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
@ -2019,6 +1969,17 @@
"eslint": ">=6.0.0" "eslint": ">=6.0.0"
} }
}, },
"node_modules/vuex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz",
"integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==",
"dependencies": {
"@vue/devtools-api": "^6.0.0-beta.11"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -3122,23 +3083,6 @@
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
}, },
"pinia": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.27.tgz",
"integrity": "sha512-nOnXP0OFeL8R4WjAHsterU+11vptda643gH02xKNtSCDPiRzVfRYodOLihLDoa0gL1KKuQKV+KOzEgdt3YvqEw==",
"requires": {
"@vue/devtools-api": "^6.4.5",
"vue-demi": "*"
},
"dependencies": {
"vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"requires": {}
}
}
},
"postcss": { "postcss": {
"version": "8.4.19", "version": "8.4.19",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
@ -3380,6 +3324,14 @@
"semver": "^7.3.6" "semver": "^7.3.6"
} }
}, },
"vuex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz",
"integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==",
"requires": {
"@vue/devtools-api": "^6.0.0-beta.11"
}
},
"which": { "which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -12,9 +12,9 @@
"dependencies": { "dependencies": {
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue": "^3.2.0",
"install": "^0.13.0", "install": "^0.13.0",
"pinia": "^2.0.27",
"vite": "^3.2.4", "vite": "^3.2.4",
"vue": "3.2" "vue": "3.2",
"vuex": "4.1"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.28.0", "eslint": "^8.28.0",

View File

@ -1,153 +1,66 @@
<template> <template>
<div id="main"> <div id="main">
<h1 id="main-title"> <h1 id="main-title">Cellular Automata Explorer</h1>
<span id="burger-toggle" @click="toggleMainMenu">{{
mainMenu == true ? "▼" : "☰"
}}</span>
Cellular Automata Explorer
</h1>
<div id="container"> <div id="container">
<MainMenu v-if="mainMenu || windowWidth >= 800" /> <MainMenu />
<CanvasBoard /> <CanvasBoard />
</div> </div>
<MenuReset row-title="" />
</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 CanvasBoard from "./components/CanvasBoard.vue";
import MenuReset from "./components/MenuReset.vue";
import { mapWritableState, mapActions } from "pinia";
import { globalStore } from "./stores/index.js";
export default { export default {
name: "App", name: "App",
components: { components: {
MainMenu, MainMenu,
MenuReset,
CanvasBoard, CanvasBoard,
}, },
data() { };
return {
mainMenu: false,
windowWidth: window.innerWidth,
};
},
computed: {
...mapWritableState(globalStore, {
canvasWidth: "canvasWidth",
canvasHeight: "canvasHeight",
}),
},
mounted() {
this.$nextTick(() => {
window.addEventListener("resize", this.onResize);
});
},
beforeUnmount() {
window.removeEventListener("resize", this.onResize);
},
methods: {
...mapActions(globalStore, ["setBoardWidth", "setBoardHeight"]),
toggleMainMenu() {
this.mainMenu = !this.mainMenu;
},
onResize() {
this.$nextTick(() => {
this.windowWidth = window.innerWidth;
this.canvasWidth = window.innerWidth;
this.canvasHeight = window.innerHeight;
this.setBoardWidth();
this.setBoardHeight();
});
},
},
};
</script> </script>
<style scope> <style>
:root { #app {
--dark1: #000000;
--dark2: #333333;
--dark3: #666666;
--light1: #999999;
--light2: #cccccc;
--light3: #eeeeee;
}
#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: 0; margin: 0;
padding: 0; padding: 0;
} }
body { body {
background: var(--dark1); background: black;
color: var(--light3); color: white;
font-family: Courier New; font-family: Courier New;
} }
canvas { canvas {
flex: auto; flex: auto;
background: rgb(0, 0, 0); background: #110812;
background: linear-gradient( margin-right: 10px;
90deg, }
rgba(0, 0, 0, 1) 0%,
rgba(131, 131, 131, 1) 52%,
rgba(0, 0, 0, 1) 100%
);
}
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-transform: uppercase; text-align: center;
} }
#container { #container {
display: flex; display: flex;
height: calc(100vh - 100px); height: calc(100vh - 100px);
overflow: hidden; overflow: hidden;
flex-direction: column; }
}
#burger-toggle {
display: none;
cursor: pointer;
font-size: 1.5em;
vertical-align: middle;
color: var(--light2);
}
@media screen and (max-width: 800px) {
h1 {
font-size: medium;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
}
#burger-toggle {
display: inline;
}
#main-menu {
background: var(--dark2);
margin: 0 auto;
}
}
</style> </style>

View File

@ -1,25 +1,25 @@
<template> <template>
<main id="mainContent">
<canvas <canvas
id="canvas-board" id="canvas-board"
ref="canvas-board" ref="canvas-board"
:width="canvasWidth" :width="canvasWidth"
:height="canvasHeight" :height="canvasHeight"
/> />
</main>
</template> </template>
<script> <script>
import { mapActions, mapState, mapWritableState } from "pinia"; import {
import { globalStore } from "../stores/index.js";
import {
create1dState, create1dState,
create1dStateOneCell, create1dStateOneCell,
create2dState, create2dState,
createBoard, createBoard,
conwayRules, conwayRules,
evolve2d, evolve2d,
} from "../modules/automata.js"; } from "../modules/automata.js";
import { getRandomInt, sleep } from "../modules/common.js"; import { getRandomInt, sleep } from "../modules/common.js";
import { mapGetters } from "vuex";
export default { export default {
name: "CanvasBoard", name: "CanvasBoard",
data() { data() {
return { return {
@ -28,25 +28,27 @@
}; };
}, },
computed: { computed: {
...mapState(globalStore, { ...mapGetters({
cellProperties: "cellProperties", cellProperties: "getCellProperties",
ruleset: "ruleset1d", rules: "get1dRules",
refreshRate: "refreshRate", canvasWidth: "getCanvasWidth",
initial1dState: "initial1dState", canvasHeight: "getCanvasHeight",
drawingDirection: "drawingDirection", refreshRate: "getRefreshRate",
canDraw: "canDraw", initial1dState: "getInitial1dState",
getDraw1d: "draw1d", drawingDirection: "getDrawingDirection",
getDraw2d: "draw2d", canDraw: "getCanDraw",
getDraw2dLast: "draw2dLast", lastBoard: "getLastBoard",
boardWidth: "boardWidth", getDraw1d: "getDraw1d",
boardHeight: "boardHeight", getDraw2d: "getDraw2d",
}), getDraw2dLast: "getDraw2dLast",
...mapWritableState(globalStore, { getReset: "getReset",
lastBoard: "lastBoard",
canvasWidth: "canvasWidth",
canvasHeight: "canvasHeight",
getReset: "reset",
}), }),
boardWidth: function () {
return Math.floor(this.canvasWidth / this.cellProperties.size);
},
boardHeight: function () {
return Math.floor(this.canvasHeight / this.cellProperties.size);
},
}, },
watch: { watch: {
getDraw1d(value) { getDraw1d(value) {
@ -65,17 +67,13 @@
mounted() { mounted() {
this.canvas = Object.freeze(document.getElementById("canvas-board")); this.canvas = Object.freeze(document.getElementById("canvas-board"));
this.ctx = this.canvas.getContext("2d"); this.ctx = this.canvas.getContext("2d");
this.canvasWidth = this.canvas.parentElement.clientWidth; this.$store.commit("setCanvasWidth", this.canvas.parentElement.clientWidth);
this.canvasHeight = this.canvas.parentElement.clientHeight; this.$store.commit(
this.setBoardWidth(); "setCanvasHeight",
this.setBoardHeight(); this.canvas.parentElement.clientHeight
);
}, },
methods: { methods: {
...mapActions(globalStore, [
"toggleStop",
"setBoardWidth",
"setBoardHeight",
]),
drawCanvas(board) { drawCanvas(board) {
const props = this.cellProperties; const props = this.cellProperties;
board.map((row, y) => { board.map((row, y) => {
@ -101,12 +99,12 @@
const initialState = this.compute1dInitialState(); const initialState = this.compute1dInitialState();
const board = createBoard( const board = createBoard(
initialState, initialState,
this.ruleset.rules, this.rules.rules,
this.boardWidth this.boardWidth
); );
this.lastBoard = Object.freeze(board); this.$store.commit("setLastBoard", Object.freeze(board));
this.drawCanvas(board); this.drawCanvas(board);
this.toggleStop(); this.$store.dispatch("stop");
}, },
draw2d(board) { draw2d(board) {
if (!this.canDraw) return; if (!this.canDraw) return;
@ -127,23 +125,22 @@
[0, 2] [0, 2]
); );
const board = evolve2d(initialState, conwayRules); const board = evolve2d(initialState, conwayRules);
this.lastBoard = Object.freeze(board); this.$store.commit("setLastBoard", Object.freeze(board));
this.draw2d(board); this.draw2d(board);
}, },
async draw2dLast() { async draw2dLast() {
if (this.lastBoard != undefined) this.draw2d(this.lastBoard); this.draw2d(this.lastBoard);
}, },
reset() { reset() {
this.toggleStop(); this.$store.dispatch("stop");
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.getReset = 0; this.$store.commit("toggleReset", 0);
}, },
}, },
}; };
</script> </script>
<style> <style>
#canvas-board { #mainContent {
flex: 1; min-width: 70%;
margin: 0 auto; }
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div id="main-menu"> <div id="sidebar">
<MenuGeneralOptions /> <MenuGeneralOptions />
<MenuCellProperties /> <MenuCellProperties />
<MenuElementaryCA /> <MenuElementaryCA />
@ -8,11 +8,11 @@
</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,
@ -20,33 +20,43 @@
MenuElementaryCA, MenuElementaryCA,
Menu2dCA, Menu2dCA,
}, },
}; };
</script> </script>
<style> <style>
#main-menu { #sidebar {
display: flex; width: 25%;
flex-direction: row; padding: 0 10px;
width: 100%; overflow-y: scroll;
}
flex: 1; /* Hide scrollbar for Chrome, Safari and Opera */
} #sidebar::-webkit-scrollbar {
/* Hide scrollbar for Chrome, Safari and Opera */
#main-menu::-webkit-scrollbar {
display: none; display: none;
} }
/* Hide scrollbar for IE, Edge and Firefox */ /* Hide scrollbar for IE, Edge and Firefox */
#main-menu { #sidebar {
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
}
@media screen and (max-width: 800px) {
#container {
display: flex;
flex-direction: column;
justify-content: center;
} }
@media screen and (max-width: 800px) { #mainContent {
#main-menu { flex: 1;
flex-direction: column; width: 100%;
position: absolute;
} }
#sidebar {
flex: 1;
padding: 0;
width: 100%;
} }
}
</style> </style>

View File

@ -1,33 +1,56 @@
<template> <template>
<MenuRow row-title="2D Cellular Automaton"> <MenuRow row-title="2D Cellular Automata">
<div class="form-field">
<label>Start from empty board</label>
<input
type="button"
name="start2d"
value="start"
@click="toggleDraw2d()"
/>
</div>
<div class="form-field"> <div class="form-field">
<label>Start from last result</label> <label>Start from last result</label>
<input type="button" value="start" @click="toggleDraw2dLast()" /> <input type="button" value="start" @click="draw2dLast" />
</div>
<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> </div>
</MenuRow> </MenuRow>
</template> </template>
<script> <script>
import { mapActions } from "pinia"; import MenuRow from "./MenuRow.vue";
import MenuRow from "./MenuRow.vue"; import { mapGetters } from "vuex";
import { globalStore } from "../stores/index.js";
export default { export default {
name: "Menu2dCA", name: "Menu2dCA",
components: { components: {
MenuRow, MenuRow,
}, },
methods: { computed: {
...mapActions(globalStore, ["toggleDraw2dLast", "toggleDraw2d"]), ...mapGetters({
lastBoard: "getLastBoard",
}),
}, },
}; methods: {
draw2d() {
this.$store.dispatch("draw2d");
},
draw2dLast() {
this.$store.dispatch("draw2dLast");
},
reset() {
this.$store.dispatch("reset");
},
stop() {
this.$store.dispatch("stop");
},
},
};
</script> </script>

View File

@ -34,27 +34,29 @@
</template> </template>
<script> <script>
import { mapWritableState } from "pinia"; import MenuRow from "./MenuRow.vue";
import { globalStore } from "../stores/index.js"; export default {
import MenuRow from "./MenuRow.vue";
export default {
name: "MainMenu", name: "MainMenu",
components: { components: {
MenuRow, MenuRow,
}, },
computed: { data() {
...mapWritableState(globalStore, ["cellProperties"]), return {
cellProperties: this.$store.state.cellProperties,
};
}, },
methods: { methods: {
getCellProperties(event) { getCellProperties(event) {
const elem = event.target; const elem = event.target;
const prop = this.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;
this.cellProperties[elem.name] = elem.value; const prop = { name: elem.name, value: elem.value };
//console.log(prop)
this.$store.commit("setCellProperties", prop);
}, },
}, },
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<MenuRow row-title="Elementary Automaton"> <MenuRow row-title="Elementary Cellular Automata">
<form> <form>
<div class="form-field"> <div class="form-field">
<label> <label>
@ -29,11 +29,11 @@
<br /> <br />
<select <select
name="ruleset-elementary" name="ruleset-elementary"
:value="ruleset.name" :value="rules.name"
@input="updateRuleset" @input="updateRules"
> >
<option <option
v-for="(ruleset, index) in presetRuleset" v-for="(ruleset, index) in presetRules"
:key="'ruleset-preset-elementary-' + index" :key="'ruleset-preset-elementary-' + index"
:value="ruleset.name" :value="ruleset.name"
> >
@ -43,10 +43,10 @@
</label> </label>
</div> </div>
<div class="form-field"> <div class="form-field">
<a style="cursor: pointer" @click="copyRuleset">copy rules</a> <a style="cursor: pointer" @click="copyRules">copy rules</a>
</div> </div>
<div <div
v-for="(rule, name, index) in ruleset.rules" v-for="(rule, name, index) in rules.rules"
:key="'rule-' + index" :key="'rule-' + index"
class="form-field" class="form-field"
> >
@ -63,48 +63,142 @@
</div> </div>
</form> </form>
<div class="form-field"> <div class="form-field">
<input type="button" name="start" value="start" @click="toggleDraw1d()" /> <input type="button" name="start" value="start" @click="draw1d" />
<input
type="button"
name="reset"
class="reset"
value="reset"
@click="reset"
/>
</div> </div>
</MenuRow> </MenuRow>
</template> </template>
<script> <script>
import { mapActions, mapWritableState } from "pinia"; import { mapGetters } from "vuex";
import { presetRuleset, initialStates } from "../modules/preset.js"; import MenuRow from "./MenuRow.vue";
import { globalStore } from "../stores/index.js"; export default {
import MenuRow from "./MenuRow.vue";
export default {
name: "MenuElementaryCA", name: "MenuElementaryCA",
components: { components: {
MenuRow, MenuRow,
}, },
data() { data() {
// TODO: Why not a getter in the store?
return { return {
presetRuleset: presetRuleset, presetRules: [
initialStates: initialStates, {
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: { computed: {
...mapWritableState(globalStore, { ...mapGetters({
initialState: "initial1dState", initialState: "getInitial1dState",
ruleset: "ruleset1d", rules: "get1dRules",
}), }),
rules1dFileName() { rules1dFileName() {
// TODO: broken
return ( return (
Object.keys(this.ruleset) Object.keys(this.rules)
.map((index) => { .map((index) => {
return this.ruleset[index]; return this.rules[index];
}) })
.join("_") + ".json" .join("_") + ".json"
); );
}, },
}, },
methods: { methods: {
...mapActions(globalStore, ["toggleDraw1d"]), copyRules() {
copyRuleset() { const rules = JSON.stringify(this.rules);
const newRuleset = JSON.stringify(this.ruleset); navigator.clipboard.writeText(rules);
navigator.clipboard.writeText(newRuleset);
}, },
isCurrentPreset(event) { isCurrentPreset(event) {
const elem = event.target; const elem = event.target;
@ -113,28 +207,39 @@
updateSingleRule(event) { updateSingleRule(event) {
const elem = event.target; const elem = event.target;
const value = elem.checked ? 1 : 0; const value = elem.checked ? 1 : 0;
this.ruleset.rules[elem.name] = value; const data = { rule: elem.name, value: value };
this.$store.commit("update1dSingleRule", data);
}, },
updateRuleset(event) { updateRules(event) {
// TODO : change this, awfully confusing
const elem = event.target; const elem = event.target;
const name = elem.value; const name = elem.value;
const newRuleset = this.presetRuleset.find((ruleset) => { const rules = this.presetRules.find((ruleset) => {
return ruleset.name === name; return ruleset.name === name;
}); });
this.ruleset = newRuleset; Object.keys(rules.rules).map((value) => {
const data = { name: name, rule: value, value: rules.rules[value] };
this.$store.commit("update1dSingleRule", data);
});
}, },
updateInitialState(event) { updateInitialState(event) {
const elem = event.target; const elem = event.target;
this.initialState = elem.value; this.$store.commit("setInitial1dState", elem.value);
},
draw1d() {
this.$store.dispatch("draw1d");
},
reset() {
this.$store.dispatch("reset");
}, },
}, },
}; };
</script> </script>
<style> <style>
.menu-row a { .menu-row a {
color: white; color: white;
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
font-size: small; font-size: small;
} }
</style> </style>

View File

@ -56,40 +56,40 @@
</template> </template>
<script> <script>
import { mapWritableState } from "pinia"; import MenuRow from "./MenuRow.vue";
import { globalStore } from "../stores/index.js"; import { mapGetters } from "vuex";
import MenuRow from "./MenuRow.vue"; export default {
export default {
name: "MenuGeneralOptions", name: "MenuGeneralOptions",
components: { components: {
MenuRow, MenuRow,
}, },
computed: { computed: {
...mapWritableState(globalStore, [ ...mapGetters({
"canvasWidth", canvasWidth: "getCanvasWidth",
"canvasHeight", canvasHeight: "getCanvasHeight",
"refreshRate", refreshRate: "getRefreshRate",
"drawingDirection", drawingDirection: "getDrawingDirection",
]), }),
}, },
methods: { methods: {
updateCanvasHeight: function (event) { updateCanvasHeight: function (event) {
const elem = event.target; const elem = event.target;
this.canvasHeight = elem.value; this.$store.commit("setCanvasHeight", elem.value);
}, },
updateCanvasWidth: function (event) { updateCanvasWidth: function (event) {
const elem = event.target; const elem = event.target;
this.canvasWidth = elem.value; this.$store.commit("setCanvasWidth", elem.value);
}, },
updateRefreshRate: function (event) { updateRefreshRate: function (event) {
const elem = event.target; const elem = event.target;
this.refreshRate = elem.value; this.$store.commit("setRefreshRate", elem.value);
}, },
updateDrawingDirection: function (event) { updateDrawingDirection: function (event) {
const elem = event.target; const elem = event.target;
const value = elem.checked ? "x" : "y"; const value = elem.checked ? "x" : "y";
this.drawingDirection = value; this.$store.commit("setDrawingDirection", value);
console.log(this.drawingDirection);
}, },
}, },
}; };
</script> </script>

View File

@ -1,36 +0,0 @@
<template>
<div class="form-field">
<input
type="button"
name="stop"
class="stop"
value="stop"
@click="toggleStop()"
/>
<input
type="button"
name="reset"
class="reset"
value="reset"
@click="toggleReset()"
/>
</div>
</template>
<script>
import { mapActions } from "pinia";
import { globalStore } from "../stores/index.js";
export default {
name: "MenuReset",
methods: {
...mapActions(globalStore, ["toggleReset", "toggleStop"]),
},
};
</script>
<style scoped>
.form-field {
display: flex;
margin: 5px;
justify-content: flex-end;
}
</style>

View File

@ -3,14 +3,16 @@
<h2 :id="rowTitle" @click="updateActiveMenu"> <h2 :id="rowTitle" @click="updateActiveMenu">
{{ rowTitle }} {{ rowTitle }}
</h2> </h2>
<div class="menu-row-content"> <div v-if="activeMenu === rowTitle" class="menu-row-content">
<slot /> <slot />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { import { mapGetters } from "vuex";
export default {
name: "MenuRow", name: "MenuRow",
props: { props: {
rowTitle: { rowTitle: {
@ -18,88 +20,56 @@
default: "", default: "",
}, },
}, },
}; computed: {
...mapGetters({
activeMenu: "getActiveMenu",
}),
},
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-bottom: 1px solid var(--dark3); border: 2px solid darkgrey;
border-top: 1px solid var(--dark3);
margin: 0 0 10px 0; margin: 0 0 10px 0;
} }
select { select {
margin-top: 10px; margin-top: 10px;
padding: 5px; 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; justify-content: space-between;
} }
.menu-row { .menu-row {
flex: 1; flex: 1;
position: relative; }
}
.menu-row:hover .menu-row-content { label,
display: block; .form-field label {
width: 100%;
}
.menu-row-content {
position: absolute;
background: var(--dark1);
display: none;
}
label,
.form-field label {
margin-right: 10px; margin-right: 10px;
font-weight: bold; font-weight: bold;
} }
@media screen and (max-width: 800px) {
.menu-row {
margin: 0 auto;
width: 100%;
}
.menu-row h2,
.form-field {
margin: 0;
}
.menu-row h2 {
border-bottom: 1px solid var(--dark3);
border-top: none;
}
.form-field {
padding: 10px;
}
.menu-row:active .menu-row-content {
display: flex;
flex-direction: column;
height: 100%;
}
.menu-row-content {
position: relative;
width: 100%;
}
}
</style> </style>

View File

@ -1,95 +0,0 @@
const presetRuleset = [
{
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,
},
},
];
const 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",
},
];
export { presetRuleset, initialStates };

View File

@ -1,10 +1,9 @@
import { createApp } from "vue"; import { createApp } from "vue";
import App from "./App.vue"; import App from "./App.vue";
import { createPinia } from "pinia"; import { store } from "./store";
const app = createApp(App); const app = createApp(App);
const pinia = createPinia();
app.use(pinia); app.use(store);
app.mount("#app"); app.mount("#app");

View File

@ -1,95 +0,0 @@
const presetRuleset = [
{
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,
},
},
];
const 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",
},
];
export { presetRuleset, initialStates };

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

@ -0,0 +1,174 @@
/* TODO: terminology is to be changed for :
canvas/board :
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:
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({
strict: process.env.NODE_ENV !== "production",
state: {
rules1d: {
name: "rule 73",
rules: {
111: 0,
110: 1,
101: 0,
100: 0,
"011": 1,
"010": 0,
"001": 0,
"000": 1,
},
},
cellProperties: {
size: 3,
liveColor: "#000000",
deadColor: "#F5F5F5",
},
canvasWidth: 0,
canvasHeight: 0,
boardWidth: 0,
boardHeight: 0,
refreshRate: 300,
initial1dState: "onecell",
activeMenu: "",
drawingDirection: "y",
lastBoard: {},
draw1d: false,
draw2d: false,
draw2dLast: false,
reset: false,
canDraw: true,
},
mutations: {
update1dSingleRule(state, data) {
state.rules1d.name = data.name;
state.rules1d.rules[data.rule] = data.value;
},
update1dRules(state, data) {
state.rules1d = data;
},
setCellProperties(state, data) {
state.cellProperties[data.name] = data.value;
},
setCanvasWidth(state, data) {
state.canvasWidth = data;
},
setCanvasHeight(state, 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: {
getCellProperties(state) {
return state.cellProperties;
},
get1dRules(state) {
return state.rules1d;
},
getRule1d(state) {
return state.rules1d;
},
getCanvasWidth(state) {
return state.canvasWidth;
},
getCanvasHeight(state) {
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: {
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: {},
});

View File

@ -1,72 +0,0 @@
import { defineStore } from "pinia";
export const globalStore = defineStore("globalStore", {
state: () => {
return {
ruleset1d: {
name: "rule 73",
rules: {
111: 0,
110: 1,
101: 0,
100: 0,
"011": 1,
"010": 0,
"001": 0,
"000": 1,
},
},
cellProperties: {
size: 3,
liveColor: "#000000",
deadColor: "#F5F5F5",
},
canvasWidth: 0,
canvasHeight: 0,
boardWidth: 0,
boardHeight: 0,
refreshRate: 300,
initial1dState: "onecell",
drawingDirection: "y",
lastBoard: {},
draw1d: false,
draw2d: false,
draw2dLast: false,
reset: false,
canDraw: true,
};
},
actions: {
setBoardWidth() {
this.boardWidth = Math.floor(this.canvasWidth / this.cellProperties.size);
},
setBoardHeight() {
this.boardHeight = Math.floor(
this.canvasHeight / this.cellProperties.size
);
},
toggleDraw1d() {
this.draw1d = true;
},
toggleDraw2d() {
this.toggleStop();
this.canDraw = true;
this.draw2d = true;
},
toggleDraw2dLast() {
this.toggleStop();
this.canDraw = true;
this.draw2dLast = true;
},
toggleReset() {
this.toggleStop();
this.reset = true;
},
toggleStop() {
this.draw1d = false;
this.draw2d = false;
this.draw2dLast = false;
this.canDraw = false;
},
},
});

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/, /\.#/],
},
},
},
};