Home Reference Source Test Repository

src/js/ui/canvas.js

import MouseHandler from '../util/mouse-handler';
import ContextMenu from '../ui/context-menu';

const SCALE_MODIFIER = 0.9;

/**
 * Front end canvas frame which reacts to mouse-clicks upon it
 * @class Canvas
 */
class Canvas {

  /**
   * An instance of the UI
   * @type {UI}
   */
  ui;

  /**
   * An instance of the canvas element on the browser
   * @type {HTMLCanvasElement}
   */
  canvas;

  /**
   * An instance of a rendering context
   * @type {CanvasRenderingContext2D}
   */
  context;

  /**
   * A number value which remembers the zoom factor
   * @type {Number}
   */
  scaleValue = 1;

  /**
   * A number value which remembers the centered horizontal position on the graph
   * @type {Number}
   */
  dx = 0;

  /**
   * A number value which remembers the centered vertical position on the graph
   * @type {Number}
   */
  dy = 0;

  /**
   * An instance of the mouseHandler which interfaces the canvas with the graph
   * @type {MouseHandler}
   */
  mouseHandler;

  /**
   * An instance of the contextMenu
   * @type {ContextMenu}
   */
  contextMenu;

  /**
   * Constructor for the Canvas object
   * @param {UI} ui - refers to the master ui object
   */
  constructor(ui) {
    this.ui = ui;
    this.canvas = document.getElementById('canvas');
    this.context = this.canvas.getContext('2d');
  }

  /**
   * Initializes the mouse handler, context menu, and other listeners
   * @param {Graph} graph - refers to the master graph object
   */
  init(graph) {
    this.mouseHandler = new MouseHandler(graph);
    this.contextMenu = new ContextMenu(this.ui, this.mouseHandler);
    this.initListeners();
  }

  resetGraph(newGraph) {
    this.mouseHandler.resetGraph(newGraph);
  }

  resize(event) {
    this.context.canvas.width = window.innerWidth;
    this.context.canvas.height = window.innerHeight;
    this.update();
  }

  get scale() {
    return this.scaleValue;
  }

  set scale(scale) {
    this.scaleValue = scale;
    this.ui.statusBar.updateZoom(scale);
  }

  reset() {
    this.dx = 0;
    this.dy = 0;
    this.scale = 1;
    this.update();
    this.ui.statusBar.updateMouse(0, 0);
  }

  clear() {
    this.context.clearRect(this.dx, this.dy, this.canvas.width / this.scale, this.canvas.height / this.scale);
  }

  /**
   * Functions gets the x-position of the click event on the full canvas
   * @param {HTMLEvent} event - the click event which triggered this call
   * @return {Number} x - The x-position of the click event on the full canvas
   */
  getCanvasX(event) {
    let canvasX;
    if (event.target === this.canvas) {
      canvasX = event.offsetX;
    } else {
      canvasX = event.pageX - this.canvas.offsetLeft;
    }
    let x = canvasX / this.scale + this.dx;
    return x;
  }

  /**
   * Functions gets the y-position of the click event on the full canvas
   * @param {HTMLEvent} event - the click event which triggered this call
   * @return {Number} y - The y-position of the click event on the full canvas
   */
  getCanvasY(event) {
    let canvasY;
    if (event.target === this.canvas) {
      canvasY = event.offsetY;
    } else {
      canvasY = event.pageY - this.canvas.offsetTop;
    }
    let y = canvasY / this.scale + this.dy;
    return y;
  }

  update() {
    // reset the transformations done to the canvas
    this.context.resetTransform();

    // context.setTransform(xScale, xSkew, ySkew, yScale, dx, dy) applies
    // the translation before the scale.
    // This is not what we want, since the scale is done relative to (0,0)
    // and the position displayed at the top-left corner will no longer
    // be (dx,dy), which means the coordinates under the mouse will change.

    // scale both x- and y-axis
    this.context.scale(this.scale, this.scale);

    // context.translate(a, b) translates the canvas origin by (a,b).
    // Since (dx,dy) are the coordinates calculated for the new origin, the
    // canvas needs to be translated by (-dx,-dy).
    this.context.translate(-1 * this.dx, -1 * this.dy);
  }

