Dibujando con canvas - Manejo de historial
Tags: typescript, javascript, beginners, webdev
Published: 2020-04-06 17:48:13

## Manejando historia

Es momento de agregar _history management_ a [nuestra app](https://dev.to/unjavascripter/dibujando-pixeles-en-el-navegador-con-canvas-y-cosas-extra-3b43). 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](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](https://www.typescriptlang.org/docs/handbook/interfaces.html) 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:
```ts
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:

```ts
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

```ts
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.


```html
<!-- ... -->
  <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.