Compare commits

..

No commits in common. "1644c8c5ce0b7c2e72e891fcce62fda9f454f12d" and "1249ba95d730da203fc042a30dc6b280aaf6d63f" have entirely different histories.

8 changed files with 82 additions and 237 deletions

View File

@ -44,4 +44,3 @@ See [Configuration Reference](https://vitejs.dev/guide/).
- https://en.wikipedia.org/wiki/Hashlife - https://en.wikipedia.org/wiki/Hashlife
- https://plato.stanford.edu/entries/cellular-automata/supplement.html - https://plato.stanford.edu/entries/cellular-automata/supplement.html
- https://www.conwaylife.com/wiki/Cellular_automaton - https://www.conwaylife.com/wiki/Cellular_automaton
- https://conwaylife.com/

View File

@ -24,18 +24,15 @@
overpopulationRules, overpopulationRules,
lonelinessRules, lonelinessRules,
threebornRules, threebornRules,
highLifeRules,
servietteRules,
evolve2d, evolve2d,
} from "../modules/automata.js"; } from "../modules/automata.js";
import { getRandomInt } from "../modules/common.js"; import { getRandomInt, sleep } from "../modules/common.js";
import { boardToPic, picToBoard } from "../modules/picture.js"; import { picToBoard, picToBlackAndWhite } from "../modules/picture.js";
export default { export default {
name: "CanvasBoard", name: "CanvasBoard",
data() { data() {
return { return {
board: null,
canvas: null, canvas: null,
workCanvas: null, workCanvas: null,
workCtx: null, workCtx: null,
@ -45,14 +42,11 @@
overpopulation: overpopulationRules, overpopulation: overpopulationRules,
loneliness: lonelinessRules, loneliness: lonelinessRules,
threeborn: threebornRules, threeborn: threebornRules,
highlife: highLifeRules,
serviette: servietteRules,
}, },
}; };
}, },
computed: { computed: {
...mapState(globalStore, { ...mapState(globalStore, {
loop: "loop",
cellProperties: "cellProperties", cellProperties: "cellProperties",
ruleset: "ruleset1d", ruleset: "ruleset1d",
refreshRate: "refreshRate", refreshRate: "refreshRate",
@ -65,8 +59,8 @@
getDraw2dPicture: "draw2dpicture", getDraw2dPicture: "draw2dpicture",
boardWidth: "boardWidth", boardWidth: "boardWidth",
boardHeight: "boardHeight", boardHeight: "boardHeight",
selected2dRules: "selected2dRules",
picture: "picture", picture: "picture",
selected2dRules: "selected2dRules",
}), }),
...mapWritableState(globalStore, { ...mapWritableState(globalStore, {
lastBoard: "lastBoard", lastBoard: "lastBoard",
@ -78,9 +72,6 @@
max() { max() {
return Math.max(this.boardWidth, this.boardHeight); return Math.max(this.boardWidth, this.boardHeight);
}, },
selectedRules() {
return this.available2dRules[this.selected2dRules.id];
},
}, },
watch: { watch: {
getDraw1d(value) { getDraw1d(value) {
@ -89,8 +80,8 @@
getDraw2d(value) { getDraw2d(value) {
if (value == true) this.draw2dNew(); if (value == true) this.draw2dNew();
}, },
async getDraw2dLast(value) { getDraw2dLast(value) {
if (value == true) await this.draw2dLast(); if (value == true) this.draw2dLast();
}, },
getDraw2dPicture(value) { getDraw2dPicture(value) {
if (value == true) this.draw2dPicture(); if (value == true) this.draw2dPicture();
@ -118,24 +109,21 @@
"setBoardHeight", "setBoardHeight",
]), ]),
// draws the board on the canvas // draws the board on the canvas
async drawCanvas(board, width, height) { drawCanvas(board) {
const d = this.cellProperties.size; const props = this.cellProperties;
// bool to RGBA colors board.map((row, y) => {
const img = await boardToPic(board, width, height, this.cellProperties); const d = props.size;
// rescale and draw return row.map((cell, x) => {
this.ctx.save(); this.ctx.fillStyle = (() => {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); if (cell === 1) return props.liveColor;
this.workCtx.putImageData(img, 0, 0); return props.deadColor;
this.ctx.imageSmoothingEnabled = false; })();
this.ctx.scale(d, d); if (this.drawingDirection === "x")
this.ctx.drawImage( this.ctx.fillRect(y * d, x * d, d, d);
this.workCanvas, else this.ctx.fillRect(x * d, y * d, d, d);
0, return cell;
0, });
this.canvasWidth, });
this.canvasHeight
);
this.ctx.restore();
}, },
// create a first state, either a single living cell // create a first state, either a single living cell
// at the center or random ones // at the center or random ones
@ -144,76 +132,73 @@
return create1dStateOneCell(this.boardWidth); return create1dStateOneCell(this.boardWidth);
return create1dState(this.boardWidth, getRandomInt, [0, 2]); return create1dState(this.boardWidth, getRandomInt, [0, 2]);
}, },
// initialize board with random cells
randomInitialState() {
return create2dState(
this.boardWidth,
this.boardHeight,
getRandomInt,
[0, 2]
);
},
// draw elementary automaton on the canvas based on selected ruleset // draw elementary automaton on the canvas based on selected ruleset
draw1d() { draw1d() {
const initialState = this.compute1dInitialState(); const initialState = this.compute1dInitialState();
const board = createBoard(initialState, this.ruleset.rules, this.max); const board = createBoard(initialState, this.ruleset.rules, this.max);
this.lastBoard = Object.freeze(board); this.lastBoard = Object.freeze(board);
this.drawCanvas(this.lastBoard, this.boardWidth, this.boardHeight); this.drawCanvas(this.lastBoard);
this.toggleStop(); this.toggleStop();
}, },
// draw 2D automaton on the canvas in a loop // draw 2D automaton on the canvas in a loop
draw2d(board) { draw2d(board) {
const newBoard = evolve2d(board, this.selectedRules); if (!this.canDraw) return;
this.drawCanvas(newBoard, this.boardWidth, this.boardHeight); const draw2dNext = async (b) => {
this.lastBoard = Object.freeze(newBoard); if (!this.canDraw) return;
}, const newBoard = evolve2d(
// draw 2d automaton in a loop, starting from passed state b,
async draw2dNext(board, time) { this.available2dRules[this.selected2dRules.id]
setTimeout(() => { );
requestAnimationFrame(() => { this.drawCanvas(b, this.cellProperties);
if (!this.canDraw) return; this.lastBoard = Object.freeze(newBoard);
this.draw2d(board); await sleep(this.refreshRate);
return this.draw2dNext(this.lastBoard); draw2dNext(newBoard);
}); };
}, this.refreshRate); return draw2dNext(board);
}, },
// draw 2d automaton from a new state // draw 2d automaton from a new state
async draw2dNew() { draw2dNew() {
if (!this.canDraw) return; const initialState = create2dState(
const initialState = this.randomInitialState(); this.boardWidth,
let board = evolve2d(initialState, this.selectedRules); this.boardHeight,
if (this.loop) return this.draw2dNext(board); getRandomInt,
else this.draw2d(board); [0, 2]
this.toggleStop(); );
const board = evolve2d(initialState, conwayRules);
this.lastBoard = Object.freeze(board);
this.draw2d(board);
}, },
// draw 2d automaton from the last known generated board // draw 2d automaton from the last known generated board
async draw2dLast() { draw2dLast() {
if (!this.canDraw) return; if (this.lastBoard != undefined) this.draw2d(this.lastBoard);
if (this.loop) return this.draw2dNext(this.lastBoard);
else this.draw2d(this.lastBoard);
this.toggleStop();
}, },
// draw 2d automaton from an uploaded picture. // draw 2d automaton from an uploaded picture.
// use the picture representation as an initial state // use the picture representation as an initial state
draw2dPicture() { draw2dPicture() {
// draw image on canvas // get image data by drawing it on a work canvas
this.ctx.fillStyle = "black"; this.workCtx.drawImage(
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
this.ctx.drawImage(
this.picture, this.picture,
Math.floor((this.canvasWidth - this.picture.width) / 2),
Math.floor((this.canvasHeight - this.picture.height) / 2),
this.picture.width,
this.picture.height
);
// get image data from canvas
const imgData = this.ctx.getImageData(
0, 0,
0, 0,
this.canvasWidth, this.canvasWidth,
this.canvasHeight 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 // draw the image back on the work canvas with the dimensions of the board
this.workCtx.drawImage( this.workCtx.drawImage(
@ -224,7 +209,6 @@
this.boardHeight this.boardHeight
); );
// get the resized image data from work canvas
const resized = this.workCtx.getImageData( const resized = this.workCtx.getImageData(
0, 0,
0, 0,
@ -232,7 +216,7 @@
this.boardHeight this.boardHeight
); );
// convert the image into a 2D board of boolean based on pixel value // convert the resized image into a 2D board of boolean based on pixel value
this.lastBoard = Object.freeze( this.lastBoard = Object.freeze(
picToBoard(resized.data, this.boardWidth, this.boardHeight) picToBoard(resized.data, this.boardWidth, this.boardHeight)
); );
@ -242,8 +226,7 @@
// stop drawing routines and clear the canvas // stop drawing routines and clear the canvas
reset() { reset() {
this.toggleStop(); this.toggleStop();
this.lastBoard = {}; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.getReset = 0; this.getReset = 0;
}, },
}, },

View File

@ -67,6 +67,7 @@
preparePicture(event) { preparePicture(event) {
const files = event.target.files; const files = event.target.files;
this.picture = new Image(); this.picture = new Image();
this.picture.width = this.canvasWidth;
if (FileReader && files && files.length) { if (FileReader && files && files.length) {
const reader = new FileReader(); const reader = new FileReader();

View File

@ -1,23 +1,5 @@
<template> <template>
<div class="form-field"> <div class="form-field">
<div class="form-field">
<label>
Loop
<input
:value="loop"
type="checkbox"
:checked="loop"
@input="toggleLoop()"
/>
</label>
</div>
<input
type="button"
name="next"
class="next"
value="Next"
@click="toggleNext()"
/>
<input <input
type="button" type="button"
name="stop" name="stop"
@ -35,21 +17,13 @@
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapActions } from "pinia"; import { mapActions } from "pinia";
import { globalStore } from "../stores/index.js"; import { globalStore } from "../stores/index.js";
export default { export default {
name: "MenuReset", name: "MenuReset",
computed: {
...mapState(globalStore, ["loop"]),
},
methods: { methods: {
...mapActions(globalStore, [ ...mapActions(globalStore, ["toggleReset", "toggleStop"]),
"toggleReset",
"toggleStop",
"toggleLoop",
"toggleNext",
]),
}, },
}; };
</script> </script>

View File

@ -1,14 +1,14 @@
// handles negative index and index bigger than its array length // handles negative index and index bigger than its array length
function guard(index, arrayLength) { function guard(index, array) {
if (index > arrayLength - 1) return 0; if (index > array.length - 1) return 0;
if (index < 0) return arrayLength - 1; if (index < 0) return array.length - 1;
return index; return index;
} }
// get the next evolution of a 1D CA initial state // get the next evolution of a 1D CA initial state
function evolve1d(state, rules) { function evolve1d(state, rules) {
function getCell(index) { function getCell(index) {
const safeIndex = guard(index, state.length); const safeIndex = guard(index, state);
return state[safeIndex]; return state[safeIndex];
} }
const newState = state.map((_, x) => { const newState = state.map((_, x) => {
@ -55,8 +55,8 @@ function getCellNeighbors(board, cellCoordinates) {
// handles board edges where the cell is missing neighbors // handles board edges where the cell is missing neighbors
function getCell(xx, yy) { function getCell(xx, yy) {
const safeX = guard(xx, board.length); const safeX = guard(xx, board);
const safeY = guard(yy, rowLength.length); const safeY = guard(yy, rowLength);
return board[safeX][safeY]; return board[safeX][safeY];
} }
@ -91,30 +91,6 @@ function conwayRules(cell, neighbors) {
return cell; return cell;
} }
// Get the next evolution of a cell according to
// Conway's game of life rules
function servietteRules(cell, neighbors) {
// loneliness rule
if (cell === 0 && [2, 3, 4].find((x) => x == neighbors)) return 1;
// the cell remains the same if none apply
return 0;
}
// variation of the game of life where a
// cell comes to live if 6 neigbor cells are alive
function highLifeRules(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
if (cell === 0 && neighbors === 2) return 1;
// highlife rules
if ((cell === 0 && neighbors === 3) || neighbors === 6) return 1;
// the cell remains the same if none apply
return cell;
}
// variation on the game of life's rules, // variation on the game of life's rules,
// where the "three live neighbors" rule is ignored // where the "three live neighbors" rule is ignored
function threebornRules(cell, neighbors) { function threebornRules(cell, neighbors) {
@ -208,8 +184,6 @@ export {
overpopulationRules, overpopulationRules,
lonelinessRules, lonelinessRules,
threebornRules, threebornRules,
highLifeRules,
servietteRules,
evolve1d, evolve1d,
evolve2d, evolve2d,
}; };

View File

@ -1,13 +1,3 @@
// https://stackoverflow.com/questions/21646738/convert-hex-to-rgba
// [
function hexToRGB(hex) {
return [
parseInt(hex.slice(1, 3), 16),
parseInt(hex.slice(3, 5), 16),
parseInt(hex.slice(5, 7), 16),
];
}
// https://stackoverflow.com/questions/4492385/convert-simple-array-into-two-dimensional-array-matrix // https://stackoverflow.com/questions/4492385/convert-simple-array-into-two-dimensional-array-matrix
// convert a 1D array into a 2D matrix // convert a 1D array into a 2D matrix
export function toMatrix(array, width) { export function toMatrix(array, width) {
@ -38,32 +28,12 @@ export function picToBlackAndWhite(pixels, width, height) {
// convert an ImageData into a 2D array of boolean (0, 1) values // convert an ImageData into a 2D array of boolean (0, 1) values
export function picToBoard(pixels, width, height) { export function picToBoard(pixels, width, height) {
const flat = pixels.reduce((acc, pixel, index) => { const flat = pixels.reduce((acc, pixel, index) => {
const i = index * 4; if (index % 4 == 0) {
const count = pixels[i] + pixels[i + 1] + pixels[i + 2]; const count = pixels[index] + pixels[index + 1] + pixels[index + 2];
const value = count >= 255 ? 1 : 0; const value = count >= 255 ? 1 : 0;
acc[index] = value; acc.push(value);
}
return acc; return acc;
}, []); }, []);
return toMatrix(flat, Math.max(width, height));
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);
const colors = [hexToRGB(live), hexToRGB(dead)];
board.flat().reduce((acc, cell, index) => {
const color = colors[cell];
const i = index * 4;
acc[i] = color[0];
acc[i + 1] = color[1];
acc[i + 2] = color[2];
acc[i + 3] = 255;
return acc;
}, img.data);
return img;
} }

View File

@ -64,19 +64,6 @@ const presetRuleset = [
"000": 0, "000": 0,
}, },
}, },
{
name: "rule 30",
rules: {
100: 1,
101: 0,
110: 0,
111: 0,
"011": 1,
"010": 1,
"001": 1,
"000": 0,
},
},
{ {
name: "unknown rule", name: "unknown rule",
rules: { rules: {
@ -90,19 +77,6 @@ const presetRuleset = [
"000": 1, "000": 1,
}, },
}, },
{
name: "unknown rule 2",
rules: {
100: 1,
101: 0,
110: 1,
111: 0,
"011": 0,
"010": 0,
"001": 0,
"000": 1,
},
},
]; ];
const initialStates = [ const initialStates = [
@ -142,17 +116,6 @@ const preset2dRules = [
description: description:
"Variation on Conway's Game of Life *without* the 'three live neighbors' rule", "Variation on Conway's Game of Life *without* the 'three live neighbors' rule",
}, },
{
id: "highlife",
name: "HighLife variation",
description:
"Variation on Conway's Game of Life where a cell live if the six neighbor cells are alive",
},
{
id: "serviette",
name: "Serviette variation",
description: "bla",
},
]; ];
export { presetRuleset, initialStates, preset2dRules }; export { presetRuleset, initialStates, preset2dRules };

View File

@ -33,7 +33,7 @@ export const globalStore = defineStore("globalStore", {
refreshRate: 300, refreshRate: 300,
initial1dState: "onecell", initial1dState: "onecell",
drawingDirection: "y", drawingDirection: "y",
lastBoard: null, lastBoard: {},
draw1d: false, draw1d: false,
draw2d: false, draw2d: false,
draw2dLast: false, draw2dLast: false,
@ -43,8 +43,6 @@ export const globalStore = defineStore("globalStore", {
picture: null, picture: null,
mainMenu: false, mainMenu: false,
activeSubMenu: "", activeSubMenu: "",
loop: false,
lastAction: "drawfromlast",
}; };
}, },
actions: { actions: {
@ -63,7 +61,6 @@ export const globalStore = defineStore("globalStore", {
}, },
toggleDraw2d() { toggleDraw2d() {
this.setActiveSubMenu(""); this.setActiveSubMenu("");
this.lastAction = "draw2d";
this.setMainMenu(false); this.setMainMenu(false);
this.toggleStop(); this.toggleStop();
this.canDraw = true; this.canDraw = true;
@ -71,7 +68,6 @@ export const globalStore = defineStore("globalStore", {
}, },
toggleDraw2dLast() { toggleDraw2dLast() {
this.setActiveSubMenu(""); this.setActiveSubMenu("");
this.lastAction = "drawfromlast";
this.setMainMenu(false); this.setMainMenu(false);
this.toggleStop(); this.toggleStop();
this.canDraw = true; this.canDraw = true;
@ -93,21 +89,6 @@ export const globalStore = defineStore("globalStore", {
this.draw2dpicture = false; this.draw2dpicture = false;
this.canDraw = false; this.canDraw = false;
}, },
toggleLoop() {
this.loop = !this.loop;
},
toggleNext() {
switch (this.lastAction) {
case "drawfromlast":
this.toggleDraw2dLast();
break;
case "draw2d":
this.toggleDraw2d();
break;
default:
return;
}
},
setActiveSubMenu(data) { setActiveSubMenu(data) {
if (this.activeSubMenu == data) this.activeSubMenu = ""; if (this.activeSubMenu == data) this.activeSubMenu = "";
else this.activeSubMenu = data; else this.activeSubMenu = data;