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)