24.1 Objetivo del proyecto
El laboratorio permitirá editar dos vectores, visualizar una operación, medir módulos y ángulos, cambiar la escala y guardar el estado como JSON. El resultado debe funcionar con controles, mouse, lápiz, tacto y teclado.
El desafío no consiste solamente en dibujar flechas. Debemos coordinar cuatro ideas:
- Un modelo matemático independiente de la pantalla.
- Una biblioteca vectorial con contratos claros.
- Una vista que transforme coordenadas y comunique resultados.
- Interacciones que actualicen una única fuente de verdad.
24.2 Requisitos funcionales
| Área | Requisito | Comprobación |
|---|---|---|
| Edición | Modificar a y b mediante controles o arrastre | Ambas entradas actualizan el mismo estado |
| Operaciones | Suma, resta, proyección y reflexión | Resultado numérico y flecha coinciden |
| Medición | Módulo, producto escalar y ángulo | Casos nulos se informan |
| Vista | Cuadrícula, zoom, componentes y etiquetas | El modelo no cambia al hacer zoom |
| Persistencia | Exportar e importar JSON | El estado restaurado reproduce la escena |
| Accesibilidad | Controles etiquetados y resumen textual | La información no depende solo de Canvas |
24.3 Arquitectura por responsabilidades
La entrada no dibuja directamente. Primero modifica el estado; luego una función de actualización calcula los valores derivados y finalmente renderiza todas las salidas. Este flujo unidireccional evita que Canvas y los controles discrepen.
24.4 Diseñar el estado
Guardamos solo información necesaria para reconstruir la interfaz. La resultante, el módulo y el ángulo son datos derivados y no necesitan persistirse.
const estado = {
a: { x: 3, y: 2 },
b: { x: -1, y: 4 },
operacion: "sumar",
camara: { escala: 40 },
mostrarComponentes: true
};
const resultado = {
x: estado.a.x + estado.b.x,
y: estado.a.y + estado.b.y
};
console.log("Estado:", estado);
console.log("Dato derivado:", resultado);Al no almacenar valores derivados evitamos tener que sincronizar varias copias de la misma información.
24.5 Núcleo matemático
El proyecto reutiliza funciones puras. Todas aceptan objetos {x, y} y crean resultados nuevos.
const sumar = (a,b) => ({x:a.x+b.x,y:a.y+b.y});
const restar = (a,b) => ({x:a.x-b.x,y:a.y-b.y});
const escalar = (v,k) => ({x:v.x*k,y:v.y*k});
const punto = (a,b) => a.x*b.x+a.y*b.y;
const modulo = v => Math.hypot(v.x,v.y);
const a={x:3,y:2}, b={x:-1,y:4};
console.log("a+b:", sumar(a,b));
console.log("a-b:", restar(a,b));
console.log("a·b:", punto(a,b));
console.log("|a|:", modulo(a));24.6 Operaciones con casos degenerados
La proyección y la reflexión requieren una dirección válida. La función devuelve null cuando b es casi nulo y la interfaz traduce ese valor a un mensaje comprensible.
const punto=(a,b)=>a.x*b.x+a.y*b.y;
const escalar=(v,k)=>({x:v.x*k,y:v.y*k});
function proyectar(v, sobre, tolerancia=1e-10) {
const m2=punto(sobre,sobre);
return m2<=tolerancia*tolerancia
? null : escalar(sobre,punto(v,sobre)/m2);
}
console.log("Sobre x:", proyectar({x:3,y:4},{x:1,y:0}));
console.log("Sobre nulo:", proyectar({x:3,y:4},{x:0,y:0}));24.7 Un despachador de operaciones
La interfaz selecciona una operación por nombre. Centralizamos la decisión para que la vista no repita fórmulas.
const sumar=(a,b)=>({x:a.x+b.x,y:a.y+b.y});
const restar=(a,b)=>({x:a.x-b.x,y:a.y-b.y});
function calcular(nombre,a,b) {
const operaciones={ sumar, restar };
const operacion=operaciones[nombre];
if (!operacion) throw new Error(`Operación desconocida: ${nombre}`);
return operacion(a,b);
}
console.log(calcular("sumar",{x:2,y:1},{x:-1,y:3}));En el proyecto completo, proyección y reflexión se agregan al mismo punto de extensión.
24.8 Coordenadas y cámara
La cámara transforma puntos del mundo en píxeles. Su origen se calcula a partir del tamaño actual de Canvas; el zoom solo cambia la escala visual.
const mundoAPantalla=(p,camara)=>({
x:camara.ox+p.x*camara.escala,
y:camara.oy-p.y*camara.escala
});
const pantallaAMundo=(p,camara)=>({
x:(p.x-camara.ox)/camara.escala,
y:(camara.oy-p.y)/camara.escala
});
const c={ox:400,oy:250,escala:50}, p={x:2,y:-3};
const pantalla=mundoAPantalla(p,c);
console.log("Coordenadas de pantalla:",pantalla);
console.log("Regreso al mundo:",pantallaAMundo(pantalla,c));24.9 Probar una transformación de ida y vuelta
Una prueba de ida y vuelta verifica que transformar un punto a la pantalla y recuperarlo produce las coordenadas originales:
const mundoAPantalla=(p,c)=>({x:c.ox+p.x*c.escala,y:c.oy-p.y*c.escala});
const pantallaAMundo=(p,c)=>({x:(p.x-c.ox)/c.escala,y:(c.oy-p.y)/c.escala});
const c={ox:400,oy:250,escala:50}, original={x:2,y:-3};
const pantalla=mundoAPantalla(original,c);
const recuperado=pantallaAMundo(pantalla,c);
console.log("Pantalla:",pantalla);
console.log("Recuperado:",recuperado);Con valores decimales complejos, la comparación final debe realizarse con la tolerancia estudiada en el tema 21.
24.10 Renderizado por capas
La función principal de dibujo no realiza cálculos físicos complejos. Organiza capas y entrega a cada función los datos ya preparados.
function dibujarEscena(ctx, estado, derivados) {
dibujarFondo(ctx);
dibujarCuadricula(ctx, estado.camara);
dibujarEjes(ctx, estado.camara);
if (estado.mostrarComponentes) {
dibujarComponentes(ctx, estado.a, estado.camara);
dibujarComponentes(ctx, estado.b, estado.camara);
}
dibujarVector(ctx, estado.a, "#61d4ff", "a");
dibujarVector(ctx, estado.b, "#ff7792", "b");
dibujarVector(ctx, derivados.resultado, "#ffcf56", "r");
dibujarManipuladores(ctx, estado);
}24.11 Selección y arrastre
La selección debe medirse en píxeles para que el área sensible conserve un tamaño cómodo con cualquier zoom. Una vez elegido el extremo, el movimiento se transforma al mundo.
function masCercano(puntero, extremos, radio=18) {
let elegido=null, menor=radio;
for (const [nombre,p] of Object.entries(extremos)) {
const distancia=Math.hypot(puntero.x-p.x,puntero.y-p.y);
if (distancia<=menor) { menor=distancia; elegido=nombre; }
}
return elegido;
}
console.log(masCercano({x:102,y:98},{a:{x:100,y:100},b:{x:180,y:90}}));24.12 Sincronizar controles y modelo
Cuando un deslizador cambia, copiamos sus valores al estado. Cuando el puntero cambia el estado, actualizamos los deslizadores. En ambos casos llamamos una sola vez a actualizar().
function actualizar() {
const derivados = calcularDerivados(estado);
sincronizarControles(estado);
mostrarResultados(estado, derivados);
dibujarEscena(ctx, estado, derivados);
actualizarDescripcionAccesible(estado, derivados);
}La función no debe producir un nuevo ciclo de eventos al asignar input.value; los eventos de entrada se originan en la interacción del usuario.
24.13 Resultados derivados
Calculamos una instantánea coherente para usarla tanto en Canvas como en el panel textual.
const punto=(a,b)=>a.x*b.x+a.y*b.y;
const modulo=v=>Math.hypot(v.x,v.y);
function medir(a,b) {
const ma=modulo(a), mb=modulo(b);
const coseno=ma===0||mb===0 ? null :
Math.max(-1,Math.min(1,punto(a,b)/(ma*mb)));
return { moduloA:ma, moduloB:mb, producto:punto(a,b),
angulo:coseno===null?null:Math.acos(coseno) };
}
console.log(medir({x:1,y:0},{x:0,y:2}));24.14 Exportar el estado
JSON permite copiar una configuración y recuperarla después. Exportamos únicamente datos simples y una versión del formato.
function exportar(estado) {
return JSON.stringify({
version:1,
a:estado.a,
b:estado.b,
operacion:estado.operacion,
escala:estado.camara.escala,
mostrarComponentes:estado.mostrarComponentes
},null,2);
}
console.log(exportar({a:{x:3,y:2},b:{x:-1,y:4},
operacion:"sumar",camara:{escala:40},mostrarComponentes:true}));24.15 Importar sin confiar ciegamente
JSON.parse solo comprueba la sintaxis. Después debemos validar versión, componentes finitas, operación permitida y rango de escala antes de modificar el estado actual.
function esVector(v) {
return v!==null && Number.isFinite(v.x) && Number.isFinite(v.y);
}
function validar(datos) {
const operaciones=["sumar","restar","proyectar","reflejar"];
if (datos.version!==1 || !esVector(datos.a) || !esVector(datos.b)) return false;
if (!operaciones.includes(datos.operacion)) return false;
return Number.isFinite(datos.escala) && datos.escala>=20 && datos.escala<=80;
}
console.log(validar({version:1,a:{x:1,y:2},b:{x:3,y:4},
operacion:"sumar",escala:40}));
console.log(validar({version:1,a:{x:"1",y:2},b:{x:3,y:4},
operacion:"sumar",escala:40}));24.16 Historial y acciones reversibles
Una mejora natural es deshacer cambios. Antes de una acción significativa guardamos una copia del estado. No basta con guardar referencias a los mismos objetos.
const copiarEstado=e=>({
...e,
a:{...e.a},
b:{...e.b},
camara:{...e.camara}
});
const original={a:{x:1,y:2},b:{x:3,y:4},camara:{escala:40}};
const copia=copiarEstado(original);
copia.a.x=99;
console.log("Original:",original.a.x);
console.log("Copia:",copia.a.x);24.17 Pruebas del proyecto
Las pruebas se distribuyen por nivel:
- Unitarias: suma, resta, producto, proyección y transformaciones.
- De propiedades:
a + 0 = a,a − a = 0y producto escalar simétrico. - De integración: un cambio del control actualiza estado, resultados y Canvas.
- Manuales: arrastre con mouse y tacto, zoom, redimensionamiento y navegación por teclado.
- Casos límite: vectores nulos, paralelos, opuestos y componentes extremas.
const sumar=(a,b)=>({x:a.x+b.x,y:a.y+b.y});
const punto=(a,b)=>a.x*b.x+a.y*b.y;
const a={x:2.5,y:-7}, cero={x:0,y:0};
console.log("Neutro de la suma:",JSON.stringify(sumar(a,cero))===JSON.stringify(a));
console.log("Simetría del producto:",punto(a,{x:3,y:1})===punto({x:3,y:1},a));24.18 Laboratorio vectorial completo
Arrastrá las puntas azul y rosa o usá los controles. La flecha amarilla representa la operación seleccionada. El editor JSON permite conservar y recuperar configuraciones.
—Laboratorio listo. Arrastrá una punta o modificá un control.
24.19 Decisiones observables en el laboratorio
- El resultado se vuelve
nullsi una proyección o reflexión usa b nulo. - La reflexión normaliza b antes de utilizarlo como normal.
- El ángulo queda “no definido” cuando algún vector no tiene dirección.
- El zoom cambia la cámara sin cambiar componentes.
- El historial registra cambios al comenzar un arrastre o ejecutar una acción.
- La importación valida antes de sustituir el estado.
- Los resultados textuales reflejan la misma instantánea que Canvas.
24.20 Mejoras posibles
Una versión posterior podría incorporar múltiples vectores, traslación de cámara, ajuste a cuadrícula, coordenadas polares, vectores 3D proyectados, fuerzas sobre cuerpos, exportación de imágenes, enlaces compartibles o pruebas automatizadas en el navegador.
Cada ampliación debería preservar el núcleo pequeño. Por ejemplo, la física de cuerpos pertenece a un módulo que consume vectores; no debe modificar las reglas básicas de suma o producto escalar.
24.21 Errores frecuentes
- Guardar resultados derivados y permitir que queden desactualizados.
- Duplicar fórmulas en el dibujo y en el panel textual.
- Usar píxeles como componentes del modelo.
- Modificar directamente Canvas sin actualizar el estado.
- Importar JSON sin validar tipos ni rangos.
- Guardar referencias en el historial en lugar de copias.
- No tratar vectores nulos en proyecciones y ángulos.
- Confundir reflexión respecto de una normal con reflexión respecto de una recta.
- Depender únicamente del color para diferenciar resultados.
- Agregar funcionalidades sin pruebas de regresión.
24.22 Desafío final
Extendé el laboratorio con una tercera operación: descomponer a en una componente paralela a b y otra perpendicular. Dibujá ambas desde el origen y verificá que su suma recupere a dentro de una tolerancia.
Ver núcleo de la solución
const sumar=(a,b)=>({x:a.x+b.x,y:a.y+b.y});
const restar=(a,b)=>({x:a.x-b.x,y:a.y-b.y});
const escalar=(v,k)=>({x:v.x*k,y:v.y*k});
const punto=(a,b)=>a.x*b.x+a.y*b.y;
function descomponer(a,b,tol=1e-10) {
const m2=punto(b,b);
if (m2<=tol*tol) return null;
const paralela=escalar(b,punto(a,b)/m2);
return {paralela,perpendicular:restar(a,paralela)};
}
const a={x:4,y:3},b={x:2,y:1},partes=descomponer(a,b);
console.log("Partes:",partes);
console.log("Reconstrucción:",sumar(partes.paralela,partes.perpendicular));24.23 Cierre del curso
A lo largo del curso pasamos de una flecha con módulo, dirección y sentido a una biblioteca capaz de sostener visualizaciones y simulaciones. Aprendimos a operar en dos y tres dimensiones, proyectar, cambiar de base, representar datos, controlar la precisión y conectar fuerzas con movimiento.
El resultado más importante no es una fórmula aislada, sino un método: definir referencias y unidades, modelar con vectores, implementar contratos claros, comprobar invariantes y comunicar los resultados visualmente.
- La geometría explica qué significa cada operación.
- El álgebra permite calcularla mediante componentes.
- La programación convierte esas reglas en herramientas reutilizables.
- Las pruebas y tolerancias hacen confiables los resultados.
- La visualización permite explorar, detectar errores y construir intuición.