Manejando historia
Es momento de agregar history management a nuestra app. Vamos a mantener un registro de los pixeles dibujados en el canvas.
Objetivo
- Agregar las acciones que se realicen sobre el canvas a una pila, el historial
- Remover elementos del historial para deshacer
- Crear un historial temporal para poder rehacer
- Asociar las acciones deshacer y rehacer a botones
Demo
Aquí: https://codepen.io/UnJavaScripter/pen/QWbeEpw
El código
PixelProp
Como vamos a necesitar referenciar cada pixel que se ha pintado, vamos a usar las interfaces de TypeScript para crear un tipo específico a nuestro caso particular.
Creamos un archivo llamado types.ts
dentro de /src
y dentro ponemos las propiedades que tiene todo pixel:
interface PixelProp {
x: number;
y: number;
color: string;
empty?: boolean;
}
HistoryHandler
Vamos al código para manejar el historial. Creamos un nuevo archivo llamado history-handler.ts
dentro de /src
con:
class HistoryHandler {
private _history: PixelProp[] = [];
private historyRedo: PixelProp[] = [];
get history() {
return this._history;
}
push(pixel: PixelProp) {
if(this.historyRedo.length) {
this.historyRedo = [];
}
this._history = this._history.concat(pixel);
}
clear() {
this._history = [];
}
undo() {
const historySize = this._history.length;
if(historySize) {
const lastHistoryElem = <PixelProp>this._history[historySize - 1];
this.historyRedo = [...this.historyRedo, lastHistoryElem];
this._history.pop();
}
}
redo() {
const historyRedoSize = this.historyRedo.length;
if(historyRedoSize) {
const lastHistoryRedoElem = <PixelProp>this.historyRedo[historyRedoSize - 1];
this._history = [...this._history, lastHistoryRedoElem];
this.historyRedo.pop();
}
}
}
export const historyHandler = new HistoryHandler();
¿historyRedo?
Cuando deshacemos una acción, queremos mantenerla en un lugar de forma temporal por si cambiamos de opinión y decidimos rehacerla. Por eso tenemos dos arrays.
Conectando
import { historyHandler } from './history-handler.js';
class PixelPaint {
// ...
undoBtn: HTMLButtonElement;
redoBtn: HTMLButtonElement;
pixelSize: number;
lastDrawnPixel: PixelProp | undefined;
constructor() {
this.undoBtn = <HTMLButtonElement>document.getElementById('undo-btn');
this.redoBtn = <HTMLButtonElement>document.getElementById('redo-btn');
// ...
this.undoBtn.addEventListener('pointerdown', (event: PointerEvent) => {
this.undo();
});
this.redoBtn.addEventListener('pointerdown', (event: PointerEvent) => {
this.redo();
});
// ...
}
// ...
private drawPixel(x: number, y: number, color = "#CACA00", skipHistory?: boolean) {
if(this.lastDrawnPixel?.x === x && this.lastDrawnPixel?.y === y) {
return;
}
const pixelToDraw = {x,y,color};
if(!skipHistory) {
historyHandler.push(pixelToDraw);
}
this.lastDrawnPixel = pixelToDraw;
this.ctx.fillStyle = color;
this.ctx.fillRect(x * this.pixelSize, y * this.pixelSize, this.pixelSize, this.pixelSize);
}
private reDrawPixelsFromHistory() {
this.ctx.clearRect(0, 0, this.canvasElem.width, this.canvasElem.height);
this.drawGrid();
historyHandler.history.forEach((pixel: PixelProp) => {
if(pixel.empty) {
return;
}
this.lastDrawnPixel = undefined;
this.drawPixel(pixel.x, pixel.y, pixel.color, true);
});
}
undo() {
historyHandler.undo();
this.reDrawPixelsFromHistory();
}
redo() {
historyHandler.redo();
this.reDrawPixelsFromHistory();
}
}
¿Qué está pasando ahí?
Guardamos una referencia al último pixel pintado con lastDrawnPixel
, esta nos servirá para evitar registros no intencionales sobre la misma posición. También nos servirá más adelante para cuando creemos la funcionalidad de borrar.
Agregamos el parámetro skipHistory
a drawPixel
para saber si queremos guardar esa acción en el historial o no.
reDrawPixelsFromHistory
borra el canvas por completo y a continuación dibuja la grilla y en seguida cada elemento que se encuentre en la historia (que no haya sido marcado como empty).
Al final tenemos los handlers par manejar el historial. Estos son invocados a través de los botones que definimos antes. Cada uno invoca a la función de historyHandler
correspondiente y procede a "re pintar" todo.
Finalmente tenemos el index.html
que incluye los botones de deshacer y rehacer junto con algunos estilos.
<!-- ... -->
<style>
body {
margin: 0;
background-color: #464655;
}
canvas {
touch-action: none;
}
.controls-container {
display: flex;
justify-content: center;
}
button {
margin: 0.5rem 0.3rem;
padding: 0.5rem 0.7rem;
background-color: #262635;
color: #eef;
border: none;
font-size: 1rem;
}
</style>
<!-- ... -->
<body>
<canvas id="canvas"></canvas>
<div class="controls-container">
<button id="undo-btn">Undo</button>
<button id="redo-btn">Redo</button>
</div>
<script src="dist/app.js" type="module"></script>
</body>
<!-- ... -->
¡Y listo!
Y listo.
Top comments (0)