+
@@ -15,16 +21,28 @@
create2dState,
createBoard,
conwayRules,
+ overpopulationRules,
+ lonelinessRules,
+ threebornRules,
evolve2d,
} from "../modules/automata.js";
import { getRandomInt, sleep } from "../modules/common.js";
+ import { picToBoard, picToBlackAndWhite } from "../modules/picture.js";
export default {
name: "CanvasBoard",
data() {
return {
canvas: null,
+ workCanvas: null,
+ workCtx: null,
ctx: null,
+ available2dRules: {
+ conway: conwayRules,
+ overpopulation: overpopulationRules,
+ loneliness: lonelinessRules,
+ threeborn: threebornRules,
+ },
};
},
computed: {
@@ -38,8 +56,11 @@
getDraw1d: "draw1d",
getDraw2d: "draw2d",
getDraw2dLast: "draw2dLast",
+ getDraw2dPicture: "draw2dpicture",
boardWidth: "boardWidth",
boardHeight: "boardHeight",
+ picture: "picture",
+ selected2dRules: "selected2dRules",
}),
...mapWritableState(globalStore, {
lastBoard: "lastBoard",
@@ -62,13 +83,20 @@
getDraw2dLast(value) {
if (value == true) this.draw2dLast();
},
+ getDraw2dPicture(value) {
+ if (value == true) this.draw2dPicture();
+ },
getReset(value) {
if (value == true) this.reset();
},
},
mounted() {
- this.canvas = Object.freeze(document.getElementById("canvas-board"));
- this.ctx = this.canvas.getContext("2d");
+ this.canvas = Object.freeze(document.getElementById("board-canvas"));
+ this.workCanvas = Object.freeze(document.getElementById("work-canvas"));
+ this.ctx = this.canvas.getContext("2d", { willReadFrequently: true });
+ this.workCtx = this.workCanvas.getContext("2d", {
+ willReadFrequently: true,
+ });
this.canvasWidth = this.canvas.parentElement.clientWidth;
this.canvasHeight = this.canvas.parentElement.clientHeight;
this.setBoardWidth();
@@ -109,16 +137,20 @@
const initialState = this.compute1dInitialState();
const board = createBoard(initialState, this.ruleset.rules, this.max);
this.lastBoard = Object.freeze(board);
- this.drawCanvas(board);
+ this.drawCanvas(this.lastBoard);
this.toggleStop();
},
- // draw 2D automaton on the canvas (currently only the game of life)
+ // draw 2D automaton on the canvas in a loop
draw2d(board) {
if (!this.canDraw) return;
const draw2dNext = async (b) => {
if (!this.canDraw) return;
- const newBoard = evolve2d(b, conwayRules);
+ const newBoard = evolve2d(
+ b,
+ this.available2dRules[this.selected2dRules.id]
+ );
this.drawCanvas(b, this.cellProperties);
+ this.lastBoard = Object.freeze(newBoard);
await sleep(this.refreshRate);
draw2dNext(newBoard);
};
@@ -137,9 +169,60 @@
this.draw2d(board);
},
// draw 2d automaton from the last known generated board
- async draw2dLast() {
+ draw2dLast() {
if (this.lastBoard != undefined) this.draw2d(this.lastBoard);
},
+ // draw 2d automaton from an uploaded picture.
+ // use the picture representation as an initial state
+ draw2dPicture() {
+ // get image data by drawing it on a work canvas
+ this.workCtx.drawImage(
+ this.picture,
+ 0,
+ 0,
+ this.canvasWidth,
+ this.canvasHeight
+ );
+ const imgData = this.workCtx.getImageData(
+ 0,
+ 0,
+ this.canvasWidth,
+ this.canvasHeight
+ );
+
+ // convert the image to black and white
+ const black = picToBlackAndWhite(
+ imgData.data,
+ this.canvasWidth,
+ this.canvasHeight
+ );
+
+ // draw it back on the canvas
+ this.ctx.putImageData(black, 0, 0);
+
+ // draw the image back on the work canvas with the dimensions of the board
+ this.workCtx.drawImage(
+ this.picture,
+ 0,
+ 0,
+ this.boardWidth,
+ this.boardHeight
+ );
+
+ const resized = this.workCtx.getImageData(
+ 0,
+ 0,
+ this.boardWidth,
+ this.boardHeight
+ );
+
+ // convert the resized image into a 2D board of boolean based on pixel value
+ this.lastBoard = Object.freeze(
+ picToBoard(resized.data, this.boardWidth, this.boardHeight)
+ );
+
+ this.toggleStop();
+ },
// stop drawing routines and clear the canvas
reset() {
this.toggleStop();
diff --git a/src/components/Menu2dCA.vue b/src/components/Menu2dCA.vue
index 58a410b..e8a8afc 100644
--- a/src/components/Menu2dCA.vue
+++ b/src/components/Menu2dCA.vue
@@ -15,26 +15,81 @@
-
-
+
+
+
+
+
diff --git a/src/components/MenuRow.vue b/src/components/MenuRow.vue
index 743bfe7..83732e4 100644
--- a/src/components/MenuRow.vue
+++ b/src/components/MenuRow.vue
@@ -30,7 +30,11 @@
window.removeEventListener("click", this.onWindowClick);
},
methods: {
- ...mapActions(globalStore, ["setActiveSubMenu", "toggleMainMenu"]),
+ ...mapActions(globalStore, [
+ "setActiveSubMenu",
+ "toggleMainMenu",
+ "setMainMenu",
+ ]),
onKeyDown: function (event) {
// escape
if (event.keyCode == 27) {
diff --git a/src/modules/automata.js b/src/modules/automata.js
index 8ed51a5..ac4df56 100644
--- a/src/modules/automata.js
+++ b/src/modules/automata.js
@@ -91,6 +91,39 @@ function conwayRules(cell, neighbors) {
return cell;
}
+// variation on the game of life's rules,
+// where the "three live neighbors" rule is ignored
+function threebornRules(cell, neighbors) {
+ // loneliness rule
+ if (cell === 1 && neighbors < 2) return 0;
+ // overpopulation rule
+ if (cell === 1 && neighbors > 3) return 0;
+ // born when three live neighbors rule
+ return cell;
+}
+
+// variation on the game of life's rules,
+// where the loneliness rule is ignored
+function lonelinessRules(cell, neighbors) {
+ // overpopulation rule
+ if (cell === 1 && neighbors > 3) return 0;
+ // born when three live neighbors rule
+ if (cell === 0 && neighbors === 3) return 1;
+ // the cell remains the same if none apply
+ return cell;
+}
+
+// variation on the game of life's rules,
+// where the overpopulation rule is ignored
+function overpopulationRules(cell, neighbors) {
+ // loneliness rule
+ if (cell === 1 && neighbors < 2) return 0;
+ // born when three live neighbors rule
+ if (cell === 0 && neighbors === 3) return 1;
+ // the cell remains the same if none apply
+ return cell;
+}
+
// get the next evolution of a 2D CA initial state
// Rules : Moore neighborhood
function evolve2d(board, rulesFn) {
@@ -148,6 +181,9 @@ export {
createBoard,
create1dStateOneCell,
conwayRules,
+ overpopulationRules,
+ lonelinessRules,
+ threebornRules,
evolve1d,
evolve2d,
};
diff --git a/src/modules/picture.js b/src/modules/picture.js
new file mode 100644
index 0000000..9a91b67
--- /dev/null
+++ b/src/modules/picture.js
@@ -0,0 +1,39 @@
+// https://stackoverflow.com/questions/4492385/convert-simple-array-into-two-dimensional-array-matrix
+// convert a 1D array into a 2D matrix
+export function toMatrix(array, width) {
+ return array.reduce(
+ (rows, key, index) =>
+ (index % width == 0
+ ? rows.push([key])
+ : rows[rows.length - 1].push(key)) && rows,
+ []
+ );
+}
+
+// convert an image into a black and white image
+export function picToBlackAndWhite(pixels, width, height) {
+ return pixels.reduce((acc, pixel, index) => {
+ if (index % 4 == 0) {
+ const count = pixels[index] + pixels[index + 1] + pixels[index + 2];
+ const colour = count >= 255 ? 255 : 1;
+ acc.data[index] = colour;
+ acc.data[index + 1] = colour;
+ acc.data[index + 2] = colour;
+ acc.data[index + 3] = 255;
+ }
+ return acc;
+ }, new ImageData(width, height));
+}
+
+// convert an ImageData into a 2D array of boolean (0, 1) values
+export function picToBoard(pixels, width, height) {
+ const flat = pixels.reduce((acc, pixel, index) => {
+ if (index % 4 == 0) {
+ const count = pixels[index] + pixels[index + 1] + pixels[index + 2];
+ const value = count >= 255 ? 1 : 0;
+ acc.push(value);
+ }
+ return acc;
+ }, []);
+ return toMatrix(flat, Math.max(width, height));
+}
diff --git a/src/modules/preset.js b/src/modules/preset.js
index 0eee491..922aab0 100644
--- a/src/modules/preset.js
+++ b/src/modules/preset.js
@@ -92,4 +92,30 @@ const initialStates = [
},
];
-export { presetRuleset, initialStates };
+const preset2dRules = [
+ {
+ id: "conway",
+ name: "Conway's Game of Life",
+ description: "The most popular 2d automata",
+ },
+ {
+ id: "overpopulation",
+ name: "Overpopulation variation",
+ description:
+ "Variation on Conway's Game of Life *without* the overpopulation rule",
+ },
+ {
+ id: "loneliness",
+ name: "Loneliness variation",
+ description:
+ "Variation on Conway's Game of Life *without* the loneliness rule",
+ },
+ {
+ id: "threeborn",
+ name: "Three lives variation",
+ description:
+ "Variation on Conway's Game of Life *without* the 'three live neighbors' rule",
+ },
+];
+
+export { presetRuleset, initialStates, preset2dRules };
diff --git a/src/stores/index.js b/src/stores/index.js
index 5b40b41..cd81b56 100644
--- a/src/stores/index.js
+++ b/src/stores/index.js
@@ -16,6 +16,11 @@ export const globalStore = defineStore("globalStore", {
"000": 1,
},
},
+ selected2dRules: {
+ id: "conway",
+ name: "Conway's Game of Life",
+ description: "The most popular 2d automata",
+ },
cellProperties: {
size: 3,
liveColor: "#000000",
@@ -32,8 +37,10 @@ export const globalStore = defineStore("globalStore", {
draw1d: false,
draw2d: false,
draw2dLast: false,
+ draw2dpicture: false,
reset: false,
canDraw: true,
+ picture: null,
mainMenu: false,
activeSubMenu: "",
};
@@ -66,6 +73,11 @@ export const globalStore = defineStore("globalStore", {
this.canDraw = true;
this.draw2dLast = true;
},
+ toggle2dDrawFromPicture() {
+ this.toggleStop();
+ this.canDraw = true;
+ this.draw2dpicture = true;
+ },
toggleReset() {
this.toggleStop();
this.reset = true;
@@ -74,6 +86,7 @@ export const globalStore = defineStore("globalStore", {
this.draw1d = false;
this.draw2d = false;
this.draw2dLast = false;
+ this.draw2dpicture = false;
this.canDraw = false;
},
setActiveSubMenu(data) {