DEV Community

Diego Coy
Diego Coy

Posted on

Dibujando con canvas - Manejo de historial

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

¿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();
  }
}
Enter fullscreen mode Exit fullscreen mode

¿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>
<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

¡Y listo!

Y listo.

Top comments (0)