
LocAR.js es una librería especializada en la creación de Realidad Aumentada (RA) basada en la ubicación (Location-Based AR) para navegadores web utilizando JavaScript. No es una librería totalmente nueva, sino que nació como un proyecto independiente dentro de la organización AR.js.
Esta tecnología utiliza los sensores del dispositivo para posicionar objetos virtuales en coordenadas geográficas del mundo real (latitud y longitud).
Su desarrolló inicio partir del código de realidad aumentada basada en la ubicación del repositorio principal de AR.js. Se separó para enfocar su desarrollo exclusivamente en la funcionalidad de geolocalización, lo que permite actualizaciones más frecuentes y compatibilidad con las últimas versiones de librerías como Three.js sin afectar las funciones de rastreo de marcadores (marker-based) de AR.js.
El concepto detrás de LocAR.js no nació de la noche a la mañana. Es la evolución de varios esfuerzos de código abierto:
- 2017 – El nacimiento de AR.js: Jerome Etienne crea AR.js, revolucionando la web al permitir AR fluida en navegadores móviles usando three.js y A-Frame. Inicialmente, solo funcionaba con marcadores visuales (Hiro/Kanji).
- 2019 – La integración del GPS: Desarrolladores como Nicolò Carpignoli comenzaron a experimentar con la fusión de datos GPS dentro del ecosistema de AR.js. Esto dio lugar a lo que muchos denominan «Location AR» o LocAR.
- Actualidad: Hoy en día, las librerías modernas de WebAR (como la versión 3 de AR.js) integran módulos nativos de Location-Based, permitiendo a estudiantes y desarrolladores crear experiencias tipo «Pokémon GO» sin instalar aplicaciones nativas.
¿Cómo funciona?
Para entender LocAR.js, debe comprender la Fusión de Sensores. La librería combina tres fuentes de datos en tiempo real:
- Geolocalización (API del Navegador)
Obtiene la Latitud y Longitud del usuario.
Dato Curioso: El GPS civil típico tiene un margen de error de 5 a 15 metros. Por eso, en LocAR.js, los objetos a veces «tiemblan» o se mueven ligeramente; es el GPS recalculando la posición.
Te puede interesar este post donde explico más detalladamente el último punto: Problemas con el GPS y geolocalización en realidad aumentada
- Orientación del Dispositivo (Brújula/Magnetómetro)
No basta saber dónde estás, la librería necesita saber hacia dónde miras.
Usa el evento deviceorientation de HTML5.
Calcula el «Norte Magnético» para alinear la cámara virtual con el mundo real.
- La Fórmula de Haversine (Matemáticas)
Función: Calcula la distancia del círculo máximo entre dos puntos de una esfera (la Tierra) dadas sus longitudes y latitudes.
Resultado: Si la fórmula dice que el objeto está a 500 metros, la librería reduce la escala del objeto 3D proporcionalmente.
¿Que se debe de tener en cuenta para desarollar este proyecto?
- Sin Instalación: La mayor ventaja es que corre en el navegador (Chrome, Safari, Firefox).
- HTTPS es Obligatorio: Por seguridad, los navegadores bloquean el acceso al GPS y a la cámara si el sitio no tiene certificado SSL (https://).
- El problema de la Altitud: La mayoría de las librerías de LocAR.js ignoran la altitud (eje Y) o la fijan en 0, debido a que el GPS es muy impreciso calculando la altura sobre el nivel del mar.
Hagamos un ejemplo:
Es de vital importancia que modifiques las siguientes líneas de código con tu latitud y longitud. Inicia agregando algún objeto a menos de 100 metros tuyos.
Yo utilice Google Maps para esta tarea: Google Maps
const target = {
latitude: 4.4388842, // Latitud modifica esta línea de código agregando la latitud.
longitude: -75.23222 // Longitud modifica esta línea de código agregando la longitud.
};Aquí comenzamos con toda la WebAR.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>WebAR Educativo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="debug">Iniciando sistema...</div>
<button id="start-btn">INICIAR EXPERIENCIA AR</button>
<div id="camara-container">
<video id="webcam" autoplay playsinline muted></video>
</div>
<div id="ar-overlay">
<div id="target-poi" class="ar-object" style="display:none;">
<div class="dist-label" id="dist-text">0m</div>
<div class="pin-image">📍</div>
</div>
</div>
<script type="module">
// Importamos la clase que realiza los cálculos matemáticos
import LocAR from './locar.js';
// Referencias a los elementos del DOM (HTML)
const debug = document.getElementById('debug');
const poi = document.getElementById('target-poi');
const distLabel = document.getElementById('dist-text');
const btn = document.getElementById('start-btn');
let locarInstance; // Variable para almacenar la instancia de la librería
// ------------------------------------------------------------------
// CONFIGURACIÓN DE COORDENADAS OBJETIVO
// ------------------------------------------------------------------
// Estas son las coordenadas Lat/Lon donde aparecerá el objeto virtual.
const target = {
latitude: 4.4388842, // Latitud del destino
longitude: -75.23222 // Longitud del destino
};
/**
* FUNCIÓN: init()
* Inicializa la cámara y los sensores GPS.
*/
async function init() {
// Ocultamos el botón de inicio una vez pulsado
btn.style.display = 'none';
try {
// 1. ACCESO A LA CÁMARA
// navigator.mediaDevices.getUserMedia solicita el stream de video.
// facingMode: "environment" solicita específicamente la cámara trasera.
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" }
});
// Asignamos el stream de la cámara al elemento <video> del HTML
document.getElementById('webcam').srcObject = stream;
// 2. INICIALIZACIÓN DE LA LIBRERÍA
// Creamos una instancia de LocAR pasándole el video para cálculos de FOV (opcional en futuras mejoras)
locarInstance = new LocAR(document.getElementById('webcam'));
// Iniciamos la escucha del GPS
locarInstance.startGPS();
// 3. INICIO DEL BUCLE DE RENDERIZADO
loop();
} catch (e) {
alert("Error al iniciar: " + e.message);
}
}
/**
* FUNCIÓN: loop()
* Bucle de renderizado que se ejecuta aprox. 60 veces por segundo.
* Actualiza la posición del objeto AR en pantalla basándose en los sensores.
*/
function loop() {
// Si la librería no está lista, volvemos a intentar en el siguiente frame
if(!locarInstance) return requestAnimationFrame(loop);
// LLAMADA CLAVE: Pedimos a la librería que calcule la posición X, Y
// basándose en nuestra ubicación actual y la del objetivo.
const pos = locarInstance.screenPosition(target.latitude, target.longitude);
// Actualizamos el panel de depuración con datos crudos
debug.innerHTML = `
Distancia: ${Math.round(pos.distance)}m <br>
Coord X: ${Math.round(pos.x)} | Coord Y: ${Math.round(pos.y)} <br>
Visible: ${pos.visible}
`;
// Si la distancia es mayor a 0 (significa que el GPS ya obtuvo lectura válida)
if(pos.distance > 0) {
poi.style.display = 'flex'; // Hacemos visible el elemento
// 1. POSICIONAMIENTO 2D
// Asignamos las coordenadas calculadas al estilo CSS.
poi.style.left = pos.x + 'px';
poi.style.top = pos.y + 'px';
// 2. ESCALADO INTELIGENTE (Efecto de Profundidad)
// Cuanto más lejos está el objeto, más pequeño debe ser.
// Fórmula base: 50 dividido por la distancia.
let scale = 50 / pos.distance;
// "Clamping": Limitamos el tamaño mínimo y máximo para que sea utilizable.
if(scale < 0.8) scale = 0.8; // Nunca más pequeño que 0.8x
if(scale > 2) scale = 2; // Nunca más grande que 2x
// Aplicamos la transformación CSS
// translate(-50%, -100%) asegura que la punta inferior del pin esté en la coordenada exacta.
poi.style.transform = `translate(-50%, -100%) scale(${scale})`;
// 3. GESTIÓN DE VISIBILIDAD
// Si 'pos.visible' es true, el objeto está frente a la cámara.
if(pos.visible) {
poi.style.opacity = 1; // Totalmente visible
} else {
// Si el objeto está detrás del usuario o fuera de ángulo,
// lo hacemos semitransparente (o podríamos ocultarlo con opacity 0).
poi.style.opacity = 0.1;
}
// Actualizamos el texto de la etiqueta
distLabel.innerText = Math.round(pos.distance) + "m";
}
// Solicitamos al navegador que ejecute esta función de nuevo en el próximo refresco de pantalla
requestAnimationFrame(loop);
}
// ------------------------------------------------------------------
// GESTIÓN DE PERMISOS (IOS / ANDROID)
// ------------------------------------------------------------------
btn.addEventListener('click', () => {
// iOS 13+ requiere solicitar permiso para usar el acelerómetro/giroscopio
if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
DeviceOrientationEvent.requestPermission()
.then(response => {
if (response === 'granted') {
init(); // Permiso concedido, iniciamos
} else {
alert('Permiso de orientación denegado. La AR no funcionará.');
}
})
.catch(console.error);
} else {
// Dispositivos Android y navegadores antiguos no requieren este paso extra
init();
}
});
</script>
</body>
</html>pasemos a explicar los estilos CSS (Recuerda que tu puedes modificarlos).
/* DOCUMENTO: style.css
DESCRIPCIÓN: Estilos para la superposición de capas y diseño del objeto AR.
*/
/* Reset básico del cuerpo de la página.
overflow: hidden; es CRÍTICO para evitar barras de desplazamiento cuando
el objeto AR se mueve fuera de la pantalla.
*/
body {
margin: 0;
overflow: hidden;
background-color: #000; /* Fondo negro mientras carga la cámara */
font-family: Arial, sans-serif;
}
/* CAPA 1: VIDEO (Fondo)
z-index: 1 -> La capa más baja.
object-fit: cover -> Asegura que el video llene toda la pantalla sin deformarse.
*/
#camara-container {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
}
video {
width: 100%; height: 100%;
object-fit: cover;
}
/* CAPA 2: INTERFAZ AR (Frente)
z-index: 2 -> Encima del video.
pointer-events: none -> IMPORTANTE: Permite que los toques del usuario "atraviesen"
esta capa vacía. Si no se pone esto, no se podría interactuar con nada debajo.
*/
#ar-overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 2;
pointer-events: none;
overflow: hidden;
}
/* OBJETO VIRTUAL
Contenedor del elemento gráfico.
will-change: transform -> Avisa al navegador que este elemento se moverá mucho,
optimizando el rendimiento gráfico.
*/
.ar-object {
position: absolute;
will-change: transform;
display: flex;
flex-direction: column;
align-items: center;
/* Define el punto de anclaje (pivot) en la parte inferior central */
transform-origin: bottom center;
}
/* DISEÑO VISUAL DEL PIN
Estilos estéticos para el icono.
*/
.pin-image {
font-size: 50px;
filter: drop-shadow(0px 4px 6px rgba(0,0,0,0.5)); /* Sombra para contraste */
/* Animación simple para dar vida al objeto */
animation: bounce 2s infinite;
}
/* Etiqueta de texto (Metros) */
.dist-label {
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
margin-bottom: 5px;
white-space: nowrap; /* Evita que el texto se parta en dos líneas */
}
/* Definición de la animación de rebote */
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
/* Estilos para utilidades de UI (Botón y Debug) */
#debug {
position: absolute; top: 10px; left: 10px;
color: lime; background: rgba(0,0,0,0.5);
padding: 5px; z-index: 99; font-size:12px;
border-radius: 5px;
}
#start-btn {
position: absolute; bottom: 50px; left: 50%;
transform: translateX(-50%); /* Centrado horizontal perfecto */
z-index: 100;
padding: 15px 30px;
background: #007bff; color: white;
border: none; border-radius: 50px;
font-size: 18px;
box-shadow: 0 4px 15px rgba(0,123,255,0.4);
cursor: pointer;
}y para finalizar pasemos al javascript, que en este caso es llamado locar.js:
/*
DOCUMENTO: locar.js
DESCRIPCIÓN: Módulo JavaScript (ES6 Class) encargado de los cálculos matemáticos de geolocalización y proyección.
NOTA: Este archivo no manipula el DOM, solo recibe datos y devuelve coordenadas.
*/
export default class LocAR {
/**
* Constructor de la clase.
* @param {HTMLElement} videoElement - Referencia al video (útil si se desea calcular FOV dinámico en el futuro).
*/
constructor(videoElement) {
this.video = videoElement;
// FOV (Field of View): Campo de visión horizontal estimado del dispositivo.
// 60 grados es un estándar aproximado para cámaras de celulares.
this.fov = 60;
// Objeto para almacenar la orientación actual del dispositivo.
this.deviceOrientation = { alpha: 0, beta: 0, gamma: 0 };
// Objeto para almacenar la posición GPS actual (Lat/Lon).
this.currentPosition = null;
// Listener: Escucha cambios en el giroscopio/acelerómetro del dispositivo.
// Se actualiza múltiples veces por segundo.
window.addEventListener('deviceorientation', (e) => {
this.deviceOrientation = {
alpha: e.alpha, // Dirección Brújula (0-360 grados)
beta: e.beta, // Inclinación Frontal (-180 a 180 grados)
gamma: e.gamma, // Inclinación Lateral (-90 a 90 grados)
webkitCompassHeading: e.webkitCompassHeading // Propiedad específica para Safari iOS
};
});
}
/**
* Inicia el rastreo del GPS.
* Utiliza watchPosition para recibir actualizaciones continuas cuando el usuario camina.
*/
startGPS() {
if ("geolocation" in navigator) {
navigator.geolocation.watchPosition((position) => {
// Guardamos la nueva posición cada vez que el GPS detecta movimiento
this.currentPosition = {
latitude: position.coords.latitude,
longitude: position.coords.longitude
};
}, (err) => console.error("Error GPS:", err), {
enableHighAccuracy: true, // Solicita el GPS de alta precisión (consume más batería)
maximumAge: 0, // No usar caché de posiciones antiguas
timeout: 5000 // Tiempo de espera máximo
});
}
}
/**
* Calcula la posición en pantalla (X, Y) de un punto geográfico destino.
* @param {number} lat - Latitud del destino.
* @param {number} lon - Longitud del destino.
* @returns {object} {x, y, visible, distance}
*/
screenPosition(lat, lon) {
// Si aún no tenemos GPS, retornamos valores vacíos
if (!this.currentPosition) return { x: 0, y: 0, visible: false, distance: 0 };
// -------------------------------------------------------
// FASE 1: CÁLCULO DE DISTANCIA (Fórmula de Haversine)
// -------------------------------------------------------
const R = 6371000; // Radio de la Tierra en metros
const rad = Math.PI / 180; // Factor de conversión Grados -> Radianes
const lat1 = this.currentPosition.latitude * rad;
const lat2 = lat * rad;
const dLat = (lat - this.currentPosition.latitude) * rad;
const dLon = (lon - this.currentPosition.longitude) * rad;
// Algoritmo para calcular distancia sobre una esfera
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
const distance = R * c; // Distancia final en metros
// -------------------------------------------------------
// FASE 2: CÁLCULO DEL RUMBO (BEARING)
// -------------------------------------------------------
// Calcula el ángulo necesario para mirar desde el origen hacia el destino (0-360 grados)
const y = Math.sin(dLon) * Math.cos(lat2);
const x = Math.cos(lat1) * Math.sin(lat2) -
Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
let bearing = Math.atan2(y, x) * 180 / Math.PI;
bearing = (bearing + 360) % 360; // Normalización para evitar ángulos negativos
// -------------------------------------------------------
// FASE 3: OBTENER ORIENTACIÓN REAL DEL DISPOSITIVO
// -------------------------------------------------------
let alpha = this.deviceOrientation.alpha;
// Corrección crítica para iOS (El norte magnético se reporta diferente en Webkit)
if(this.deviceOrientation.webkitCompassHeading) {
alpha = this.deviceOrientation.webkitCompassHeading;
}
// -------------------------------------------------------
// FASE 4: PROYECCIÓN HORIZONTAL (Eje X)
// -------------------------------------------------------
// Diferencia entre adónde debo mirar (bearing) y adónde estoy mirando (alpha)
let angleDiff = bearing - alpha;
// Normalización matemática para encontrar el camino más corto (-180 a 180)
// Ejemplo: Si bearing es 350° y alpha es 10°, la diferencia no es 340°, es -20°.
if (angleDiff < -180) angleDiff += 360;
if (angleDiff > 180) angleDiff -= 360;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
// Calculamos cuántos píxeles representa cada grado de giro
const pxPerDegreeX = screenWidth / this.fov;
// Posición X: Centro de pantalla + (Diferencia de ángulo * Píxeles por grado)
const xPos = (screenWidth / 2) + (angleDiff * pxPerDegreeX);
// -------------------------------------------------------
// FASE 5: PROYECCIÓN VERTICAL (Eje Y) - Corrección de Inclinación
// -------------------------------------------------------
let beta = this.deviceOrientation.beta;
if (beta === null) beta = 90; // Valor por defecto si falla el sensor
// Asumimos que 90 grados es el teléfono vertical mirando al horizonte.
// Calculamos cuánto se desvía el teléfono de la vertical.
const angleDiffY = beta - 90;
// Ajustamos el FOV vertical proporcional al aspect ratio de la pantalla
const fovY = this.fov * (screenHeight / screenWidth);
const pxPerDegreeY = screenHeight / fovY;
// Posición Y: Centro + Desviación vertical
const yPos = (screenHeight / 2) + (angleDiffY * pxPerDegreeY);
// -------------------------------------------------------
// FASE 6: DETERMINAR VISIBILIDAD
// -------------------------------------------------------
// El objeto es visible horizontalmente si el ángulo está dentro de la mitad del FOV
const visibleX = Math.abs(angleDiff) < (this.fov / 2);
// Margen extra de 100px verticalmente
const visibleY = yPos > -100 && yPos < (screenHeight + 100);
return {
x: xPos,
y: yPos,
visible: visibleX && visibleY, // Solo es true si cumple ambas condiciones
distance: distance
};
}
}Expliquemos un poco mejor cómo funciona todo eso, locar.js es una librería fácil de trabajar, pero al comentar cada línea de código se hace largo y tedioso visualmente.
Te doy más información sobre toda esta aplicación de realidad aumentada que utilizar el GPS y posicionamiento en tiempo real:
Sistema de Coordenadas (Geolocalización)
- La aplicación utiliza el sistema estándar WGS84 (Latitud y Longitud).
- Latitud: Posición Norte/Sur.
- Longitud: Posición Este/Oeste.
- El objetivo matemático es convertir estas dos coordenadas geográficas en píxeles (X, Y) en la pantalla del dispositivo.
Orientación del Dispositivo (API DeviceOrientation) Para saber hacia dónde mira el usuario, se utilizan tres ejes de rotación:
- Alpha (α): Brújula. 0° es Norte, 90° Este, 180° Sur, 270° Oeste. (Eje Z).
- Beta (β): Inclinación frontal. 90° es el teléfono vertical, 0° es el teléfono acostado sobre una mesa. (Eje X).
- Gamma (γ): Inclinación lateral (izquierda/derecha). (Eje Y).
- Nota: En iOS (iPhone), se requiere un permiso explícito del usuario para acceder a estos datos.
Proyección de Cámara (FOV)
- Para superponer un objeto digital sobre la imagen de la cámara, debemos simular el Campo de Visión (Field of View – FOV).
- Se asume un FOV vertical promedio de 60 grados para teléfonos móviles.
- Si la diferencia angular entre la brújula del teléfono y la ubicación del objetivo está dentro de este rango de 60 grados, el objeto se renderiza en pantalla.
No quieres copiar y pegar los códigos HTML, CSS y Javascript. Encuéntralos para su descarga directa en mi Patreon.
Recuerda que puedes hacerte Patreon desde 1 dólar. Muchas gracias por tu ayuda y apoyo.
DESCARGA DANDO CLIC EN LA IMAGEN – El link esta direccionado hacia mi Patreon.
