Visualización trigonométrica

Visualización interactiva para dibujar un triángulo usando fórmulas trigonométricas con la API Canvas

Visualización trigonométrica

A primera vista, la trigonometría puede parecer un área de las matemáticas distante de la programación, pero su aplicación en el ámbito informático es realmente destacable. En esencia, la trigonometría es la rama de las matemáticas que estudia las relaciones entre los ángulos y los lados de los triángulos. En el universo de la programación, esta disciplina se convierte en un recurso básico para resolver problemas geométricos, gráficos, de movimiento, animación, física simulada y mucho más.

Normalmente cuando se explican estos conceptos en la escuela apenas hay tiempo para aplicarlos en casos más complejos, o bien faltan otros conocimientos para usar la trigonometría en casos más interesantes. Por eso me pareció divertido crear una visualización interactiva para mostrar la aplicación de un caso real utilizando Javascript para dibujar sobre el elemento Canvas de HTML.

Conceptos básicos de trigonometría

Pero antes vamos a repasar un poco las bases de la trigonometría: el seno y el coseno. En JavaScript, estas funciones son accesibles a través de sus métodos nativos:

  • Seno (sin): Calcula la relación entre el lado opuesto y la hipotenusa de un triángulo rectángulo. En JavaScript, se utiliza mediante Math.sin().

  • Coseno (cos): Calcula la relación entre el lado adyacente y la hipotenusa de un triángulo rectángulo. En JavaScript, se emplea a través de Math.cos().

Estas funciones son la clave para resolver una amplia gama de problemas con triángulos rectangulos. Pero, ¿qué sucede con la diferencia entre radianes y grados? Los radianes son una medida angular que relaciona el radio de un arco con su longitud. Mientras que los grados son la unidad de medida angular más común.

En JavaScript, podemos convertir fácilmente de radianes a grados y viceversa. Para convertir de radianes a grados, la fórmula es simple:

function radianesAGrados(radianes) {
  return radianes * (180 / Math.PI);
}

Con estas fórmulas podremos entender la mayoría de operaciones que requeriremos para crear la visualización.

Visualización interactiva

Vamos a empezar con un archivo llamado index.html. Escribiremos el elemento Canvas, sobre el que luego dibujaremos la visualización

<canvas id="canvas" width="600" height="300"></canvas>

Luego añadiremos unos tags <script></script> y dentro escribiremos todo el código JavaScript. Primero escribiremos las variables para manipular el elemento canvas y su contexto, así como algunos valores por defecto, como el ángulo inicial y los colores:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const padding = 10;
let thetaAngle = 45
let isDrawing = false;

const colors = {
  black: '#333',
  blue: '#1E90FF',
  red: '#FF6347',
};

Funciones para ayudarnos a dibujar

En esta sección, definimos funciones que permiten dibujar un círculo, una línea, texto paralelo y calcular el ángulo a partir de un clic en un canvas HTML usando el objeto MouseEvent.

const drawCircle = (ctx, x, y, r, fill, border, arc) => {
  ctx.beginPath();
  ctx.fillStyle = 'transparent'
  ctx.arc(x, y, r, 0, arc || Math.PI * 2, true);
  if (border) {
    ctx.stroke();
  }
  if (fill) {
    ctx.lineTo(x, y);
    ctx.fill();
    ctx.closePath();
  }
};

const drawLine = (ctx, p1, p2) => {
  ctx.beginPath();
  ctx.moveTo(...p1);
  ctx.lineTo(...p2);
  ctx.stroke();
};

const drawParallelText = (ctx, text, x1, y1, x2, y2) => {
  const angle = Math.atan2(y2 - y1, x2 - x1);
  const labelX = (x1 + x2) / 2;
  const labelY = (y1 + y2) / 2;

  ctx.save();
  ctx.translate(labelX, labelY);
  ctx.rotate(angle);

  ctx.fillStyle = colors.black;
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'center';
  ctx.fillText(text, 0, -10);

  ctx.restore();
};

const calculateAngleFromClick = (event) => {
  const rect = canvas.getBoundingClientRect();
  const clickX = event.clientX - rect.left - canvas.width / 2;
  const clickY = canvas.height / 2 - (event.clientY - rect.top);
  let angle = Math.atan2(clickY, clickX);
  if (angle < 0) {
    angle += Math.PI * 2;
  }
  return Math.round(angle * 180 / Math.PI);
};

Event Listeners para interacción con el canvas

Estos event listeners están pendientes de la interacción del usuario con el canvas: detectan clics del mouse, movimientos y liberación del botón del mouse.

