247 lines
7.6 KiB
Vue
247 lines
7.6 KiB
Vue
<template>
|
|
<canvas
|
|
id="board-canvas"
|
|
ref="board-canvas"
|
|
:width="canvasWidth"
|
|
:height="canvasHeight"
|
|
/>
|
|
<canvas
|
|
id="work-canvas"
|
|
ref="work-canvas"
|
|
:width="canvasWidth"
|
|
:height="canvasHeight"
|
|
/>
|
|
</template>
|
|
<script>
|
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
|
import { globalStore } from "../stores/index.js";
|
|
import {
|
|
create1dState,
|
|
create1dStateOneCell,
|
|
create2dState,
|
|
createBoard,
|
|
conwayRules,
|
|
overpopulationRules,
|
|
lonelinessRules,
|
|
threebornRules,
|
|
highLifeRules,
|
|
servietteRules,
|
|
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,
|
|
highlife: highLifeRules,
|
|
serviette: servietteRules,
|
|
},
|
|
};
|
|
},
|
|
computed: {
|
|
...mapState(globalStore, {
|
|
loop: "loop",
|
|
cellProperties: "cellProperties",
|
|
ruleset: "ruleset1d",
|
|
refreshRate: "refreshRate",
|
|
initial1dState: "initial1dState",
|
|
drawingDirection: "drawingDirection",
|
|
canDraw: "canDraw",
|
|
getDraw1d: "draw1d",
|
|
getDraw2d: "draw2d",
|
|
getDraw2dLast: "draw2dLast",
|
|
getDraw2dPicture: "draw2dpicture",
|
|
boardWidth: "boardWidth",
|
|
boardHeight: "boardHeight",
|
|
selected2dRules: "selected2dRules",
|
|
}),
|
|
...mapWritableState(globalStore, {
|
|
lastBoard: "lastBoard",
|
|
canvasWidth: "canvasWidth",
|
|
canvasHeight: "canvasHeight",
|
|
getReset: "reset",
|
|
picture: "picture",
|
|
}),
|
|
// used to determine the dimensions of the board
|
|
max() {
|
|
return Math.max(this.boardWidth, this.boardHeight);
|
|
},
|
|
selectedRules() {
|
|
return this.available2dRules[this.selected2dRules.id];
|
|
},
|
|
},
|
|
watch: {
|
|
getDraw1d(value) {
|
|
if (value == true) this.draw1d();
|
|
},
|
|
getDraw2d(value) {
|
|
if (value == true) this.draw2dNew();
|
|
},
|
|
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("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();
|
|
this.setBoardHeight();
|
|
},
|
|
methods: {
|
|
...mapActions(globalStore, [
|
|
"toggleStop",
|
|
"setBoardWidth",
|
|
"setBoardHeight",
|
|
]),
|
|
// draws the board on the canvas
|
|
drawCanvas(board, width, height) {
|
|
const d = this.cellProperties.size;
|
|
// bool to RGBA colors
|
|
const img = board.flat().reduce((acc, cell, index) => {
|
|
const color = cell === 1 ? 0 : 255;
|
|
const i = index * 4;
|
|
acc.data[i] = color;
|
|
acc.data[i + 1] = color;
|
|
acc.data[i + 2] = color;
|
|
acc.data[i + 3] = 255;
|
|
return acc;
|
|
}, new ImageData(width, height));
|
|
// rescale and draw
|
|
this.ctx.save();
|
|
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
|
|
this.workCtx.putImageData(img, 0, 0);
|
|
this.ctx.imageSmoothingEnabled = false;
|
|
if (this.getDraw2dPicture != true) this.ctx.scale(d, d);
|
|
this.ctx.drawImage(
|
|
this.workCanvas,
|
|
0,
|
|
0,
|
|
this.canvasWidth,
|
|
this.canvasHeight
|
|
);
|
|
this.ctx.restore();
|
|
},
|
|
// create a first state, either a single living cell
|
|
// at the center or random ones
|
|
compute1dInitialState() {
|
|
if (this.initial1dState === "onecell")
|
|
return create1dStateOneCell(this.boardWidth);
|
|
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
|
|
draw1d() {
|
|
const initialState = this.compute1dInitialState();
|
|
const board = createBoard(initialState, this.ruleset.rules, this.max);
|
|
this.lastBoard = Object.freeze(board);
|
|
this.drawCanvas(this.lastBoard, this.boardWidth, this.boardHeight);
|
|
this.toggleStop();
|
|
},
|
|
// draw 2D automaton on the canvas in a loop
|
|
draw2d(board) {
|
|
const newBoard = evolve2d(board, this.selectedRules);
|
|
this.drawCanvas(newBoard, this.boardWidth, this.boardHeight);
|
|
this.lastBoard = Object.freeze(newBoard);
|
|
},
|
|
// draw 2d automaton in a loop, starting from passed state
|
|
async draw2dNext(board) {
|
|
requestAnimationFrame(() => {
|
|
if (!this.canDraw) return;
|
|
const newBoard = evolve2d(board, this.selectedRules);
|
|
this.draw2d(board);
|
|
this.lastBoard = Object.freeze(newBoard);
|
|
/* await sleep(this.refreshRate) */
|
|
return this.draw2dNext(newBoard);
|
|
});
|
|
},
|
|
// draw 2d automaton from a new state
|
|
async draw2dNew() {
|
|
if (!this.canDraw) return;
|
|
const initialState = this.randomInitialState();
|
|
let board = evolve2d(initialState, conwayRules);
|
|
if (this.loop) return this.draw2dNext(board);
|
|
else this.draw2d(board);
|
|
this.toggleStop();
|
|
},
|
|
// draw 2d automaton from the last known generated board
|
|
async draw2dLast() {
|
|
if (!this.canDraw) return;
|
|
if (this.loop) return this.draw2dNext(this.lastBoard);
|
|
else this.draw2d(this.lastBoard);
|
|
this.toggleStop();
|
|
},
|
|
// 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.ctx.fillStyle = "black";
|
|
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
|
|
this.ctx.drawImage(
|
|
this.picture,
|
|
Math.floor((this.canvasWidth - this.picture.width) / 2),
|
|
Math.floor((this.canvasHeight - this.picture.height) / 2),
|
|
this.picture.width,
|
|
this.picture.height
|
|
);
|
|
|
|
const imgData = this.ctx.getImageData(
|
|
0,
|
|
0,
|
|
this.canvasWidth,
|
|
this.canvasHeight
|
|
);
|
|
|
|
// convert the image into a 2D board of boolean based on pixel value
|
|
this.lastBoard = Object.freeze(
|
|
picToBoard(imgData.data, this.canvasWidth, this.canvasHeight)
|
|
);
|
|
this.toggleStop();
|
|
},
|
|
// stop drawing routines and clear the canvas
|
|
reset() {
|
|
this.toggleStop();
|
|
this.lastBoard = {};
|
|
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
|
|
this.getReset = 0;
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
<style>
|
|
#canvas-board {
|
|
flex: 1;
|
|
margin: 0 auto;
|
|
}
|
|
</style>
|