Files
explorata/src/components/CanvasBoard.vue
2022-12-21 23:45:22 +01:00

234 lines
6.2 KiB
Vue

<script setup>
import { onMounted, watch } from "vue";
import { globalStore } from "../stores/index.js";
import {
create1dState,
create1dStateOneCell,
create2dState,
createBoard,
conwayRules,
overpopulationRules,
lonelinessRules,
threebornRules,
highLifeRules,
servietteRules,
evolve2d,
} from "../modules/automata.js";
import { getRandomInt } from "../modules/common.js";
import { boardToPic, picToBoard } from "../modules/picture.js";
const store = globalStore();
// TODO: Do we really need to declare a work canvas in this scope?
// Do we really need to declare a canvas here at all?
let canvas = null;
let workCanvas = null;
let workCtx = null;
let ctx = null;
const available2dRules = {
conway: conwayRules,
overpopulation: overpopulationRules,
loneliness: lonelinessRules,
threeborn: threebornRules,
highlife: highLifeRules,
serviette: servietteRules,
};
// used to determine the dimensions of the board
const max = () => {
return Math.max(store.boardWidth, store.boardHeight);
};
const selectedRules = () => {
return available2dRules[store.selected2dRules.id];
};
watch(
() => store.draw1d,
(value) => {
if (value == true) draw1d();
}
);
watch(
() => store.draw2d,
(value) => {
if (value == true) draw2dNew();
}
);
watch(
() => store.draw2dLast,
async (value) => {
if (value == true) await draw2dLast();
}
);
watch(
() => store.draw2dpicture,
(value) => {
if (value == true) draw2dPicture();
}
);
watch(
() => store.reset,
(value) => {
if (value == true) reset();
}
);
onMounted(() => {
canvas = Object.freeze(document.getElementById("board-canvas"));
workCanvas = Object.freeze(document.getElementById("work-canvas"));
ctx = canvas.getContext("2d", { willReadFrequently: true });
workCtx = workCanvas.getContext("2d", {
willReadFrequently: true,
});
store.canvasWidth = canvas.parentElement.clientWidth;
store.canvasHeight = canvas.parentElement.clientHeight;
store.setBoardWidth();
store.setBoardHeight();
});
// draws the board on the canvas
const drawCanvas = async (board, width, height) => {
const d = store.cellProperties.size;
// bool to RGBA colors
const img = await boardToPic(board, width, height, store.cellProperties);
// rescale and draw
ctx.save();
ctx.clearRect(0, 0, store.canvasWidth, store.canvasHeight);
workCtx.putImageData(img, 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.scale(d, d);
ctx.drawImage(workCanvas, 0, 0, store.canvasWidth, store.canvasHeight);
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 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.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);
};
// draw 2d automaton in a loop, starting from passed state
const draw2dNext = async (board) => {
setTimeout(() => {
requestAnimationFrame(() => {
if (!store.canDraw) return;
draw2d(board);
return draw2dNext(store.lastBoard);
});
}, store.refreshRate);
};
// 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);
store.toggleStop();
};
// draw 2d automaton from the last known generated board
const draw2dLast = async () => {
if (!store.canDraw) return;
if (store.loop) return store.draw2dNext(store.lastBoard);
else draw2d(store.lastBoard);
store.toggleStop();
};
// draw 2d automaton from an uploaded picture.
// use the picture representation as an initial state
const draw2dPicture = () => {
// draw image on canvas
ctx.fillStyle = "black";
ctx.fillRect(0, 0, store.canvasWidth, store.canvasHeight);
ctx.drawImage(
store.picture,
Math.floor((store.canvasWidth - store.picture.width) / 2),
Math.floor((store.canvasHeight - store.picture.height) / 2),
store.picture.width,
store.picture.height
);
// draw the image back on the work canvas with the dimensions of the board
workCtx.drawImage(store.picture, 0, 0, store.boardWidth, store.boardHeight);
// get the resized image data from work canvas
const resized = workCtx.getImageData(
0,
0,
store.boardWidth,
store.boardHeight
);
// 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.toggleStop();
};
// stop drawing routines and clear the canvas
const reset = () => {
store.toggleStop();
store.lastBoard = null;
ctx.clearRect(0, 0, store.canvasWidth, store.canvasHeight);
store.reset = false;
};
</script>
<template>
<canvas
id="board-canvas"
ref="board-canvas"
:width="store.canvasWidth"
:height="store.canvasHeight"
/>
<canvas
id="work-canvas"
ref="work-canvas"
:width="store.canvasWidth"
:height="store.canvasHeight"
/>
</template>
<style>
#canvas-board {
flex: 1;
margin: 0 auto;
}
</style>