Nivel avanzado
Código acumulativo para producción
En este nivel trabajamos con tres archivos: index.html, app.js y data/content.json. Cada lección dice exactamente qué archivo tocar y muestra cómo queda completo en ese momento.
Aula privada
Construye una experiencia WebAR avanzada con código acumulativo, JSON, fetch, errores, estado, rendimiento y Three.js.
Nivel avanzado
En este nivel trabajamos con tres archivos: index.html, app.js y data/content.json. Cada lección dice exactamente qué archivo tocar y muestra cómo queda completo en ese momento.
HTML base
Este HTML contiene la UI, la escena MindAR y el modelo que luego será controlado por JavaScript.
index.html y crea o reemplaza el archivo completo por esta versión.<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebAR avanzado con JavaScript</title>
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mind-ar@1.2.5/dist/mindar-image-aframe.prod.js"></script>
<script src="app.js" defer></script>
</head>
<body>
<div class="ui">
<h1 id="status">Cargando experiencia...</h1>
<p id="fps">FPS aprox: --</p>
</div>
<a-scene mindar-image="imageTargetSrc: targets/avanzado.mind; autoStart: true;" vr-mode-ui="enabled: false" device-orientation-permission-ui="enabled: false">
<a-camera position="0 0 0" look-controls="enabled: false"></a-camera>
<a-entity id="target" mindar-image-target="targetIndex: 0">
<a-box id="model" color="#0ea5e9" position="0 0 0" visible="false"></a-box>
</a-entity>
</a-scene>
</body>
</html>
JSON
El contenido ya no estará escrito dentro de app.js. Lo guardaremos en un archivo JSON para practicar carga de datos externos.
data/content.json y crea la carpeta data, luego crea content.json y pega este contenido.{
"products": [
{
"title": "Café Aurora",
"color": "#facc15",
"scale": 0.35,
"rotationSpeed": 0.01
}
]
}
Estado inicial
Ahora creamos el archivo JavaScript. Primero conectamos los elementos del HTML y declaramos un objeto state.
querySelectorCada línea con document.querySelector() conecta JavaScript con un elemento del HTML. El texto entre comillas debe coincidir con el id del elemento: #target busca id="target", #model busca id="model" y así sucesivamente.
app.js y crea el archivo desde la línea 1 con este contenido.const target = document.querySelector("#target");
const model = document.querySelector("#model");
const statusText = document.querySelector("#status");
const fpsText = document.querySelector("#fps");
const state = {
targetVisible: false,
products: [],
activeProduct: null
};
fetch
Esta función carga data/content.json. Debe ir debajo del objeto state.
async, await y fetch()fetch() pide un archivo o una URL al servidor. En este curso lo usamos para cargar data/content.json. Como esa petición tarda un poco, usamos async y await.
async indica que una función puede esperar procesos.await pausa esa función hasta que la respuesta llegue.response.json() convierte la respuesta en datos que JavaScript puede usar.app.js y agrega la función después de la línea 10. Aquí ves el archivo completo.const target = document.querySelector("#target");
const model = document.querySelector("#model");
const statusText = document.querySelector("#status");
const fpsText = document.querySelector("#fps");
const state = {
targetVisible: false,
products: [],
activeProduct: null
};
async function loadContent() {
const response = await fetch("data/content.json");
if (!response.ok) {
throw new Error("No se pudo cargar data/content.json");
}
const data = await response.json();
return data.products;
}
setupScene
Agregamos una función que recibe productos y configura el modelo con el primer producto.
setAttribute()setAttribute() cambia un atributo de un elemento HTML. En A-Frame es muy importante porque los objetos 3D también se configuran con atributos.
Por ejemplo, box.setAttribute("visible", "true") busca el atributo visible de la caja y lo cambia a true. Eso hace que el objeto aparezca en la escena.
setAttribute("visible", "false") oculta un objeto.setAttribute("color", "#22c55e") cambia el color.setAttribute("position", "0 0 0") cambia la posición en X, Y, Z.object3Dobject3D es la puerta desde A-Frame hacia Three.js. A-Frame te deja escribir una escena con etiquetas HTML, pero por debajo cada entidad tiene un objeto real de Three.js. Ese objeto vive en elemento.object3D.
Cuando usamos box.object3D.scale.set(...), box.object3D.rotation.y o box.object3D.visible, estamos modificando el objeto 3D directamente desde Three.js.
app.js y agrega esta función debajo de loadContent.const target = document.querySelector("#target");
const model = document.querySelector("#model");
const statusText = document.querySelector("#status");
const fpsText = document.querySelector("#fps");
const state = {
targetVisible: false,
products: [],
activeProduct: null
};
async function loadContent() {
const response = await fetch("data/content.json");
if (!response.ok) {
throw new Error("No se pudo cargar data/content.json");
}
const data = await response.json();
return data.products;
}
function setupScene(products) {
state.products = products;
state.activeProduct = products[0];
model.setAttribute("color", state.activeProduct.color);
model.object3D.scale.set(
state.activeProduct.scale,
state.activeProduct.scale,
state.activeProduct.scale
);
statusText.textContent = "Escanea el target";
}
Errores y eventos
Ahora agregamos el arranque seguro con try/catch y los eventos targetFound/targetLost. Todo va al final de app.js.
app.js y pega este bloque al final. Aquí queda acumulado.const target = document.querySelector("#target");
const model = document.querySelector("#model");
const statusText = document.querySelector("#status");
const fpsText = document.querySelector("#fps");
const state = {
targetVisible: false,
products: [],
activeProduct: null
};
async function loadContent() {
const response = await fetch("data/content.json");
if (!response.ok) {
throw new Error("No se pudo cargar data/content.json");
}
const data = await response.json();
return data.products;
}
function setupScene(products) {
state.products = products;
state.activeProduct = products[0];
model.setAttribute("color", state.activeProduct.color);
model.object3D.scale.set(
state.activeProduct.scale,
state.activeProduct.scale,
state.activeProduct.scale
);
statusText.textContent = "Escanea el target";
}
async function startExperience() {
try {
const products = await loadContent();
setupScene(products);
} catch (error) {
console.error(error);
statusText.textContent = "Error cargando datos. Revisa data/content.json y usa servidor local.";
}
}
target.addEventListener("targetFound", () => {
state.targetVisible = true;
model.object3D.visible = true;
});
target.addEventListener("targetLost", () => {
state.targetVisible = false;
model.object3D.visible = false;
});
Rendimiento
Este es el último bloque del archivo. Mide FPS aproximados y solo rota el modelo cuando el target está visible.
requestAnimationFrame()requestAnimationFrame() le pide al navegador ejecutar una función justo antes del siguiente dibujo de pantalla. Es la forma correcta de crear animaciones fluidas en web.
En WebAR lo usamos con cuidado: si el target no está visible, evitamos animar el modelo para no gastar recursos innecesarios del celular.
app.js y agrega este bloque al final del archivo. Aquí ves el código completo final.const target = document.querySelector("#target");
const model = document.querySelector("#model");
const statusText = document.querySelector("#status");
const fpsText = document.querySelector("#fps");
const state = {
targetVisible: false,
products: [],
activeProduct: null
};
async function loadContent() {
const response = await fetch("data/content.json");
if (!response.ok) {
throw new Error("No se pudo cargar data/content.json");
}
const data = await response.json();
return data.products;
}
function setupScene(products) {
state.products = products;
state.activeProduct = products[0];
model.setAttribute("color", state.activeProduct.color);
model.object3D.scale.set(
state.activeProduct.scale,
state.activeProduct.scale,
state.activeProduct.scale
);
statusText.textContent = "Escanea el target";
}
async function startExperience() {
try {
const products = await loadContent();
setupScene(products);
} catch (error) {
console.error(error);
statusText.textContent = "Error cargando datos. Revisa data/content.json y usa servidor local.";
}
}
target.addEventListener("targetFound", () => {
state.targetVisible = true;
model.object3D.visible = true;
});
target.addEventListener("targetLost", () => {
state.targetVisible = false;
model.object3D.visible = false;
});
let lastTime = performance.now();
let frames = 0;
function animate(now) {
frames = frames + 1;
if (state.targetVisible && state.activeProduct) {
model.object3D.rotation.y += state.activeProduct.rotationSpeed;
}
if (now - lastTime >= 1000) {
fpsText.textContent = "FPS aprox: " + frames;
frames = 0;
lastTime = now;
}
requestAnimationFrame(animate);
}
startExperience();
requestAnimationFrame(animate);
Código final
Al terminar, el proyecto debe tener estos tres archivos completos.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebAR avanzado con JavaScript</title>
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mind-ar@1.2.5/dist/mindar-image-aframe.prod.js"></script>
<script src="app.js" defer></script>
</head>
<body>
<div class="ui">
<h1 id="status">Cargando experiencia...</h1>
<p id="fps">FPS aprox: --</p>
</div>
<a-scene mindar-image="imageTargetSrc: targets/avanzado.mind; autoStart: true;" vr-mode-ui="enabled: false" device-orientation-permission-ui="enabled: false">
<a-camera position="0 0 0" look-controls="enabled: false"></a-camera>
<a-entity id="target" mindar-image-target="targetIndex: 0">
<a-box id="model" color="#0ea5e9" position="0 0 0" visible="false"></a-box>
</a-entity>
</a-scene>
</body>
</html>
{
"products": [
{
"title": "Café Aurora",
"color": "#facc15",
"scale": 0.35,
"rotationSpeed": 0.01
}
]
}
const target = document.querySelector("#target");
const model = document.querySelector("#model");
const statusText = document.querySelector("#status");
const fpsText = document.querySelector("#fps");
const state = {
targetVisible: false,
products: [],
activeProduct: null
};
async function loadContent() {
const response = await fetch("data/content.json");
if (!response.ok) {
throw new Error("No se pudo cargar data/content.json");
}
const data = await response.json();
return data.products;
}
function setupScene(products) {
state.products = products;
state.activeProduct = products[0];
model.setAttribute("color", state.activeProduct.color);
model.object3D.scale.set(
state.activeProduct.scale,
state.activeProduct.scale,
state.activeProduct.scale
);
statusText.textContent = "Escanea el target";
}
async function startExperience() {
try {
const products = await loadContent();
setupScene(products);
} catch (error) {
console.error(error);
statusText.textContent = "Error cargando datos. Revisa data/content.json y usa servidor local.";
}
}
target.addEventListener("targetFound", () => {
state.targetVisible = true;
model.object3D.visible = true;
});
target.addEventListener("targetLost", () => {
state.targetVisible = false;
model.object3D.visible = false;
});
let lastTime = performance.now();
let frames = 0;
function animate(now) {
frames = frames + 1;
if (state.targetVisible && state.activeProduct) {
model.object3D.rotation.y += state.activeProduct.rotationSpeed;
}
if (now - lastTime >= 1000) {
fpsText.textContent = "FPS aprox: " + frames;
frames = 0;
lastTime = now;
}
requestAnimationFrame(animate);
}
startExperience();
requestAnimationFrame(animate);
Template
El template final incluye estructura con data/content.json, carga asíncrona, manejo de errores, eventos MindAR y loop de rendimiento.
Base avanzada con JSON, fetch, estado y Three.js.
Descargar template avanzado