canvas.addEventListener('mousedown', (event) => {
  isDrawing = true;
  thetaAngle = calculateAngleFromClick(event);
  draw()
});

canvas.addEventListener('mousemove', (event) => {
  if (isDrawing) {
    thetaAngle = calculateAngleFromClick(event);
    draw()
  }
});

canvas.addEventListener('mouseup', () => {
    isDrawing = false;
});

Función de dibujo principal y su invocación inicial

La función draw() se encarga de dibujar en el canvas. Define la apariencia de un círculo, líneas, texto y otros elementos, utilizando operaciones trigonométricas como el coseno y el seno para posicionar y rotar elementos en el canvas.

Esta función realiza los siguientes pasos:

  • Limpia el canvas.
  • Dibuja un círculo, líneas de referencia y un círculo giratorio con base en un ángulo.
  • Dibuja líneas y texto indicando valores de seno y coseno.
  • Dibuja puntos para representar los valores de seno y coseno.
  • Muestra el ángulo en el canvas.
let radius = canvas.height / 2.2;
const draw = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.lineWidth = 1;
  ctx.lineCap = 'round';
  let theta = thetaAngle * Math.PI / 180;
  ctx.strokeStyle = colors.black;

  drawCircle(
    ctx,
    canvas.width / 2, canvas.height / 2,
    radius, false, true
  );

  const margin = 40;
  ctx.setLineDash([5, 5]);
  drawLine(ctx,
    [canvas.width / 2 - radius - margin, canvas.height / 2],
    [canvas.width / 2 + radius + margin, canvas.height / 2]
  );
  drawLine(ctx,
    [canvas.width / 2, canvas.height / 2 - radius - margin],
    [canvas.width / 2, canvas.height / 2 + radius + margin]
  );
  ctx.setLineDash([]);

  if (theta != 0) {
    drawCircle(
      ctx,
      canvas.width / 2, canvas.height / 2,
      30, true, true, -theta
    );
  }

  ctx.lineWidth = 3;
  drawLine(ctx,
    [canvas.width / 2, canvas.height / 2],
    [canvas.width / 2 + radius * Math.cos(theta),
    canvas.height / 2 + -radius * Math.sin(theta)]
  );

  ctx.font = '16px sans-serif';
  drawParallelText(ctx, 'radio',
    canvas.width / 2, canvas.height / 2,
    canvas.width / 2 + radius * Math.cos(theta),
    canvas.height / 2 + -radius * Math.sin(theta)
  );

  ctx.fillStyle = colors.black;
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'center';
  ctx.fillText(`𝜃 = ${thetaAngle}º`,
    canvas.width / 2 + 40 * Math.cos(theta / 2),
    canvas.height / 2 - 40 * Math.sin(theta / 2),
  );

  ctx.strokeStyle = colors.red;
  ctx.fillStyle = colors.red;
  drawLine(ctx,
    [canvas.width / 2 + radius * Math.cos(theta), canvas.height / 2],
    [canvas.width / 2 + radius * Math.cos(theta),
    canvas.height / 2 + -radius * Math.sin(theta)]
  );

  ctx.textBaseline = 'middle';
  ctx.textAlign = 'start';

  ctx.fillText('sin(𝜃)',
    canvas.width / 2 + radius * Math.cos(theta) + padding,
    canvas.height / 2 + -radius * Math.sin(theta) / 2
  );

  ctx.strokeStyle = colors.blue;
  ctx.fillStyle = colors.blue;
  drawLine(ctx,
    [canvas.width / 2, canvas.height / 2],
    [canvas.width / 2 + radius * Math.cos(theta),
    canvas.height / 2]
  );

  ctx.textBaseline = 'top';
  ctx.textAlign = 'center';
  ctx.fillText('cos(𝜃)',
    canvas.width / 2 + radius * Math.cos(theta) / 2,
    canvas.height / 2 + padding
  );

  ctx.fillStyle = colors.black;
  ctx.strokeStyle = colors.black;

  drawCircle(
    ctx,
    canvas.width / 2, canvas.height / 2,
    2, true, true
  );

  drawCircle(
    ctx,
    canvas.width / 2 + radius * Math.cos(theta),
    canvas.height / 2 + -radius * Math.sin(theta),
    2, true, true
  );

  drawCircle(
    ctx,
    canvas.width / 2 + radius * Math.cos(theta),
    canvas.height / 2,
    2, true, true
  );
};

draw();

Visualización completa

Finalmente, aquí tienes la visualización interactiva completa: