Sistema de coordenadas en un plano 2d infinito con JavaScript

Implementación de un sistema de coordenadas con las propiedades de zoom y pan

Sistema de coordenadas 2d

Introducción

Ya sea para encontrar la ubicación aproximada en un mapa geográfico o las coordenadas de unos píxeles en una imagen, necesitamos un sistema de referencia. Rene Descartes inventó el plano cartesiano, que en 2 dimensiones se representa mediante dos líneas perpendiculares entre sí cuyo eje vertical y horizontal interseccionan en el punto 0,0.

Para crear este sistema de coordenadas utilizando JavaScript y la API Canvas de HTML, podríamos empezar dibujando los ejes: escribiríamos un bucle para trazar lineas horizontales y otro para las verticales usando la instrucción ctx.lineTo(x, y). Y luego podríamos añadir la funcionalidad de movernos por el plano mediante eventos del ratón, guardando la posición inicial al primer clic y después al soltar haríamos la traslación de las lineas del plano restando la diferencia de la posición actual con la guardada anteriormente para simular el efecto de "arrastrar" los objetos.

Pero si aun quisieramos complicarlo más, bastaría con añadirle la opción de poder aumentar o disminuir la escala del plano de forma infinita.

En este artículo haremos todo lo anterior. Explicaremos qué herramientas matemáticas se necesitan y proporcionaremos el código para generar un sistema de coordenadas 2d usando JavaScript.

Demostración

Si quieres ver directamete el código en acción, aquí puedes ver el resultado final del sistema de coordenadas en 2d:

Implemetación en JavaScript

Lo primero es crear el elemento canvas y añadirlo dentro del elemento <body>

const canvas = document.createElement("canvas");
canvas.width = 1000;
canvas.height = 600;
document.body.appendChild(canvas);

Para iniciar la aplicación crearemos una nueva instancia de la clase CoordinateSystem pasándole el elemento canva como parámetro:

new CoordinateSystem(canvas);

Puedes ver el código completo en el ejemplo anterior. Aquí sólo comentaremos los puntos más importantes.

Para dibujar la cuadrícula de lineas, lo haremos en el método drawGridLines(). Dibujaremos unas lineas más gruesas y otras más finas:

Dibujar lineas de cuadrícula

drawGridlines() {
  this.context.beginPath();
  const topLeft = this.fromCanvasCoords(0, 0);
  const bottomRight = this.fromCanvasCoords(
    this.canvas.width,
    this.canvas.height
  );

  for (const i in this.settings.gridLines) {
    this.context.strokeStyle = this.settings.gridLines[i].color;
    this.context.lineWidth = this.settings.gridLines[i].width;

    const spacing = this.settings.gridLines[i].spacing;

    let startX = spacing * Math.ceil(topLeft[0] / spacing);
    let endX = spacing * Math.floor(bottomRight[0] / spacing);

    this.context.beginPath();

    for (let x = startX; x <= endX; x += spacing) {
      const coords = this.canvasCoords(x, 0);

      this.context.moveTo(coords[0], 0);
      this.context.lineTo(coords[0], this.canvas.height);
    }

    let endY = spacing * Math.ceil(topLeft[1] / spacing);
    let startY = spacing * Math.floor(bottomRight[1] / spacing);

    for (let y = startY; y <= endY; y += spacing) {
      const coords = this.canvasCoords(0, y);
      this.context.moveTo(0, coords[1]);
      this.context.lineTo(this.canvas.width, coords[1]);
    }

    this.context.stroke();
  }
}

Dibujar los ejes x e y

Para dibujar los ejes x e y, usaremos el método drawAxes. Obtendremos el origen 0,0 como punto de referencia y luego trazaremos primero la linea horizontal y luego la vertical.

drawAxes() {
  const origin = this.canvasCoords(0, 0);
  const x = origin[0];
  const y = origin[1];

  if (
    (x >= 0 && x <= this.canvas.width) ||
    (y >= 0 && y <= this.canvas.height)
  ) {
    this.context.beginPath();
    this.context.strokeStyle = this.settings.axes.color;
    this.context.lineWidth = this.settings.axes.width;
    this.context.moveTo(0, y);
    this.context.lineTo(this.canvas.width, y);
    this.context.moveTo(x, 0);
    this.context.lineTo(x, this.canvas.height);
    this.context.stroke();
  }
}

Métodos para transformar coordenadas del Canvas y viceversa

Hay dos métodos básicos que son clave para gestionar la visualización del gráfico, estos son canvasCoords que convierte cualquier posición x e y a la posición que tiene dentro del canvas y fromCanvasCoords que hace justo lo contrario, a partir de unas coordenadas del canvas nos devuelve la posición x e y reales.

canvasCoords(x, y) {
  const point = new Matrix([[x], [y]]);
  const newPoint = this.zoomMatrix.multiply(point).add(this.translation);

  const coords = [newPoint.entry(0, 0), newPoint.entry(1, 0)];

  return [
    coords[0] + 0.5 * this.canvas.width,
    -coords[1] + 0.5 * this.canvas.height,
  ];
}

fromCanvasCoords(x, y) {
  x -= 0.5 * this.canvas.width;
  y = -(y - 0.5 * this.canvas.height);

  const point = new Matrix([[x], [y]]);
  const newPoint = this.zoomMatrix
    .inverse()
    .multiply(point.subtract(this.translation));
  return [newPoint.entry(0, 0), newPoint.entry(1, 0)];
}

Como ves, ambos métodos utilizan operaciones matemáticas con matrices para aplicar traslaciones de las coordenadas. Mira detallademente el código de la función Matrix en el código de ejemplo para ver los métodos implementados para operar con matrices.

Mover y ampliar/reducir

Lo que en inglés se conoce como pan & zoom. Gestionaremos la traslación con el método translate que llamaremos desde el evento mousemove (cuando el ratón esté pulsado y en movimiento) para generar el efecto de mover el gráfico. Y algo parecido conn el método zoom.

translate(u, v) {
  const w = new Matrix([[u], [v]]);
  const vector = this.zoomMatrix.multiply(w);
  this.translation = this.translation.add(vector);
  this.draw();
}

zoom(zoomFactor, mouseX, mouseY) {
  const worldCoords = this.fromCanvasCoords(mouseX, mouseY);
  const worldCoordsMatrix = new Matrix([[worldCoords[0]], [worldCoords[1]]]);
  const newZoom = this.zoomMatrix.scale(zoomFactor + 1);

  const zoomDifference = newZoom.subtract(this.zoomMatrix);

  this.translation = this.translation.subtract(
    zoomDifference.multiply(worldCoordsMatrix)
  );
  this.zoomMatrix = newZoom;

  this.zoomLevel *= zoomFactor + 1;
  if (this.zoomLevel >= 2 || this.zoomLevel <= 0.5) {
    const factor = this.zoomLevel >= 2 ? 0.5 : 2;

    for (const i in this.settings.gridLines) {
      this.settings.gridLines[i].spacing *= factor;
    }

    this.zoomLevel *= factor;
  }

  this.draw();
}

¿Y ahora...?

Este código es sólo un ejemplo de cómo implementar un sistema de coordenadas, pero si quieres ampliarlo te propongo varias mejoras:

  • Aunque el gráfico tiene zoom hacia dentro y hacia fuera, no es realmente infinito, hay un punto en el que se bloquea, podrías añadir algunas condiciones para que esto no pase si sobrepasa algunos valores al aumentar o disminuir el zoom.
  • Por si solo el gráfico no sirve de mucho. Podrías dibujar otros objetos como líneas o formas geométricas.