22.1 Canvas como superficie de dibujo
El elemento HTML <canvas> contiene una cuadrícula de píxeles que JavaScript puede modificar. Su contexto 2D ofrece líneas, curvas, texto, colores y transformaciones suficientes para representar vectores y escenas físicas sencillas.
<canvas id="lienzo" width="800" height="450">
Tu navegador no admite Canvas.
</canvas>
<script>
const canvas = document.getElementById("lienzo");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#123f5c";
ctx.fillRect(0, 0, canvas.width, canvas.height);
</script>Canvas utiliza dibujo inmediato: después de trazar una figura no conserva un objeto “flecha”. Para cambiarla debemos limpiar y redibujar la escena.
22.2 Dos sistemas de coordenadas
En física solemos ubicar el origen donde resulte conveniente, con x hacia la derecha e y hacia arriba. En Canvas, el origen está inicialmente en la esquina superior izquierda, x crece hacia la derecha e y hacia abajo.
| Característica | Mundo | Canvas |
|---|---|---|
| Origen | Elegido por el modelo | Esquina superior izquierda |
| Eje x | Derecha positiva | Derecha positiva |
| Eje y | Arriba positivo | Abajo positivo |
| Unidad | m, km, N, etc. | Píxel CSS |
22.3 Transformación mundo–pantalla
Definimos un origen de pantalla (ox, oy) y una escala en píxeles por unidad. Para invertir el eje vertical usamos un signo negativo:
pantallaY = oy − mundoY · escala
function mundoAPantalla(p, camara) {
return {
x: camara.ox + p.x * camara.escala,
y: camara.oy - p.y * camara.escala
};
}
const camara = { ox: 400, oy: 225, escala: 40 };
console.log(mundoAPantalla({ x: 3, y: 2 }, camara));22.4 Transformación inversa
Para interpretar el puntero hacemos la conversión contraria. Restamos el origen, dividimos por la escala y volvemos a invertir y:
function pantallaAMundo(p, camara) {
return {
x: (p.x - camara.ox) / camara.escala,
y: (camara.oy - p.y) / camara.escala
};
}
const camara = { ox: 400, oy: 225, escala: 40 };
console.log(pantallaAMundo({ x: 520, y: 145 }, camara));Las dos funciones deberían ser inversas salvo por el pequeño error de coma flotante estudiado en el tema anterior.
22.5 Trazar segmentos
Un trazado comienza con beginPath. moveTo ubica el punto inicial, lineTo agrega un segmento y stroke lo pinta.
function segmento(ctx, a, b, color = "#ffffff", ancho = 2) {
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = color;
ctx.lineWidth = ancho;
ctx.stroke();
}beginPath evita que el nuevo estilo vuelva a pintar trazados anteriores. Guardar y restaurar el estado con ctx.save() y ctx.restore() también impide que estilos locales afecten otros dibujos.
22.6 Construir una punta de flecha
La orientación visual se obtiene con atan2(dy, dx). Desde el extremo retrocedemos dos segmentos inclinados para formar la punta.
function puntosDePunta(inicio, fin, longitud = 12, abertura = Math.PI / 6) {
const angulo = Math.atan2(fin.y - inicio.y, fin.x - inicio.x);
return [
{ x: fin.x - longitud * Math.cos(angulo - abertura),
y: fin.y - longitud * Math.sin(angulo - abertura) },
{ x: fin.x - longitud * Math.cos(angulo + abertura),
y: fin.y - longitud * Math.sin(angulo + abertura) }
];
}
console.log(puntosDePunta({x:0,y:0}, {x:40,y:0}));La punta suele medirse en píxeles para mantener un tamaño legible aunque cambie el zoom. El cuerpo de la flecha, en cambio, representa la magnitud escalada del vector.
22.7 Una función dibujarVector
La función recibe valores del mundo, los transforma y dibuja el cuerpo y la punta. Mantiene separadas matemática y presentación.
function dibujarVector(ctx, origen, vector, camara, color) {
const inicio = mundoAPantalla(origen, camara);
const extremoMundo = {
x: origen.x + vector.x,
y: origen.y + vector.y
};
const fin = mundoAPantalla(extremoMundo, camara);
const [p1, p2] = puntosDePunta(inicio, fin);
ctx.beginPath();
ctx.moveTo(inicio.x, inicio.y);
ctx.lineTo(fin.x, fin.y);
ctx.lineTo(p1.x, p1.y);
ctx.moveTo(fin.x, fin.y);
ctx.lineTo(p2.x, p2.y);
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.stroke();
}22.8 Ejes, marcas y cuadrícula
La cuadrícula comunica escala. Sus líneas deben ser visualmente secundarias y sus marcas deben calcularse a partir del rango visible, no escribirse manualmente.
function rangoVisible(ancho, alto, camara) {
return {
minX: -camara.ox / camara.escala,
maxX: (ancho - camara.ox) / camara.escala,
minY: (camara.oy - alto) / camara.escala,
maxY: camara.oy / camara.escala
};
}
console.log(rangoVisible(800, 450, {ox:400, oy:225, escala:40}));Con zoom variable conviene elegir saltos “agradables” como 1, 2 o 5 multiplicados por potencias de diez para evitar una cuadrícula demasiado densa.
22.9 Componentes y suma gráfica
Las componentes pueden dibujarse como proyecciones punteadas. Para representar a + b mediante cabeza-cola, dibujamos a desde el origen y b desde el extremo de a. La resultante une el origen con el extremo final.
const sumar = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
const origen = { x: 0, y: 0 };
const a = { x: 3, y: 1 };
const b = { x: -1, y: 4 };
const extremoA = sumar(origen, a);
const resultante = sumar(a, b);
console.log("b comienza en:", extremoA);
console.log("resultante:", resultante);22.10 Tamaño CSS y resolución interna
El tamaño visual de Canvas y la cantidad de píxeles internos son conceptos diferentes. En pantallas de alta densidad, usar solamente width: 100% puede producir líneas borrosas.
function ajustarResolucion(canvas, ctx) {
const rect = canvas.getBoundingClientRect();
const ratio = window.devicePixelRatio || 1;
canvas.width = Math.round(rect.width * ratio);
canvas.height = Math.round(rect.height * ratio);
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
return { ancho: rect.width, alto: rect.height };
}Después de cambiar canvas.width o canvas.height, el contenido y el estado del contexto se reinician. Por eso debemos restablecer la transformación y redibujar.
22.11 Diseño adaptable
ResizeObserver permite reaccionar cuando cambia el ancho real del elemento, incluso si no hubo un evento global de ventana. La cámara puede recentrar su origen sin modificar los vectores del modelo.
const observador = new ResizeObserver(() => {
const { ancho, alto } = ajustarResolucion(canvas, ctx);
camara.ox = ancho / 2;
camara.oy = alto / 2;
dibujarEscena();
});
observador.observe(canvas);22.12 Coordenadas del puntero
clientX y clientY pertenecen a la ventana. Para obtener coordenadas CSS dentro de Canvas restamos su rectángulo y luego aplicamos la transformación inversa.
canvas.addEventListener("pointermove", evento => {
const rect = canvas.getBoundingClientRect();
const pantalla = {
x: evento.clientX - rect.left,
y: evento.clientY - rect.top
};
const mundo = pantallaAMundo(pantalla, camara);
console.log(mundo);
});No multiplicamos por devicePixelRatio: el contexto ya fue ajustado para trabajar en unidades CSS mediante setTransform.
22.13 Arrastrar el extremo de un vector
Los Pointer Events unifican mouse, lápiz y tacto. Al presionar cerca del extremo capturamos el puntero; al moverlo convertimos la posición al mundo y actualizamos el vector.
canvas.addEventListener("pointerdown", evento => {
arrastrando = true;
canvas.setPointerCapture(evento.pointerId);
});
canvas.addEventListener("pointermove", evento => {
if (!arrastrando) return;
const punto = obtenerPuntoMundo(evento);
vector.x = punto.x - origen.x;
vector.y = punto.y - origen.y;
dibujarEscena();
});
canvas.addEventListener("pointerup", () => arrastrando = false);En una interfaz con varios objetos conviene realizar una prueba de proximidad y seleccionar solo el elemento situado bajo el puntero.
22.14 Zoom y desplazamiento de cámara
El zoom modifica píxeles por unidad, no las componentes físicas. Para acercar manteniendo fijo el punto bajo el cursor, calculamos su posición mundial antes y después del cambio y corregimos el origen.
function limitarZoom(escala) {
return Math.max(10, Math.min(160, escala));
}
console.log(limitarZoom(5));
console.log(limitarZoom(80));
console.log(limitarZoom(300));Desplazar la cámara cambia ox y oy. Desplazar un cuerpo cambia su posición en el modelo. Mantener esta distinción evita que la interacción altere accidentalmente la física.
22.15 Animación con requestAnimationFrame
El navegador llama a requestAnimationFrame antes de un repintado. La marca temporal permite calcular dt y hacer que el movimiento dependa del tiempo, no de la cantidad de cuadros.
let anterior;
function cuadro(ahora) {
if (anterior === undefined) anterior = ahora;
const dt = Math.min((ahora - anterior) / 1000, 0.05);
anterior = ahora;
actualizarFisica(dt);
dibujarEscena();
requestAnimationFrame(cuadro);
}
requestAnimationFrame(cuadro);Limitar un dt excepcional evita un salto enorme después de cambiar de pestaña. Una simulación exigente puede usar pasos físicos fijos y dibujar a una frecuencia independiente.
22.16 Bucle de renderizado
Una escena clara se dibuja en capas, siempre en el mismo orden:
- Limpiar o pintar el fondo.
- Dibujar cuadrícula y ejes.
- Dibujar guías y componentes.
- Dibujar vectores principales y resultantes.
- Dibujar puntos interactivos y etiquetas.
- Actualizar información accesible fuera de Canvas.
Redibujar solo cuando cambia el estado ahorra trabajo en diagramas estáticos. En una animación continua, el bucle ya produce un cuadro nuevo periódicamente.
22.17 Accesibilidad y alternativas
Los píxeles de Canvas no forman un árbol semántico. Un lector de pantalla no conoce automáticamente cada flecha. Por eso debemos proporcionar un nombre accesible y resultados textuales actualizados.
<canvas id="lienzo" role="img"
aria-label="Vector de componentes 3 y 4, módulo 5"></canvas>
<p aria-live="polite">
Vector: (3, 4). Módulo: 5. Ángulo: 53,13 grados.
</p>Los controles también deben funcionar con teclado. Los colores no deberían ser el único medio para distinguir vectores: etiquetas, trazos y descripciones aportan redundancia.
22.18 Laboratorio vectorial con Canvas
Arrastrá los extremos azul y rosa o utilizá los controles. Podés visualizar componentes, suma cabeza-cola, ajustar el zoom y animar la rotación del vector b.
—Arrastrá la punta de a o b para modificar el vector.
22.19 Rendimiento y organización
Separar actualizar(dt) de dibujar(ctx) permite probar la física sin Canvas. También conviene evitar crear miles de objetos temporales por cuadro si una medición demuestra que generan pausas.
Para escenas complejas pueden almacenarse fondos estáticos en otro Canvas, reducir texto y sombras, agrupar cambios de estilo y evitar redibujos innecesarios. Antes de optimizar debemos medir con las herramientas del navegador.
22.20 Errores frecuentes
- Usar directamente coordenadas físicas como píxeles.
- Olvidar que el eje y de Canvas crece hacia abajo.
- Modificar el vector del modelo al cambiar el zoom.
- Confundir tamaño CSS y resolución interna.
- Acumular transformaciones del contexto en cada cuadro.
- Omitir
beginPathy repintar trazados anteriores. - Usar coordenadas globales del puntero sin restar el rectángulo de Canvas.
- Mover objetos una cantidad fija por cuadro, ignorando
dt. - Depender únicamente del color o del dibujo para comunicar resultados.
22.21 Ejercicio integrador
Implementá una función que calcule el nivel de cuadrícula adecuado para cualquier zoom. El espaciado visual debe permanecer entre 40 y 100 píxeles y el salto mundial debe pertenecer a la serie 1, 2, 5, 10, 20, 50…
Ver solución y pruebas
function pasoCuadricula(escala, minimoPx = 40) {
const minimoMundo = minimoPx / escala;
const potencia = 10 ** Math.floor(Math.log10(minimoMundo));
const normalizado = minimoMundo / potencia;
const factor = normalizado <= 1 ? 1 : normalizado <= 2 ? 2 :
normalizado <= 5 ? 5 : 10;
return factor * potencia;
}
for (const escala of [10, 20, 40, 80, 160]) {
const paso = pasoCuadricula(escala);
console.log(`${escala} px/u: paso ${paso} u, ${paso * escala} px`);
}Al aumentar el zoom, el salto mundial disminuye. La distancia visual nunca baja del mínimo y adopta números fáciles de leer.
22.22 Ideas para recordar
- Canvas dibuja píxeles y no conserva objetos gráficos.
- El modelo físico y la vista usan sistemas de coordenadas distintos.
- Las transformaciones mundo–pantalla deben centralizarse.
- La punta indica sentido; la longitud escalada comunica módulo.
- El zoom modifica la cámara, no los datos físicos.
- Pointer Events permiten interacción con mouse, lápiz y tacto.
requestAnimationFramecoordina la animación con el navegador.- Una resolución ajustada a
devicePixelRatioconserva nitidez. - Canvas necesita alternativas textuales y controles accesibles.
En el próximo tema aplicaremos estas herramientas a desplazamientos, velocidades y fuerzas, conectando los vectores con situaciones físicas completas.