Compare commits

..

11 Commits

11 changed files with 343 additions and 117 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1,27 +1,81 @@
<template> <template>
<div id="main"> <div id="main">
<h1 id="main-title">Cellular Automata Explorer</h1> <h1 id="main-title">
<span id="burger-toggle" @click="toggleMainMenu">{{
mainMenu == true ? "▼" : "☰"
}}</span>
Cellular Automata Explorer
</h1>
<div id="container"> <div id="container">
<MainMenu /> <MainMenu v-if="mainMenu || windowWidth >= 800" />
<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> <style scope>
:root {
--dark1: #000000;
--dark2: #333333;
--dark3: #666666;
--light1: #999999;
--light2: #cccccc;
--light3: #eeeeee;
}
#app { #app {
font-family: Avenir, Helvetica, Arial, sans-serif; font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@ -36,15 +90,20 @@
} }
body { body {
background: black; background: var(--dark1);
color: white; color: var(--light3);
font-family: Courier New; font-family: Courier New;
} }
canvas { canvas {
flex: auto; flex: auto;
background: #110812; background: rgb(0, 0, 0);
margin-right: 10px; background: linear-gradient(
90deg,
rgba(0, 0, 0, 1) 0%,
rgba(131, 131, 131, 1) 52%,
rgba(0, 0, 0, 1) 100%
);
} }
h1, h1,
@ -55,12 +114,40 @@
h1 { h1 {
margin: 10px auto; margin: 10px auto;
font-size: larger; font-size: larger;
text-align: center; text-transform: uppercase;
} }
#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,12 +1,10 @@
<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 { mapActions, mapState, mapWritableState } from "pinia";
@ -32,7 +30,7 @@
computed: { computed: {
...mapState(globalStore, { ...mapState(globalStore, {
cellProperties: "cellProperties", cellProperties: "cellProperties",
rules: "rules1d", ruleset: "ruleset1d",
refreshRate: "refreshRate", refreshRate: "refreshRate",
initial1dState: "initial1dState", initial1dState: "initial1dState",
drawingDirection: "drawingDirection", drawingDirection: "drawingDirection",
@ -40,6 +38,8 @@
getDraw1d: "draw1d", getDraw1d: "draw1d",
getDraw2d: "draw2d", getDraw2d: "draw2d",
getDraw2dLast: "draw2dLast", getDraw2dLast: "draw2dLast",
boardWidth: "boardWidth",
boardHeight: "boardHeight",
}), }),
...mapWritableState(globalStore, { ...mapWritableState(globalStore, {
lastBoard: "lastBoard", lastBoard: "lastBoard",
@ -47,12 +47,6 @@
canvasHeight: "canvasHeight", canvasHeight: "canvasHeight",
getReset: "reset", 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) {
@ -73,9 +67,15 @@
this.ctx = this.canvas.getContext("2d"); this.ctx = this.canvas.getContext("2d");
this.canvasWidth = this.canvas.parentElement.clientWidth; this.canvasWidth = this.canvas.parentElement.clientWidth;
this.canvasHeight = this.canvas.parentElement.clientHeight; this.canvasHeight = this.canvas.parentElement.clientHeight;
this.setBoardWidth();
this.setBoardHeight();
}, },
methods: { methods: {
...mapActions(globalStore, ["toggleStop"]), ...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,7 +101,7 @@
const initialState = this.compute1dInitialState(); const initialState = this.compute1dInitialState();
const board = createBoard( const board = createBoard(
initialState, initialState,
this.rules.rules, this.ruleset.rules,
this.boardWidth this.boardWidth
); );
this.lastBoard = Object.freeze(board); this.lastBoard = Object.freeze(board);
@ -142,7 +142,8 @@
}; };
</script> </script>
<style> <style>
#mainContent { #canvas-board {
min-width: 70%; flex: 1;
margin: 0 auto;
} }
</style> </style>

View File

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

View File

@ -1,30 +1,17 @@
<template> <template>
<MenuRow row-title="2D Cellular Automata"> <MenuRow row-title="2D Cellular Automaton">
<div class="form-field">
<label>Start from last result</label>
<input type="button" value="start" @click="toggleDraw2dLast()" />
</div>
<div class="form-field"> <div class="form-field">
<label>Start from empty board</label>
<input <input
type="button" type="button"
name="start2d" name="start2d"
value="start" value="start"
@click="toggleDraw2d()" @click="toggleDraw2d()"
/> />
<input </div>
type="button" <div class="form-field">
name="stop" <label>Start from last result</label>
class="stop" <input type="button" value="start" @click="toggleDraw2dLast()" />
value="stop"
@click="toggleStop()"
/>
<input
type="button"
name="reset"
class="reset"
value="reset"
@click="toggleReset()"
/>
</div> </div>
</MenuRow> </MenuRow>
</template> </template>
@ -40,12 +27,7 @@
MenuRow, MenuRow,
}, },
methods: { methods: {
...mapActions(globalStore, [ ...mapActions(globalStore, ["toggleDraw2dLast", "toggleDraw2d"]),
"toggleDraw2d",
"toggleDraw2dLast",
"toggleReset",
"toggleStop",
]),
}, },
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<MenuRow row-title="Elementary Cellular Automata"> <MenuRow row-title="Elementary Automaton">
<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="rules.name" :value="ruleset.name"
@input="updateRules" @input="updateRuleset"
> >
<option <option
v-for="(ruleset, index) in presetRules" v-for="(ruleset, index) in presetRuleset"
: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="copyRules">copy rules</a> <a style="cursor: pointer" @click="copyRuleset">copy rules</a>
</div> </div>
<div <div
v-for="(rule, name, index) in rules.rules" v-for="(rule, name, index) in ruleset.rules"
:key="'rule-' + index" :key="'rule-' + index"
class="form-field" class="form-field"
> >
@ -64,20 +64,13 @@
</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="toggleDraw1d()" />
<input
type="button"
name="reset"
class="reset"
value="reset"
@click="toggleReset"
/>
</div> </div>
</MenuRow> </MenuRow>
</template> </template>
<script> <script>
import { mapActions, mapWritableState } from "pinia"; import { mapActions, mapWritableState } from "pinia";
import { presetRules, initialStates } from "./preset.js"; import { presetRuleset, initialStates } from "../modules/preset.js";
import { globalStore } from "../stores/index.js"; import { globalStore } from "../stores/index.js";
import MenuRow from "./MenuRow.vue"; import MenuRow from "./MenuRow.vue";
export default { export default {
@ -87,31 +80,31 @@
}, },
data() { data() {
return { return {
presetRules: presetRules, presetRuleset: presetRuleset,
initialStates: initialStates, initialStates: initialStates,
}; };
}, },
computed: { computed: {
...mapWritableState(globalStore, { ...mapWritableState(globalStore, {
initialState: "initial1dState", initialState: "initial1dState",
rules: "rules1d", ruleset: "ruleset1d",
}), }),
rules1dFileName() { rules1dFileName() {
// TODO: broken // TODO: broken
return ( return (
Object.keys(this.rules) Object.keys(this.ruleset)
.map((index) => { .map((index) => {
return this.rules[index]; return this.ruleset[index];
}) })
.join("_") + ".json" .join("_") + ".json"
); );
}, },
}, },
methods: { methods: {
...mapActions(globalStore, ["toggleDraw1d", "toggleReset"]), ...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;
@ -120,15 +113,15 @@
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.rules.rules[elem.name] = value; this.ruleset.rules[elem.name] = value;
}, },
updateRules(event) { updateRuleset(event) {
const elem = event.target; const elem = event.target;
const name = elem.value; const name = elem.value;
const newRuleset = this.presetRules.find((ruleset) => { const newRuleset = this.presetRuleset.find((ruleset) => {
return ruleset.name === name; return ruleset.name === name;
}); });
this.rules.rules = newRuleset.rules; this.ruleset = newRuleset;
}, },
updateInitialState(event) { updateInitialState(event) {
const elem = event.target; const elem = event.target;

View File

@ -0,0 +1,36 @@
<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,7 +3,7 @@
<h2 :id="rowTitle" @click="updateActiveMenu"> <h2 :id="rowTitle" @click="updateActiveMenu">
{{ rowTitle }} {{ rowTitle }}
</h2> </h2>
<div v-if="activeMenu === rowTitle" class="menu-row-content"> <div class="menu-row-content">
<slot /> <slot />
</div> </div>
</div> </div>
@ -18,19 +18,6 @@
default: "", default: "",
}, },
}, },
data() {
return {
activeMenu: "",
};
},
methods: {
updateActiveMenu(event) {
const elem = event.target;
const value = elem.id;
if (value == this.activeMenu) this.activeMenu = "";
else this.activeMenu = value;
},
},
}; };
</script> </script>
@ -39,7 +26,8 @@
font-size: medium; font-size: medium;
padding: 10px; padding: 10px;
cursor: pointer; cursor: pointer;
border: 2px solid darkgrey; border-bottom: 1px solid var(--dark3);
border-top: 1px solid var(--dark3);
margin: 0 0 10px 0; margin: 0 0 10px 0;
} }
@ -63,6 +51,18 @@
.menu-row { .menu-row {
flex: 1; flex: 1;
position: relative;
}
.menu-row:hover .menu-row-content {
display: block;
width: 100%;
}
.menu-row-content {
position: absolute;
background: var(--dark1);
display: none;
} }
label, label,
@ -70,4 +70,36 @@
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,4 +1,4 @@
const presetRules = [ const presetRuleset = [
{ {
name: "rule 73", name: "rule 73",
rules: { rules: {
@ -92,4 +92,4 @@ const initialStates = [
}, },
]; ];
export { presetRules, initialStates }; export { presetRuleset, initialStates };

95
src/modules/preset.js Normal file
View File

@ -0,0 +1,95 @@
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

@ -3,7 +3,7 @@ import { defineStore } from "pinia";
export const globalStore = defineStore("globalStore", { export const globalStore = defineStore("globalStore", {
state: () => { state: () => {
return { return {
rules1d: { ruleset1d: {
name: "rule 73", name: "rule 73",
rules: { rules: {
111: 0, 111: 0,
@ -37,14 +37,24 @@ export const globalStore = defineStore("globalStore", {
}; };
}, },
actions: { actions: {
setBoardWidth() {
this.boardWidth = Math.floor(this.canvasWidth / this.cellProperties.size);
},
setBoardHeight() {
this.boardHeight = Math.floor(
this.canvasHeight / this.cellProperties.size
);
},
toggleDraw1d() { toggleDraw1d() {
this.draw1d = true; this.draw1d = true;
}, },
toggleDraw2d() { toggleDraw2d() {
this.toggleStop();
this.canDraw = true; this.canDraw = true;
this.draw2d = true; this.draw2d = true;
}, },
toggleDraw2dLast() { toggleDraw2dLast() {
this.toggleStop();
this.canDraw = true; this.canDraw = true;
this.draw2dLast = true; this.draw2dLast = true;
}, },