diff --git a/src/components/CanvasBoard.vue b/src/components/CanvasBoard.vue index 8d1c16b..4362795 100644 --- a/src/components/CanvasBoard.vue +++ b/src/components/CanvasBoard.vue @@ -2,9 +2,6 @@ import { onMounted, watch } from "vue"; import { globalStore } from "../stores/index.js"; import { - create1dState, - create1dStateOneCell, - create2dState, createBoard, conwayRules, overpopulationRules, @@ -13,8 +10,11 @@ highLifeRules, servietteRules, evolve2d, - } from "../modules/automata.js"; - import { getRandomInt } from "../modules/common.js"; + } from "../modules/core.js"; + import { + create1dInitialState, + create2dRandomGrid, + } from "../modules/board.js"; import { boardToPic, picToBoard } from "../modules/picture.js"; const store = globalStore(); @@ -36,7 +36,7 @@ // used to determine the dimensions of the board const max = () => { - return Math.max(store.boardWidth, store.boardHeight); + return Math.max(store.board.width, store.board.height); }; const selectedRules = () => { @@ -92,10 +92,10 @@ }); // draws the board on the canvas - const drawCanvas = async (board, width, height) => { - const d = store.cellProperties.size; + const drawCanvas = async (board) => { + const d = board.cellProperties.size; // bool to RGBA colors - const img = await boardToPic(board, width, height, store.cellProperties); + const img = await boardToPic(board); // rescale and draw ctx.save(); ctx.clearRect(0, 0, store.canvasWidth, store.canvasHeight); @@ -106,39 +106,22 @@ ctx.restore(); }; - // create a first state, either a single living cell - // at the center or random ones - const compute1dInitialState = () => { - if (store.initial1dState === "onecell") - return create1dStateOneCell(store.boardWidth); - return create1dState(store.boardWidth, getRandomInt, [0, 2]); - }; - - // initialize board with random cells - const randomInitialState = () => { - return create2dState( - store.boardWidth, - store.boardHeight, - getRandomInt, - [0, 2] - ); - }; - // draw elementary automaton on the canvas based on selected ruleset const draw1d = () => { - const initialState = compute1dInitialState(); + const initialState = create1dInitialState( + store.board, + store.initial1dState + ); const board = createBoard(initialState, store.ruleset1d.rules, max()); - store.lastBoard = Object.freeze(board); - // TODO: the board clearly could be an object - drawCanvas(store.lastBoard, store.boardWidth, store.boardHeight); + store.board.grid = Object.freeze(board); + drawCanvas(store.board); store.toggleStop(); }; // draw 2D automaton on the canvas in a loop const draw2d = (board) => { - const newBoard = evolve2d(board, selectedRules()); - drawCanvas(newBoard, store.boardWidth, store.boardHeight); - store.lastBoard = Object.freeze(newBoard); + store.board.grid = Object.freeze(evolve2d(board.grid, selectedRules())); + drawCanvas(store.board); }; // draw 2d automaton in a loop, starting from passed state @@ -147,7 +130,7 @@ requestAnimationFrame(() => { if (!store.canDraw) return; draw2d(board); - return draw2dNext(store.lastBoard); + return draw2dNext(store.board); }); }, store.refreshRate); }; @@ -155,18 +138,21 @@ // draw 2d automaton from a new state const draw2dNew = async () => { if (!store.canDraw) return; - const initialState = randomInitialState(); - const board = evolve2d(initialState, selectedRules()); - if (store.loop) return draw2dNext(board); - else draw2d(board); + const initialGrid = create2dRandomGrid( + store.board.width, + store.board.height + ); + store.board.grid = Object.freeze(evolve2d(initialGrid, selectedRules())); + if (store.loop) return draw2dNext(store.board); + else draw2d(store.board); store.toggleStop(); }; // draw 2d automaton from the last known generated board const draw2dLast = async () => { if (!store.canDraw) return; - if (store.loop) return draw2dNext(store.lastBoard); - else draw2d(store.lastBoard); + if (store.loop) return draw2dNext(store.board); + else draw2d(store.board); store.toggleStop(); }; @@ -185,19 +171,25 @@ ); // draw the image back on the work canvas with the dimensions of the board - workCtx.drawImage(store.picture, 0, 0, store.boardWidth, store.boardHeight); + workCtx.drawImage( + store.picture, + 0, + 0, + store.board.width, + store.board.height + ); // get the resized image data from work canvas const resized = workCtx.getImageData( 0, 0, - store.boardWidth, - store.boardHeight + store.board.width, + store.board.height ); // convert the image into a 2D board of boolean based on pixel value - store.lastBoard = Object.freeze( - picToBoard(resized.data, store.boardWidth, store.boardHeight) + store.board.grid = Object.freeze( + picToBoard(resized.data, store.board.width, store.board.height) ); store.toggleStop(); @@ -206,7 +198,7 @@ // stop drawing routines and clear the canvas const reset = () => { store.toggleStop(); - store.lastBoard = null; + store.board.grid = null; ctx.clearRect(0, 0, store.canvasWidth, store.canvasHeight); store.reset = false; }; diff --git a/src/components/MenuCellProperties.vue b/src/components/MenuCellProperties.vue index 3f9f4a1..4a5d7d3 100644 --- a/src/components/MenuCellProperties.vue +++ b/src/components/MenuCellProperties.vue @@ -5,17 +5,14 @@ const store = globalStore(); const updateCellProperties = (event) => { - const elem = event.target; - store.cellProperties[elem.name] = elem.value; + const { name, value } = event.target; + store.setCellProperties(name, value); store.setBoardWidth(); store.setBoardHeight(); }; const switchColor = () => { - [store.cellProperties["liveColor"], store.cellProperties["deadColor"]] = [ - store.cellProperties["deadColor"], - store.cellProperties["liveColor"], - ]; + store.switchColor(); }; @@ -27,7 +24,7 @@ @@ -36,7 +33,7 @@ @@ -49,7 +46,7 @@ name="size" type="number" min="1" - :value="store.cellProperties.size" + :value="store.board.cellProperties.size" @click="updateCellProperties" /> diff --git a/src/modules/board.js b/src/modules/board.js new file mode 100644 index 0000000..6e663c0 --- /dev/null +++ b/src/modules/board.js @@ -0,0 +1,28 @@ +import { create2dState, create1dStateOneCell, create1dState } from "./core.js"; + +import { getRandomInt } from "./common.js"; + +function Board(width, height, grid = []) { + this.width = width; + this.height = height; + (this.cellProperties = { + size: 3, + liveColor: "#000000", + deadColor: "#F5F5F5", + }), + (this.grid = grid); +} + +// create a first state, either a single living cell +// at the center or random ones +const create1dInitialState = (board, type = "onecell") => { + if (type === "onecell") return create1dStateOneCell(board.width); + return create1dState(board.width, getRandomInt, [0, 2]); +}; + +// initialize 2d board with random cells +const create2dRandomGrid = (width, height) => { + return create2dState(width, height, getRandomInt, [0, 2]); +}; + +export { Board, create1dInitialState, create2dRandomGrid }; diff --git a/src/modules/picture.js b/src/modules/picture.js index 06e9ea3..c037b8f 100644 --- a/src/modules/picture.js +++ b/src/modules/picture.js @@ -44,19 +44,21 @@ export function picToBoard(pixels, width, height) { acc[index] = value; return acc; }, []); - + // TODO: The representation has to be 2D, not the data structure + // (change to flat) return toMatrix(flat, width, height); } // convert board to ImageData // TODO : different cell to color functions // (binary, intermediate states, camaieux, etc) -export function boardToPic(board, width, height, cellProperties) { - const live = cellProperties.liveColor; - const dead = cellProperties.deadColor; - const img = new ImageData(width, height); +export function boardToPic(board) { + const live = board.cellProperties.liveColor; + const dead = board.cellProperties.deadColor; + const img = new ImageData(board.width, board.height); const colors = [hexToRGB(live), hexToRGB(dead)]; - board.flat().reduce((acc, cell, index) => { + board.grid.flat().reduce((acc, cell, index) => { + // TODO : bitshift operation instead of ternary const color = cell === 1 ? colors[0] : colors[1]; const i = index * 4; acc[i] = color[0]; diff --git a/src/stores/index.js b/src/stores/index.js index 1be37a4..07bae1f 100644 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -1,4 +1,5 @@ import { defineStore } from "pinia"; +import { Board } from "../modules/board.js"; export const globalStore = defineStore("globalStore", { state: () => { @@ -21,19 +22,12 @@ export const globalStore = defineStore("globalStore", { name: "Conway's Game of Life", description: "The most popular 2d automata", }, - cellProperties: { - size: 3, - liveColor: "#000000", - deadColor: "#F5F5F5", - }, canvasWidth: 0, canvasHeight: 0, - boardWidth: 0, - boardHeight: 0, refreshRate: 300, initial1dState: "onecell", drawingDirection: "y", - lastBoard: null, + board: new Board(), draw1d: false, draw2d: false, draw2dLast: false, @@ -49,13 +43,27 @@ export const globalStore = defineStore("globalStore", { }, actions: { setBoardWidth() { - this.boardWidth = Math.floor(this.canvasWidth / this.cellProperties.size); + this.board.width = Math.floor( + this.canvasWidth / this.board.cellProperties.size + ); }, setBoardHeight() { - this.boardHeight = Math.floor( - this.canvasHeight / this.cellProperties.size + this.board.height = Math.floor( + this.canvasHeight / this.board.cellProperties.size ); }, + setCellProperties(name, value) { + this.board.cellProperties[name] = value; + }, + switchColor() { + [ + this.board.cellProperties["liveColor"], + this.board.cellProperties["deadColor"], + ] = [ + this.board.cellProperties["deadColor"], + this.board.cellProperties["liveColor"], + ]; + }, toggleDraw1d() { this.setActiveSubMenu(""); this.setMainMenu(false); diff --git a/tests/board.test.js b/tests/board.test.js new file mode 100644 index 0000000..84227c9 --- /dev/null +++ b/tests/board.test.js @@ -0,0 +1,28 @@ +import { describe, expect, test } from "vitest"; +import { + Board, + create1dInitialState, + create2dRandomGrid, +} from "src/modules/board.js"; + +describe("Board", () => { + const board = new Board(503, 301); + test("create1dInitialState onecell", () => { + const stateOne = create1dInitialState(board); + expect(stateOne.length).toBe(board.width); + expect(stateOne).toContain(1); + }); + + test("create1dInitialState random", () => { + const stateRandom = create1dInitialState(board, "random"); + expect(stateRandom.length).toBe(board.width); + expect(stateRandom).toContain(1); + }); + + test("create2dRandomGrid", () => { + const board = new Board(503, 301); + const got = create2dRandomGrid(board.width, board.height); + expect(got.length).toBe(board.height); + expect(got[0].length).toBe(board.width); + }); +});