  /**
   * Creates all the listeners for every event which happens upon the canvas
   * Based on the event it calls the relevant function
   */
  initListeners() {
    this.canvas.addEventListener('mousedown', (event) => {
      event.stopPropagation();
      let x = this.getCanvasX(event);
      let y = this.getCanvasY(event);

      if (event.button === 0 && !this.contextMenu.isDisplayed) {
        this.mouseHandler.downListener(event, this.ui.toolbar.currentTool, x, y);
      } else if (event.button === 2) {
        this.mouseHandler.rightDownListener(event, x, y);
      }

      if (this.contextMenu.isDisplayed) {
        this.contextMenu.toggleContextMenu();
      }
    });

    window.addEventListener('mouseup', (event) => {
      event.stopPropagation();
      let x = this.getCanvasX(event);
      let y = this.getCanvasY(event);

      if (event.button === 0) {
        this.mouseHandler.upListener(event, this.ui.toolbar.currentTool, x, y);
      } else if (event.button === 2) {
        this.mouseHandler.rightUpListener(event, x, y);
      }
    });

    window.addEventListener('mousemove', (event) => {
      event.stopPropagation();
      let x = this.getCanvasX(event);
      let y = this.getCanvasY(event);

      if (event.button === 0 && event.buttons !== 2) {
        this.mouseHandler.moveListener(event, this.ui.toolbar.currentTool, x, y);
      } else if (event.button === 2 || event.buttons === 2) {
        this.mouseHandler.rightMoveListener(event, x, y);
      }
    });

    window.addEventListener('contextmenu', (event) => {
      if (event.target === this.canvas) {
        // prevent default context menu
        event.preventDefault();
      }
      event.stopPropagation();
      let x = this.getCanvasX(event);
      let y = this.getCanvasY(event);

      this.mouseHandler.contextmenuEventListener(event, x, y, this.contextMenu);
    });

    this.canvas.addEventListener('wheel', (event) => {
      event.stopPropagation();
      // prevent page scrolling (the default scroll behavior)
      event.preventDefault();

      // store the current scale value
      let oldScale = this.scale;

      // get the amount the mousewheel was scrolled
      let delta = event.deltaY;
      if (delta > 0) {
        // scroll down
        this.scale = Math.max(this.scale * SCALE_MODIFIER, 0.1);
      } else if (delta < 0) {
        // scroll up
        this.scale = Math.min(this.scale / SCALE_MODIFIER, 10);
      }

      // get the mouse position (relative to the canvas element)
      let mouseX = event.offsetX;
      let mouseY = event.offsetY;

      // Calculate the new position of the displayed origin (top left corner).
      // (dx,dy) are the coordinates of the top-left corner.
      // (x1,y1) and (x2,y2) are the coordinates at the mouse position before
      // and after changing the scale, respectively.
      //
      // With the old scale value:
      //     dx1 = dx
      //     dy1 = dy
      //     x1 = dx + mouseX / oldScale
      //     y1 = dy + mouseY / oldScale
      //
      // With new scale:
      //     dx2 = dx + mouseX / oldScale - mouseX / scale
      //     dy2 = dy + mouseY / oldScale - mouseY / scale
      //     x2 = dx2 + mouseX / scale
      //        = dx + mouseX / oldScale - mouseX / scale + mouseX / scale
      //        = dx + mouseX / oldScale
      //        = x1
      //     y2 = dy2 + mouseY / scale
      //        = dy + mouseY / oldScale - mouseY / scale + mouseY / scale
      //        = dy + mouseY / oldScale
      //        = y1
      //
      // Note that (x1,y1) = (x2,y2) which means the coordinates under
      // the mouse stays the same.
      this.dx += mouseX / oldScale - mouseX / this.scale;
      this.dy += mouseY / oldScale - mouseY / this.scale;

      this.update();

      this.ui.statusBar.updateZoom(this.scale);
    });

    document.addEventListener('selectstart', (event) => {
      event.stopPropagation();
      event.preventDefault();
      return false;
    });

    document.addEventListener('dblclick', (event) => {
      event.stopPropagation();
      event.preventDefault();
      return false;
    });
  }
}

export { Canvas };
export default Canvas;